LoginSignup
33
15

More than 3 years have passed since last update.

【GitHub Actions】Slack通知用のActionsをTypeScriptで開発してみた

Last updated at Posted at 2019-08-28

前置き

皆さん、CI・CD機能が追加されたGitHub Actionsを使っていますか?
私はステージングや本番環境へデプロイするためにGitHub Actionsを使っています。
新しくなったGitHub Actionsによって様々な恩恵を受けていますが、CI・CDの経過や結果の通知に困っていました。今までは、既に組み込まれていたメールの通知機能を使ってわざわざ結果を確認したり、その場しのぎでcurlコマンドを使ってSlackへ投稿したりしていました。さすがにしんどくなったので、結果をSlackに通知するためのactionを開発してみました。

開発環境

  • MacOS 10.14
  • TypeScript 3.5.1

開発の流れ

actionを開発するざっくりとした流れは↓のようになります。GitHubがactionの開発をサポートするためにテンプレートを既に用意しているので、それを基に開発していきます。

  1. actionの実行環境を決める
  2. テンプレートからリポジトリを作成 (javascript-templatecontainer-toolkit-templatecontainer-template)
  3. actionの定義
  4. いざ、開発!
  5. ビルド
  6. リリース

今回は3と4について説明していきます!

action定義

actionの定義はYAMLで書きます。何を定義するのかというと、actionの名前や入力値などです。実際に書いたYAMLを例に説明していきます。

action.yml
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の本体(コード)側で入力値として受け取りたいものを書きます。例ではtypejob_nameicon_emojichannelusernameを変数名とした入力値を受け取れることを定義しています。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に表示されるアイコンを設定します。↓こんな感じになります。

スクリーンショット 2019-08-28 18.51.01.png

ソースコードの解説

actionの開発に直接関わる入力値の受け取り方とSlack通知の部分を解説します。
すべてのソースはこちらにあるので参考にして下さい。

main.ts

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のソースコードでそれを確認することができます↓

@actions/core/core.ts
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のトークンは必要ありません。

slack.ts
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のリポジトリで走らせているものになります。

.github/workflows/main.yml
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成功例

スクリーンショット 2019-08-28 11.26.44.png

job失敗例

スクリーンショット 2019-08-28 11.29.10.png

repositoryはGitHub Actionsが動いているリポジトリのトップページへのリンク、workflowはActionsタブへのリンクが張られています。なので、なにかしらのjobが失敗した場合にはworkflow名をクリックすればすぐにログを見ることができます。
これでslackで結果を視覚的に素早く確認でき、情報元(GitHub)にワンクリックで移動できるようになりました:clap:

おわりに

私は、このactionを開発する時にはじめてTypeScriptを書いた初心者(歴2・3日)なので、こういう風に書いたほうがいいとかがあればコメントやGitHubにイシューやプルリクを送ってください。
また、こんな機能を追加してほしいとか、UIの希望があれば可能な限り開発していこうと思ってますので、希望があれば遠慮なく下さい。

Refenrence

33
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
15