前置き
皆さん、CI・CD機能が追加されたGitHub Actionsを使っていますか?
私はステージングや本番環境へデプロイするためにGitHub Actionsを使っています。
新しくなったGitHub Actionsによって様々な恩恵を受けていますが、CI・CDの経過や結果の通知に困っていました。今までは、既に組み込まれていたメールの通知機能を使ってわざわざ結果を確認したり、その場しのぎでcurlコマンドを使ってSlackへ投稿したりしていました。さすがにしんどくなったので、結果をSlackに通知するためのactionを開発してみました。
開発環境
- MacOS 10.14
- TypeScript 3.5.1
開発の流れ
actionを開発するざっくりとした流れは↓のようになります。GitHubがactionの開発をサポートするためにテンプレートを既に用意しているので、それを基に開発していきます。
- actionの実行環境を決める
- テンプレートからリポジトリを作成 (javascript-template、container-toolkit-template、container-template)
- actionの定義
- いざ、開発!
- ビルド
- リリース
今回は3と4について説明していきます!
action定義
actionの定義はYAMLで書きます。何を定義するのかというと、actionの名前や入力値などです。実際に書いたYAMLを例に説明していきます。
name: 'Slatify'
description: 'Slack Notification for Github Actions'
author: 'homoluctus'
inputs:
type:
description: 'job status (success or failure)'
required: true
job_name:
description: 'job name of workflow (format: markdown)'
required: true
username:
description: 'slack username'
required: false
default: 'Github Actions'
icon_emoji:
description: 'slack icon emoji'
required: false
default: 'github'
channel:
description: 'slack channel'
required: false
default: '#general'
runs:
using: 'node12'
main: 'lib/main.js'
branding:
icon: 'bell'
color: 'white'
name
開発したactionの名前です。GitHub Actionsの環境変数GITHUB_ACTION
の構成要素として使われたり、GitHub Actions Marketplaceでの表示名として使われます。
description
actionの説明を書きます。
author
actionの開発者名を書きましょう。
inputs
actionの本体(コード)側で入力値として受け取りたいものを書きます。例ではtype
、job_name
、icon_emoji
、channel
、username
を変数名とした入力値を受け取れることを定義しています。inputs.<VARIABLE_NAME>
には、入力値が必須かどうかを示すrequired
やデフォルト値を宣言するdefault
パラメーターがあります。required
パラメーターの指定は必須ですが、default
パラメーターはオプションです。
inputsの実体は環境変数です。ジョブが起動する際にINPUT_<VARIABLE_NAME>
という環境変数として値が挿入されます。
runs
actionを動作させるためのパラメーターを指定します。using
はmainを動かすためのアプリケーションを指定し、main
はactionのメインスクリプトのPATHを指定します。GitHubが用意してくれているテンプレートを使ってTypeScriptで開発すると、JSのアウトプット先(outDir)がデフォルトでlibとなっているのでlib/main.js
となっています。これもテンプレートをいじらなければそのようになっています。メインのスクリプト名をindex.jsにしたい場合などには変更が必要です。
docker用のテンプレートを使っている場合はusing: 'docker'
、main: 'Dockerfile'
というようになっています。
branding
GitHub Actions Marketplaceに表示されるアイコンを設定します。↓こんな感じになります。
ソースコードの解説
actionの開発に直接関わる入力値の受け取り方とSlack通知の部分を解説します。
すべてのソースはこちらにあるので参考にして下さい。
main.ts
import * as core from '@actions/core';
import { Slack } from './slack';
import { Status, getStatus } from './utils';
async function run() {
try {
const type: string = core.getInput('type', { required: true });
const job_name: string = core.getInput('job_name', { required: true });
const username: string = core.getInput('username') || 'GitHub Actions';
const icon_emoji: string = core.getInput('icon_emoji') || 'github';
const channel: string = core.getInput('channel') || '#general';
const SLACK_WEBHOOK: string = process.env.SLACK_WEBHOOK || '';
if (SLACK_WEBHOOK === '') {
throw new Error('ERROR: Missing "SLACK_WEBHOOK"\nPlease configure "SLACK_WEBHOOK" as environment variable');
}
const status: Status = getStatus(type);
const slack = new Slack(SLACK_WEBHOOK, username, icon_emoji, channel);
const result = await slack.notify(status, job_name);
core.debug(`Response from Slack: ${JSON.stringify(result)}`);
} catch (err) {
console.log(err)
core.setFailed(err.message);
}
}
run();
core.getInput
@actions/coreのgetInputメソッドの第1引数にaction.ymlのinputsパラメーターで定義した変数名を渡すと、workflowのwithパラメーターで指定した値が取得できます。オプションで{required: true}
と指定すると、値がfalseと判定される場合にErrorをはかせることができます。
先程、inputsで指定した変数は環境変数として挿入されると説明しましたが、@actions/coreのソースコードでそれを確認することができます↓
1 export function getInput(name: string, options?: InputOptions): string {
2 const val: string =
3 process.env[`INPUT_${name.replace(' ', '_').toUpperCase()}`] || ''
4 if (options && options.required && !val) {
5 throw new Error(`Input required and not supplied: ${name}`)
6 }
7
8 return val.trim()
9 }
3行目から、name引数を大文字に変えてINPUT_という環境変数を取得していることが分かりますね。
core.debug
標準出力にログをアウトプットします。
core.setFailed
これはプロセスのexitcodeを1に指定しかつ、エラーメッセージを標準出力にアウトプットするメソッドです。
slack.ts
今回はSlackへ通知するためにIncoming WebHookを使用しました。公式のnpmパッケージがあるので便利です。
job_nameに絵文字を入れたり、よりリッチにするためにSlackのBlocksを採用しました。ただ、Blocksであると色をつけられないのでレガシーのAttachmentsと組み合わせて使っています。
Blocksにのせたいのは、GitHubに関する情報(リポジトリやイベント)なので@actions/github
を使って欲しい情報を取得しています。これはGITHUB_
で始まる環境変数をラップしていい感じにしてくれるパッケージです。ですので、GitHubのトークンは必要ありません。
import * as github from '@actions/github';
import * as core from '@actions/core';
import { Status } from './utils';
import { SectionBlock, MessageAttachment, MrkdwnElement } from '@slack/types';
import {
IncomingWebhook, IncomingWebhookSendArguments,
IncomingWebhookResult
} from '@slack/webhook';
export class Slack extends IncomingWebhook {
// 0: failure, 1: success
static readonly color: string[] = ['#cb2431', '#2cbe4e'];
constructor(
url: string,
username: string = 'GitHub Actions',
icon_emoji: string = 'github',
channel: string = '#general'
) {
super(url, {username, icon_emoji, channel});
}
/**
* Get slack blocks UI
*/
protected get blocks(): SectionBlock {
const context = github.context;
const { sha, eventName, workflow, ref } = context;
const { owner, repo } = context.repo;
const repo_url: string = `https://github.com/${owner}/${repo}`;
const action_url: string = `${repo_url}/commit/${sha}/checks`;
const blocks: SectionBlock = {
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*repository*\n<${repo_url}|${owner}/${repo}>` },
{ type: 'mrkdwn', text: `*ref*\n${ref}` },
{ type: 'mrkdwn', text: `*event name*\n${eventName}` },
{ type: 'mrkdwn', text: `*workflow*\n<${action_url}|${workflow}>` },
]
}
return blocks;
}
/**
* Generate slack payload
*/
protected generatePayload(status: Status, text: string): IncomingWebhookSendArguments {
const text_for_slack: MrkdwnElement = { type: 'mrkdwn', text };
const blocks: SectionBlock = { ...this.blocks, text: text_for_slack };
const attachments: MessageAttachment = {
color: Slack.color[status],
blocks: [blocks]
}
const payload: IncomingWebhookSendArguments = {
attachments: [attachments]
}
core.debug(`Generated payload for slack: ${JSON.stringify(payload)}`);
return payload;
}
/**
* Notify information about github actions to Slack
*/
public async notify(status: Status, text: string): Promise<IncomingWebhookResult> {
try {
const payload: IncomingWebhookSendArguments = this.generatePayload(status, text);
const result = await this.send(payload);
core.debug('Sent message to Slack');
return result;
} catch (err) {
throw err;
}
}
}
Workflowの例
開発したactionをGitHub Actionsのworkflowの一部に取り入れた例を紹介します。基本的な使い方は他のactionなどと一緒です。
以下の例はこのactionのリポジトリで走らせているものになります。
on: [push, pull_request]
name: TS Lint Check
jobs:
lint:
name: Lint Check
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Setup TS
run: npm install tslint typescript -g
- name: Lint check with tslint
run: tslint './src/*.ts'
# 開発したSlack通知用のaction
- name: Slack Notification
uses: homoluctus/slatify@master
if: always() # jobの成否に関わらずSlackへ通知
with:
type: ${{ job.status }} # jobの成否を代入(Failure/Success)
job_name: '*Lint Check*'
channel: '#random'
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
出来上がったSlack通知
job成功例
job失敗例
repositoryはGitHub Actionsが動いているリポジトリのトップページへのリンク、workflowはActionsタブへのリンクが張られています。なので、なにかしらのjobが失敗した場合にはworkflow名をクリックすればすぐにログを見ることができます。
これでslackで結果を視覚的に素早く確認でき、情報元(GitHub)にワンクリックで移動できるようになりました
おわりに
私は、このactionを開発する時にはじめてTypeScriptを書いた初心者(歴2・3日)なので、こういう風に書いたほうがいいとかがあればコメントやGitHubにイシューやプルリクを送ってください。
また、こんな機能を追加してほしいとか、UIの希望があれば可能な限り開発していこうと思ってますので、希望があれば遠慮なく下さい。