滅入るんるん

何か書きます

GitHub ActionsのActionを作る

Azure Pipelines職人(自称)ですが、そのAzure PipelinesのクローンであるGitHub Actionsでも職人(自称)をやらないとなぁ~~となったのでActionを作って公開してみましたので、Actionの作り方など

前置き

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 actions - GitHub Help

Docker Containerに関してはAzure Pipelinesでの対応状況を見る感じそのうちWindowsにも対応しそうですが、現在のところではLinuxでしか動かないみたいです

今回はWindowsやmacOSにも対応させたいのでJavaScript(Node.js)のほうで作ろうと思います。そしてJavaScriptは書きたくないのでTypeScriptを使います

あと、今回はDangerを実行するactionを作ります

リポジトリーセットアップ

Node.jsのv12系統を使うので公式ページなどから最新版をインストールしてください。あとVisual Studio Codeのインストールも忘れずに

ローカルにリポジトリーとなるフォルダーを作成したら、VS Codeで開いてセットアップをしていきます

  1. npm init -y
  2. npm install typescript --save-dev (typescriptがグローバルになければnpm install -g typescriptも)
  3. tsc -init
  4. npm install @types/node --save-dev
  5. npm install @actions/core --save
  6. npm install @actions/io --save
  7. 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/iowhich関数の第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/coregetInput関数があるのでそれを使っていきましょう

今回は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/execexec関数を利用してDangerのインストールやDanger pluginのインストールを行っていきます。第3引数でexec関数のオプションを渡せるので、failOnStdErrtrueにしておきます

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.com

Marketplaceにも公開してます:

github.com