Azure Pipelines職人(自称)ですが、そのAzure PipelinesのクローンであるGitHub Actionsでも職人(自称)をやらないとなぁ~~となったのでActionを作って公開してみましたので、Actionの作り方など
前置き
GitHub Actions documentation - GitHub Docs
Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI/CD, and combine actions in a completely customized workflow.
help.github.com
 
 GitHub ActionsやActionの作り方などは公式ドキュメントにも書かれてますので1次ソースとしてどうぞ
作り方を決める
GitHub ActionsではAzure Pipelinesと同様にWindows, Linux, macOSで動作します。そしてActionの作り方は表のように2つあります
| 作り方 | 動作環境 | 
|---|---|
| Docker Container | Linux | 
| JavaScript(Node.js) | Linux, Windows, macOS | 
About custom actions - GitHub Docs
Actions are individual tasks that you can combine to create jobs and customize your workflow. You can create your own actions, or use and customize actions shared by the GitHub community.
help.github.com
 
 Docker Containerに関してはAzure Pipelinesでの対応状況を見る感じそのうちWindowsにも対応しそうですが、現在のところではLinuxでしか動かないみたいです
今回はWindowsやmacOSにも対応させたいのでJavaScript(Node.js)のほうで作ろうと思います。そしてJavaScriptは書きたくないのでTypeScriptを使います
あと、今回はDangerを実行するactionを作ります
リポジトリーセットアップ
Node.jsのv12系統を使うので公式ページなどから最新版をインストールしてください。あとVisual Studio Codeのインストールも忘れずに
ローカルにリポジトリーとなるフォルダーを作成したら、VS Codeで開いてセットアップをしていきます
- npm init -y
- npm install typescript --save-dev(typescriptがグローバルになければ- npm install -g typescriptも)
- tsc -init
- npm install @types/node --save-dev
- npm install @actions/core --save
- npm install @actions/io --save
- npm install @actions/exec --save
@actions/coreとかはGitHub Actions用に用意されているパッケージでactions/toolkitに見に行けば使い方などが書かれてます。今回はRubyとBundlerが入ってるかを確認するために@actions/io、コマンドの実行のために@actions/execも入れます
また、tsconfig.jsonではoutDirを./libに、rootDirを./srcにしておいてください
実行コードを書く
src/main.tsファイルを作り、そこにactionの実行コードを書いていきます
import * as core from "@actions/core";
async function run() {
    try {
    } catch (error) {
        core.setFailed(error.message);
    }
}
run();
基本の形としてrun関数を書いておき、その中のtry-catchのtry部分に処理を書いていくことにします
import * as io from "@actions/io";
async function checkEnvironment() {
    await io.which("ruby", true);
    await io.which("bundle", true);
}
まず、RubyとBundlerの存在確認ですが、@actions/ioのwhich関数の第2引数をtrueにしておくとコマンドが存在しなかった場合にエラーを排出するようなのでこれを利用します
interface Option {
    readonly dangerVersion: string;
    readonly pluginsFile: string | null;
    readonly dangerFile: string;
    readonly dangerId: string;
}
async function getOption(): Promise<Option> {
    return {
        dangerVersion: core.getInput("danger_version", { required: true }),
        pluginsFile: core.getInput("plugins_file"),
        dangerFile: core.getInput("danger_file", { required: true }),
        dangerId: core.getInput("danger_id", { required: true })
    };
}
次にactionに渡される引数を取得する部分を書きます。@actions/coreにgetInput関数があるのでそれを使っていきましょう
今回はDangerを実行するだけのactionなので、Dangerのバージョン情報とDangerのpluginをインストールするためのgemfileのパスとDangerを実行するための情報を引数から取得します
import * as exec from "@actions/exec";
async function installDanger(option: Option) {
    await exec.exec(
        `gem install danger --version "${option.dangerVersion}"`,
        undefined,
        { failOnStdErr: true }
    );
    if (option.pluginsFile != null) {
        await exec.exec(
            `bundle install --gemfile=${option.pluginsFile}`,
            undefined,
            { failOnStdErr: true }
        );
    }
}
そして@actions/execのexec関数を利用してDangerのインストールやDanger pluginのインストールを行っていきます。第3引数でexec関数のオプションを渡せるので、failOnStdErrをtrueにしておきます
async function runDanger(option: Option) {
    await exec.exec(
        `danger --dangerfile=${option.dangerFile} --danger_id=${option.dangerId}`,
        undefined,
        { failOnStdErr: true }
    );
}
ここまで来るとあとはただDangerを実行するだけのコードを書けばいいだけです
async function ignoreRubyWarning() {
    await core.exportVariable("RUBYOPT", "-W0");
}
ただ、これを実行してみるとRubyがおせっかいにも warning: Insecure world writable dir {RubyBinPath} in PATH, mode 040777を排出してきて、actionが失敗した状態になってしまうので、Rubyの警告を出さないようにしておきます
あとはこれらの関数を先ほどのrun関数で繋げていけばいいだけです
完成コード: src/main.ts
action.ymlを書く
あとはリポジトリートップにaction.ymlを書くだけです
name: 'Danger action'
description: 'Run danger, unofficial action'
author: 'MeilCli'
branding:
  icon: zap
  color: red
inputs:
  danger_version:
    description: 'danger version'
    default: '>= 6.0.0'
  plugins_file:
    description: 'gemfile for danger plugin'
  danger_file:
    description: 'dangerfile for danger'
    required: true
  danger_id:
    description: 'danger id, set identifier string'
    required: true
runs:
  using: 'node12'
  main: 'lib/main.js'
今回作成したactionのaction.ymlはこのようになっています
inputsに引数の情報を書いていき、runsでNode.jsを使うことと実行ファイルを指定してください
作ったactionはMarketplaceに公開するためにbrandingとか書いてますがリポジトリーを公開するだけなら必要ないはずです
公開する
公開するにはGitHubのリポジトリーに置いておく必要があります
tscコマンドなどでTypeScriptをトランスコンパイルしておきlib/main.jsがあることを確認します。確認出来たらpushしましょう
注意点として、実行ファイルが依存しているnode_modulesもリポジトリー内に配置しておく必要があります
node_modules/*
!node_modules/@actions
今回は必要最低限にとどめるために.gitignoreにこのように書きました。またRelease Tagを作っておくと便利です(v1とかv2のような感じのもの)
利用する
jobs:
  danger:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.6'
      - uses: MeilCli/danger-action@v1
        with:
          plugins_file: 'Gemfile'
          danger_file: 'Dangerfile'
          danger_id: 'danger-pr'
        env:
          DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
利用するときは通常のactionを利用するように{アカウント名}/{リポジトリー名}@{バージョン}を書きます
実はactions/checkoutなどはactionsのOrganizationで公開されていて、usesの部分で特殊対応が入ってるわけではありません(action.ymlのところで特殊対応はあったりしますが)
おわりに
というわけで、GitHub ActionsのActionの作り方の流れはわかってもらえたかなと思います。TypeScriptを使えば比較的簡単にActionを作ることができるので、皆さんもどんどん作っていって、どんどん便利なものができていけばなと思います
今回作ったもの:
GitHub - MeilCli/danger-action: Execute danger action for GitHub Actions.
Execute danger action for GitHub Actions. Contribute to MeilCli/danger-action development by creating an account on GitHub.
github.com
Marketplaceにも公開してます:
Danger action - GitHub Marketplace
Run danger
github.com
