パターン

ワークフローの.ymlには都度パターンが出現します。使える特殊な文字は以下のようなものがあります。

  • *
  • **
  • !
  • []
  • ?
  • +

*

*はすべてにマッチしますが/は跨ぎません。これは*と設定した時foobarはマッチしますが、foo/foobar/barにはマッチしません。

またあるワードの前後に適当な文字を付けると、適当な文字は必ず含まれるランダムな文字にマッチします。*-barfoo-*foo-barにマッチします。

**

**はすべてにマッチします。こちらは/にもマッチするのでfoo/foobar/barにもマッチします。

こちらもランダムな文字と組み合わせて使えます。**/src/main.jspath/to/src/main.jsa/b/c/src/main.jsなどにマッチします。

また特によく使う表現は拡張子と使う方法です。**.js**.mdなどはpath/to/foo.jspath/to/readme.mdなど、適当なディレクトリにある.js.mdファイルにマッチします。

!

!は直前でマッチしたものの中から一部除外する為のパターンです。例えば**.jsはすべての.jsがマッチしますが。この後に続けて!foo/src/main.jsを置けば、foo/src/main.js以外のすべてのリポジトリに含まれる.jsファイルがマッチします。

[ と ]

[]で囲んだ部分はある1文字についてのパターンを表現します。*ではすべての文字がマッチしていましたが、この表現にするとその文字をある程度絞り込めます。例えば[abc]であればabcのどれかになります。もうひとつ挙げると[0123456789]は数値だけにマッチすることになります。

上記のような0123...というのは続いた値です。このような場合は[0-9]-を挟んで省略できます。これはアルファベットも同じで[a-z][A-Z]と書けます。
また、これらを[0-9a-zA-Z]のように組み合わせることもできます。

?

直前の文字が「あっても無くても良い」ことを表現できます。*.jsx?というパターンの場合*.js*.jsxどちらにもマッチします。

+

直前の文字は必ず1つ以上続くことを表現できます。(無理やり感がありますが)[ademe]+.mdとすればreadme.mdadem.mdなどにマッチします。

他にこれをよく使うのはセマンティック・バージョニングのパターンなどです。このパターンはv[0-9].[0-9]+.[0-9]+のように書きます。こえでv1.23.681のような文字にマッチします。

Trigger Events ∋ push & pull_request

on.pushイベントを使うと、そのリポジトリにpushした時にそのワークフローを走らせられます。on.pull_requestの場合はプルリクを上げた時だけです。

使うには以下のような一行をワークフローファイルへ置くだけです。どちらかだけを使いたいなら配列じゃなくでも良いです。

on: [push, pull_request]
# 1つだけなら `on: push` 

またこのon.pushon.pull_requestは下に詳細設定を置くことがでます。

ブランチ・タグフィルター

「このブランチ、このタグがpushされた時だけ」のような設定ができます。これはブランチはbranchesまたはbranches-ignore、タグはtagsまたはtags-ignoreで設定できます。-ignoreが付いているものとそうでないものは同時に設定できません。

また一度含ませてから、一部だけ含ませないような書き方も!を頭に付けることで可能です。これは.gitignoreのような設定ファイルと同じです。

ブランチ

ブランチはデフォルトですべてのブランチが実行対象に含まれてます。例えば以下のような設定があるとします。

on:
  push:
    branches-ignore:
      - 'features/**'
      - '!features/*-master'

上記の設定上ではfeatures/**にマッチしない時とfeat/*-masterにマッチする(feat/foo-masterなど)時のみワークフローが走ります。つまりこれはmasterブランチなども対象になります。

!はもちろんbranchesでも効きます。

on:
  push:
    branches:
      - "features/**"
      - "!features/*-test"

タグ

タグの場合はデフォルトではすべて対象外になってます。もしすべてのタグを含めないのであれば、

on:
  push:
    tags:
      - "*"

とすることでできます。セマンティック・バージョニングな形のタグだけにしたいなら、

on:
  push:
    tags:
      - "v[0-9].[0-9]+.[0-9]+"

この場合ではfeatures/*-testにはマッチしないfeatures/**にマッチするブランチでワークフローが走ります。

ファイルフィルター

on.pushの下にはpathspaths-ignoreというキーも置けます。これは「コミットにパターンにマッチするファイルがあった時だけ処理するよ」またはその逆のように設定できます。

on:
  push:
    paths-ignore:
      - '**.md'

上記のような設定にしておけばすべての**.md(リポジトリ上のすべての*.mdファイル)だけのコミットの時にはワークフローを走らせずに済みます。

on:
  push:
    paths:
      - '**.js'

このようにすれば**.jsの時だけ対象にできます。

Trigger Events ∋ schedule

on.scheduleはある決められた時刻にワークフローを実行する為の仕組みです。このイベントによってワークフローが走る時対象になるブランチは、そのリポジトリのデフォルトブランチになります。またその内容は最新のコミット時のものになります。

設定するにはon.scheduleの下にcronキーを持つ連想配列を配列で置きます。cronPOSIX クーロン構文で書く必要があります。配列ということは同時に複数の値を置くことができます。

on:
  schedule:
    - cron: "*/5 * * * *"
    - cron: "*/7 * * * *"  

このような設定ではワークフローは5分毎と7分毎に実行されます。注意点として2020年1月時点ではcronの最小インターバルは5分と設定されてます。

どの環境で実行するか: runs-on

ジョブ毎にどの環境で処理を実行するか指定するものとしてruns-onがあります。これが取りうる値は2020年1月現在以下の通りです。

  • ubuntu-latest,ubuntu-18.04 Ubuntu 18.04
  • ubuntu-16.04 Ubuntu 16.04
  • windows-latest Windows Server 2019
  • macos-latest macOS Catalina 10.15

例えば以下のようになります。

jobs:
  build:
    runs-on: ubuntu-latest

複数の環境で実行

ビルドマトリクスjobs.<job>.strategy.matrixを用いることで、1つのジョブステップを別々の環境で複数回実行できます。例えば、

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, ubuntu-16.04]  

のようにするとubuntu-18.04ubuntu-16.04の2環境で検証できます。

ローカル Docker 環境 で runs-on: self-hosted

runs-onself-hostedを選択すると、自身のマシンでワークフローを実行させれます。その為には先に自身のマシンで環境構築が必要です。
マシンには自分のマシン環境そのままを使っても良いですが、ここではあまり環境を汚したくない為 Docker 上で起動した Ubuntu の上で環境構築します。

とりあえず構築前に以下で Ubuntu を建てます。

docker run -itd \
  --name self-hosted-runner \
  --env WORKING_DIR=/home/runner/actions-runner
  ubuntu

次で ID が表示されれば建てれてます。

docker ps --filter name=self-hosted-runner --quiet

依存インストール

Docker の Ubuntu は最小環境らしくcurlが無いのでインストールします。

docker exec -it self-hosted-runner \
  sh -c 'apt update && apt install curl --yes'

ユーザーを作る

マシンはいつワークフローが実行されても大丈夫なように、(GitHub 提供の).shを実行して待機状態にしておく必要がありますが、この.shはスーパーユーザー権限だと実行できません。

そのため以下でユーザーrunnerを作り、このユーザーでその.shを実行させます。

docker exec -it self-hosted-runner \
  sh -c 'useradd runner -m && echo runner:secret | chpasswd'

最初のユーザーなので恐らく UID が1000runnerが作られたはずです。その為今後は、

docker exec --user 1000 -it self-hosted-runner sh -c '...'

として実行した...runnerユーザーによって行われるコマンドになります。

依存ファイルのインストール

これは適当なリポジトリの

  1. Settings
  2. Actions
  3. Add runner

で開くダイアログに載っているコマンドを叩くだけです。

docker exec --user 1000 -it self-hosted-runner sh -c '
  mkdir -p $WORKING_DIR && \
  cd $WORKING_DIR && \
  curl -O -L https://github.com/actions/runner/releases/download/v2.164.0/actions-runner-linux-x64-2.164.0.tar.gz && \
  tar xzf ./actions-runner-linux-x64-2.164.0.tar.gz
'

そしてダイアログに示されてる次の./config.shから始まるコマンドを実行すると以下のメッセージが出ます。

Libicu's dependencies is missing for Dotnet Core 3.0
Execute ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies.

また「欲しいツールが入ってないので、./bin/installdependencies.shを実行してインストールしてね」という感じのことを言ってるので./bin/installdependencies.shスーパーユーザーで叩きます。

docker exec -it self-hosted-runner \
  sh -c 'cd $WORKING_DIR && ./bin/installdependencies.sh'

再度runnerユーザーで./config.sh ...を叩きます。これもコピペで良いです。注意点として、一定時間が経過するとトークンが期限切れになるようなので、その場合は再度ダイアログを開き直して再生成してください。

docker exec --user runner -it self-hosted-runner \
  sh -c 'cd $WORKING_DIR && ./config.sh --url https://github.com/<owner>/<repo> --token <token>'

いくつか聞かれるものがありますが、それもデフォルト値が入ってるので変えたいものがあれば変えます。試しに以下のようにしてみました。

# self-hosted runner の名前
Enter the name of runner: [press Enter for 06332a3d575d] my-runner
# ...
#
# ワークフローを実行する際にいるフォルダ名
Enter name of work folder: [press Enter for _work]
# ...

うまくいくと Settings → Actions の Self-hosted runners へこのように runner が追加されます。

待ち受け状態にする

最後のコマンドを実行します。このコマンドで GitHub runner として接続され、ctrl-cを叩くまで実行されます。また、マシンの通信などが切れた場合は終了せず、自動で再接続を試みてくれるようです。

docker exec --user runner -it self-hosted-runner \
  sh -c 'cd $WORKING_DIR && ./run.sh'

すると先程はオフラインだった runner がアイドル状態になります。

最後にこの状態でgit pushなどを行った時に実行状態のターミナル画面にワークフローのログが現れればうまくいってます。こま### Docker 化

以下のようなDockerfileで Image 化すればdocker run -it --rm <image-name>だけで簡単にアイドル状態にできます。

FROM ubuntu:bionic

RUN apt update && apt install language-pack-ja sudo curl --yes

ENV TZ Asia/Tokyo
ENV LANG ja_JP.UTF-8

RUN useradd runner -m && \
  echo runner:secret | chpasswd && \
  usermod -aG sudo runner

USER runner
WORKDIR /home/runner

RUN mkdir actions-runner && cd actions-runner
RUN curl -O -L https://github.com/actions/runner/releases/download/v2.164.0/actions-runner-linux-x64-2.164.0.tar.gz
RUN tar xzf ./actions-runner-linux-x64-2.164.0.tar.gz

USER root

RUN ./bin/installdependencies.sh

USER runner

RUN ./config.sh --url https://github.com/nju33/github-actions-self-hosted-runner --token ADZKC3TA36BF3Y6VR7K7CPC6FZGMY

CMD [ "./run.sh" ]

外部のイベントから実行: Trigger Events ∋ repository_dispatch

トリガーイベントにrepository_dispatchを指定すると、GitHub API からイベントのディスパッチをした時に実行されるワークフローを作れます。

このトリガーを有効にするにはワークフロー.ymlファイルに次を追加するだけです。

on: repository_dispatch

イベント送信

イベントの送信は例えばcurlで以下のように行なえます。

curl -X POST \
--header 'accept: application/vnd.github.v3+json' \
--data '{"event_type": "foo"}' \
https://api.github.com/repos/<owner>/<repo>/dispatches

<owner>/<repo>はワークフローを置いている GitHub リポジトリによって変わります。
もしリポジトリがプライベートであれば、repo権限を持ったアクセストークンも送る必要があります。

--header 'authorization: bearer <private-access-token>'

値も送ってワークフローで使う

データのclient_payloadに好きなオブジェクトデータを入れて送るとワークフローでその値が使えます。例えば以下で送ると、

curl -X POST \
--header 'accept: application/vnd.github.v3+json' \
--data '{"event_type": "foo", "client_data": {"value": "value"}}' \
https://api.github.com/repos/<owner>/<repo>/dispatches

ワークフローではgithubコンテキストを通してそれを取得できます。

jobs:
  build:
    steps:
      - name: client_payload
        run: echo ${{ github.event.client_payload.value }}
        # value 

ちなみにevent_typegithub.event.actionで取得できます。

cache アクション

CI ではプロジェクトの依存パッケージのインストールを走らせることが多いです。 1 からインストールを行うと、規模によっては数分掛かりることもあります。 数回であれば気になりませんが、pushの度複数ワークフローを実行とかになると、準備ステップに時間をあまり掛けたくありません。 (プライベートリポジトリの場合、 ワークフローの合計実行時間の制限もあるので余計に)

cache アクション

キャッシュには公式が管理している actions/cache という action を使います。これを使うと、以前と同じ状態、または近いものがあれば今に復元できます。
実行時間の掛かる処理によって作られたものを復元することで、 1 から再度作るよりも時間を短縮できるはずです。

2020-05-07 だとバージョン 1 が主流です。使うには以下のように記述します。

steps:
  - # ...略
  - uses: actions/cache@v1
    id: something-cache
    with:
      path: ...
      key: ...
      restore-keys: ...

入力値

3 つの値を渡して実行できます。pathkeyそしてrestore-keysです。この中でpathkeyの 2 つの値は必須です。

まず最初にpathにはキャッシュしたいディレクトリやファイルへの絶対パスを渡します。次にkeyにはキャッシュする際のキー( 512 文字以下)を指定します。 ワークフローの最後にそのキーによるキャッシュが行われ、次に実行した際にそのキーによるキャッシュが存在すればそれが復元されます。

restore-keysは指定したキーによるキャッシュが存在しなかった場合に使われるフォールバック的なキーです。キーによって前方一致した最新のキャッシュを復元できるようになります。不完全でも前の結果がある方が処理を早く終えられるようなものに設定するのが良いです。

出力値

cache-hitという値があります。これはキャッシュを復元できた場合のみ、steps.something-cache.outputs.cache-hittrueという文字列が格納されます。

ジョブの例

このジョブはパッケージマネージャ Yarn を用いた JavaScript プロジェクトを想定してます。プロジェクトルートにはpackage.jsonがあり、適用にパッケージが追加されてます。また依存はローカルでインストール済でその解決方法が記載されたyarn.lockもプロジェクトルートに置かれれる状態です。

キャッシュに関するステップは- id: yarn-cache-dirの次の次の行にあります。

jobs:
  build:
    # ubuntu 18.04 環境上で実行
    runs-on: ubuntu-18.04

    # それぞれ以下のNode.jsが使える環境にして実行
    # - node@^10
    # - node@^12
    # - node@^13
    strategy:
      matrix:
        node-version: [10.x, 12.x, 13.x]

    # 実行処理
    steps:
      # Node.js の指定バージョンで使えるようにする
      - uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      # コードを持ってくる
      - uses: actions/checkout@v2

      # キャッシュがあれば復元
      - id: yarn-cache-dir
        run: echo "::set-output name=value::$(yarn cache dir)"
      - id: restore-keys
        run: echo "::set-output name=value::node-dependencies-${{ matrix.node-version }}-v1"
      - uses: actions/cache@v1
        id: node-dependencies
        with:
          path: ${{ steps.yarn-cache-dir.outputs.value }}
          key: ${{ steps.restore-keys.outputs.value }}-${{ hashFiles('**/yarn.lock') }}
          restore-keys: ${{ steps.restore-keys.outputs.value }}

      # インストール
      - run: yarn

キャッシュアクション部分を見ていきます。まずpathですが${{ steps.yarn-cache-dir.outputs.value }}を指定してます、この中身は id がyarn-cache-dirのステップのアウトプット値value、つまりはyarn cache dirコマンドを実行時の結果となっています。
yarnはインストール時に各パッケージ結果をキャッシュディレクトリに保存していて、次回のインストール時には可能ならそこからパッケージを解決してくれる為高速化が望めます。

echo "::set-output name=NAME::VALUE"部分ですが、ここは特殊なechoコマンドで、 GitHub Actions ではこのように書くと、steps.step-id.outputs.NAMEVALUEを格納して、後のステップで変数のように使えるようになります。

次にkeyです。これには${{ steps.restore-keys.outputs.value }}-${{ hashFiles('**/yarn.lock') }}-を指定してます。steps.restore-keys.outputs.valueidrestore-keysなステップのアウトプット値valuehashFilesによるハッシュ値、それぞれの文字列を組み合わせたものを指定してます。

hashFilesは GitHub Actions で使える関数の 1 つで指定したファイルパターンからハッシュを計算します。例のように**/yarn.lockのように書くとプロジェクトに含まれるすべてのyarn.lockファイルを対象に計算してくれます。

最後のrestore-keysでは、keyからhashFiles部分を除いたものを指定してます。こうしておくことで、依存に変更が入りyarn.lockの中身が更新され、完全マッチなキャッシュが無くなったとしても、この最新のキャッシュを復元できるようになります。これは上記でも述べた前方一致すればいいキーだからです。

実行してみる

yarn.lockが同じ状態で 2 回ワークフローを走らせてみます。キャッシュがうまく効いていれば 2 回目はより早く完了するはずです。

1 回目

1 回目を実行しました。これにより 2 つの事が分かりました。

  1. yarnの実行時間は 17 秒
  2. 終わる前に実行されるキャッシュアクションによるステップのログCache saved successfullyからキャッシュがnode-dependencies-12.x-v1-a135e48fc66de6bb6175d4726583f1b07aefbaac334cc580f8f80e8b70532c3aキーで作られた

2 回目

上記が完了した後 2 回目を実行しました。

  1. yarnの実行時間は 5 秒
  2. Cache hit occurred on the primary key... not saving cache(プリマリキーでキャッシュヒットが発生しました、キャッシュを保存してません)から今度はキャッシュが作られなかった

上記の結果からキャッシュが正常に作られたことで、ワークフローに掛かる時間が短縮できることが分かりました。

1 回目2 回目はリンク先から結果を見ることができます。

キャッシュを復元できた時、作成コマンドの実行がいらない場合

トップの例でキャッシュが復元できた時steps.something-cache.outputs.cache-hittrueになると書きました。もし作成コマンドの実行がいらないなら、この値を使ってステップをスキップできます。
それにはstep.ifを設定します。以下ではsteps.something-cache.outputs.cache-hit != 'true'の時のみrun: ...部分のコマンドが実行されます。

steps:
  - # ...略
  - uses: actions/cache@v1
    id: something-cache
    with:
      path: ...
      key: ...
      restore-keys: ...
    - if: steps.something-cache.outputs.cache-hit != 'true'
      run: ...

ワークフローコマンド

GitHub Actions には少し特殊なワークフローコマンドというものがあります。実態はechoコマンドですが、出力の形式が決められてます。

ワークフローコマンドには以下のような種類があります。

  • set-env
  • set-output
  • add-path
  • error
  • warning
  • debug
  • add-mask

set-env

set-envは環境変数を設定できます。設定した環境変数はその後のステップで使うことができます。これは主に何かしら動的に取得した情報などを元に環境変数を設定したい場合に使います。

jobs:
  build:
    steps:
      # `echo nju33`は動的な情報入力のつもり
      - run: echo nju33 | xargs -I{} echo "::set-env name=NAME::{}"
      - run: echo "$NAME"

静的な環境変数の設定なら<job-id>.env<job-id>.steps.<step-id>.envでの設定がオススメです。

jobs:
  build:
    env:
      FOO: foo
    steps:
      - run: ...
        env:
          BAR: bar

set-output

ワークフローでの変数のようなものを定義できます。定義後はsteps.<step-id>.outputs.<value-name>のような形で、その後のステップで使うことができます。<step-id>を使う為、ステップにはid指定が必須です。

以下は例です。

jobs:
  build:
    steps:
      - id: org
        run: echo "::set-output name=name::nju33-com"
      - run: echo "${{ steps.org.outputs.name }}"

run: echo "${{ steps.org.outputs.name }}"部分のステップが実行された時、nju33-comという文字が出力されます。

ところで、「これなら環境変数を使えばいいんじゃないか」と考える人が出てくると思いますが、シェル部分であれば環境変数でも良いです。しかしシェル以外の部分では環境変数を参照できない為、そういった部分ではset-outputで一旦定義してから使います。

そういった部分と言うのは具体的に 1 つ言うと action の inputs の値があります。例えば、actions/checkout では inputs 値の 1 つにrepositoryが指定できます。その指定に outputs 値を使うと以下のようになります。

jobs:
  build:
    steps:
      - id: org
        run: echo "::set-output name=name::nju33-com"
      - run: echo "${{ steps.org.outputs.name }}"
      - id: repo
        run: echo "::set-output name=name::${{ steps.org.outputs.name }}/sandbox-github-actions-workflow-command"
      - uses: actions/checkout@v2
        with:
          repository: ${{ steps.repo.outputs.name }}

この実行結果は、repositorynju33-com/sandbox-github-actions-workflow-commandが渡り、actions/checkoutが実行されてます。

set-path

PATHに追加します。 Node.js プロジェクトでcowsayというパッケージを使い例を見てみます。ちなみに、このパッケージは牛が何か喋ってるように見せることができる CLI ツールです。

npmyarnを使い実際にパッケージをローカルインストールするとnode_modules/.bin以下にcowsayという実行可能ファイルが作られます。
インストール直後では相対パスでnode_modules/.bin/cowsayと指定する必要がありますが、これをcowsayだけで実行できるようにしてみます。

以下は例です。 3 つ目のステップでPATHに追加してます。

jobs:
  build:
    steps:
      - run: |
          yarn init -y
          yarn add -D cowsay
      - run: |
          if command -v cowsay > /dev/null; then
            cowsay Moo
          else
            echo cowsay: command not found
          fi
      - run: echo "::add-path::$(pwd)/node_modules/.bin"
      - run: |
          if command -v cowsay > /dev/null; then
            cowsay Moo
          else
            echo cowsay: command not found
          fi

この実行結果は、最初のステップでcowsayがローカルインストールされます。次にもしcowsayコマンドにPATHが通っていればMooと鳴くようにしてますが、まだ通っていないのでcowsay: command not foundと出力されます。
その後、一度add-pathワークフローコマンドで$(pwd)/node_modules/.binという絶対パスを設定し、再度cowsayコマンドの実行を試みます。
今度はパスが通っているのでMooと鳴きます。

 _____
< Moo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

error

エラーメッセージを表示できます。この形でエラーメッセージを記述すると、実行結果ページ赤い線の目立つ枠でエラーメッセージが表示されます。

jobs:
  build:
    steps:
      - run: echo ::error::error-message

エラーメッセージの他にどのファイル(file)の何行(line)何列(col)目に対してのメッセージなのかを指定できます。(,の後のスペースを入れないように気を付けます。もしスペースを入れてしまうとうまく解釈されなくなります)

jobs:
  build:
    steps:
      - run: |
          echo ::error file=index.js,line=1,col=1::The first line is bad
          exit 1
      - run: echo foo

詳細にファイル名や行の指定があると、エラーメッセージもより詳細に分かりやすい形で表示されます。

warning

警告メッセージを表示できます。使い方はerrorワークフローコマンドと同じですが、実行結果ページメッセージの線が黄色になってます。

jobs:
  build:
    runs-on: ubuntu-16.04
    steps:
      - run: echo ::warning::warning-message
      - run: echo ::warning file=.github/workflows/warning.yml,line=10,col=14::ubuntu-18.04 使ってね

debug

デバッグ時のみのメッセージを出力します。デバッグ時とはリポジトリのシークレット変数ACTIONS_STEP_DEBUGtrueに設定した状態でワークフローを実行した時のことで、以下のようにやってる事が細かに吐き出されるようになります。(以下はecho 123したステップのログ)

##[debug]Evaluating condition for step: 'Run echo 123'
##[debug]Evaluating: success()
##[debug]Evaluating success:
##[debug]=> true
##[debug]Result: true
##[debug]Starting: Run echo 123
##[debug]Loading inputs
##[debug]Loading env
Run echo 123
##[debug]/bin/bash -e /home/runner/work/_temp/b85ddda2-9c07-4f5d-80c2-f0b6b46fc9a8.sh
123
##[debug]Finishing: Run echo 123

debugワークフローコマンドは以下のように使います。これはdebug-messageと表示する例です。

jobs:
  build:
    steps:
      - run: echo ::debug::debug-message

実行結果ページでもログで表示されてます。

debugワークフローコマンドのメッセージはACTIONS_STEP_DEBUGtrueの状態だと表示されません

add-mask

渡したメッセージを***にする事ができます。

jobs:
  build:
    steps:
      - run: echo ::add-mask::foo

実行結果ページで確認すると、::add-mask::***となってることが分かります。

ステータスによるステップの制御

終了ステータスが0以外で終了した時デフォルトでは、そのステップのジョブはそこで終了されます。単にジョブの全ステップが正常に実行されて終了することを期待するのであれば、この仕様は直感的で扱いやすいものです。ですが時々、予期していない出来事で最後まで完了しなかった場合だけ後処理を実行したい場合があったりします。

GitHub Actions ではそのような「エラーの時の後処理」を記述できる仕組みが提供されてます。また同じように「正常終了時の後処理」というような仕組みも用意されてます。

関数

GitHub Actions には以下の終了ステータスに関する関数が使えます。

  • success
  • failure
  • cancelled
  • always

これらの関数は真偽値を返すので、主に<step>.ifに渡して使います。

success

successは前回のステップの終了ステータスが0の時trueになる関数です。以下のワークフローではecho successも実行されます。

jobs:
  build:
    - run: echo foo
    - if: success()
      run: echo success

failure

今度は逆に以前のコマンドが0以外の時だけtrueになる関数です。以下のワークフローでは最初のステップを終了ステータス1で終わってるので、echo failureのステップも実行されます。

jobs:
  build:
    - run: exit 1
    - if: failure()
      run: echo failure

注意点として、一度終了ステータスが0以外のもので終了したステップがある時点で、そのワークフローは失敗した扱いになるというのがあります。このワークフローはechoで正常に終了してますが、これもワークフローは失敗という扱いになります。
使い所はワークフローが失敗したことに対する後処理を行う為に使うと良いと思います。

ちなみに失敗するかもしれないステップが成功しても失敗してもどちらでも良いような場合continue-on-error: trueを設定することで、終了ステータスが0以外でも成功扱いにすることができます。以下のワークフローの場合、echo failureは実行されません。

jobs:
  build:
    - continue-on-error: true
      run: exit 1
    - if: failure()
      run: echo failure

cancelled

これはステップの終了ステータスではないですが、ワークフローがキャンセルされた以降にtrueになる関数です。以下のワークフローは、sleep 60で待っている間にキャンセルボタンをクリックしてキャンセルすることで、echo cancelledのステップを実行できます。

jobs:
  build:
    steps:
      - run: sleep 60
      - if: cancelled()
        run: echo cancelled

always

以前のステップが成功でも失敗でも、ワークフローがキャンセルされていたとしても、とにかく常にtrueになる関数です。以下のワークフローは、最初のステップでわざと失敗させていますが、echo alwaysは実行されます。

jobs:
  build:
    steps:
      - run: exit 1
      - if: always()
        run: echo always
      - if: failure()
        run: echo failure

alwaysfaiureと同じく、一度終了ステータスが0以外で終わったステップがある時点で、そのワークフローは失敗扱いになります。

バージョンタグをプッシュした時のみ実行

バージョンタグとはセマンティック バージョニング (SemVer) 2.0.0のような1.2.3というバージョンの頭にvが付いたv1.2.3のようなタグの事をここでは指します。

例えば以下のようなワークフローがあるとします。これがv1.2.3のようなタグをプッシュした時のみ、ジョブが実行されるワークフローです。これはecho fooと表示されます。

on:
  push:
    branches-ignore:
      - '**'
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - run: echo foo

これは全体をタグがプッシュした時に実行というフィルターを掛けているような形になっています。ただケースによってはブランチプッシュでもタグプッシュでも共に実行したい共通ステップが出てきてしまったりするかもしれません。
この対応方法にはざっと以下のようなものがあります。

  1. ブランチとタグで別々のワークフローを実行
  2. ブランチとタグで同じワークフローを実行し、バージョンタグのプッシュ時にだけしか実行されないようなステップで対応

どちらが良いのかは規模感によって変わります。個人プロジェクトのような小さなものなら 1 で良いでしょうし、大きいなら 1 だと管理が辛くなるかもしれません。

1. ブランチとタグで別々のワークフローを実行

これは単にワークフローファイルを別々に置くというだけです。

例えば上記のワークフローがバージョンタグのプッシュ時のみというものだったので、ブランチプッシュ時のみ実行されるワークフローは以下のように書き始めます。

on: push

jobs: # 略

pushイベントはbrannchstagsで何もフィルタリングしていない時、すべてのブランチが対象で全てのタグが除外という設定になるので、これだけでブランチ用のワークフローになります。

2. ブランチとタグで同じワークフローを実行し、バージョンタグのプッシュ時にだけしか実行されないようなステップで対応

同じワークフローで実行というのは、<job>.if<step>-ifなどの制御を活用して、バージョンタグをプッシュした時だけに実行される領域をワークフロー内に作ろうという意味です。これにはまず 2 つの要素の理解をします。

1 つ目は参照の名前の存在です。何かしらをプッシュするとgithubコンテキストのref値に参照の名前が入ります。参照の名前というのはブランチのプッシュであればrefs/heads/<branch-name>(refs/heads/masterなど)、タグのプッシュであればrefs/tags/<tag-name>(refs/tags/v0.0.2など)のような名前のことです。

2 つ目はワークフロー内ではstartsWithという関数が使えます。これは引数を文字列として 2 つ取り、 1 つ目の文字列が 2 つ目に渡した文字列から始まっていればtrueを返します。

この 2 つの要素を組み合わせます。例えばタグの参照名はrefs/tags/から始まると分かったので、startsWith( github.ref, 'refs/tags/' )のようにすればタグの時だけtrueになりそうです。ただ今回はバージョンダグを扱うので、refs/tags/vとします。

上記から作成したブランチとバージョンタグが共存したワークフローが以下のようなものです。

on:
  push:
    branches:
      - '**'
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - run: echo ${{ github.ref }}
      - if: startsWith( github.ref, 'refs/tags/v' )
        env:
          REF: ${{ github.ref }}
        run: echo "${REF##*/}"

これに対してブランチプッシュバージョンタグプッシュを行いました。想定通り、バージョンタグをプッシュした時だけ 2 つ目のステップが実行されました。

2 つ目のステップのenv.REFecho "${REF##*/}はちょっとしたテクニックで、シェルが Bash (デフォルト)の時だけですが、このようにするとechov0.0.3のような値が表示(取得)できます。(ちなみにecho "${REF##*/v}とすると0.0.3という値になります)

複数のジョブ

1 つのワークフローには複数のジョブを持てます。

並行実行

以下の例はjob1job2という 2 つのジョブを設定してます。

jobs:
  job1: # ...
  job2: # ...

何かしらのトリガーを弾いた時、これらは並行に実行されます。

順次実行

<job>.needs値を設定すると順次実行できます。

例えば、job2job1が完了後実行させたいならneeds: job1と設定します。これは配列で複数指定することもできます。

jobs:
  # ...
  job1: # ...
  job2:
    needs:
      - job1
      # - 他にもあるなら
    steps: # ...

以前のジョブの出力値を使う

各ジョブにはoutputsに Map 値が設定できます。これは依存関係となっている他のジョブに向けての出力で、以降のジョブはここから出力されている値を好きにジョブ内で使うことができます。
outputsの各キーの値は、そのジョブが完了した時のコンテキストの値を設定できます。つまり、例えばあるステップの値を設定するには以下のようにします。

jobs:
  job1:
    runs-on: ubuntu-18.04
    outputs:
      foo: ${{ steps.foo.outputs.value }}
    steps:
      - id: foo
        run: echo "::set-output name=value::foo"
  job2: # ...

set-outputワークフローコマンドを使いsteps.foo.outputs.value値を設定し、それをoutputsで再度登録します。( JavaScript でいうexport、 Rust でいう pub のような感じ)

このようにするとjob2ではneedsコンテキストから、needs.<job-id>.outputs.<output-name>という書き方を使うと、job1outputs値を取得できます。

jobs:
  job1: # ...
  job2:
    runs-on: ubuntu-18.04
    needs:
      - job1
    steps:
      - run: echo ${{ needs.job1.outputs.foo }}

needs.job1.outputs.fooが取得してる箇所です。この部分は${{ }}により値が展開され、fooという文字に置き換わります。よってjob2結果は、echo fooというコマンドを実行して終わります。

すべて展開したものが以下になります。

on: push

jobs:
  job1:
    runs-on: ubuntu-18.04
    outputs:
      foo: ${{ steps.foo.outputs.value }}
    steps:
      - id: foo
        run: echo "::set-output name=value::foo"
  job2:
    runs-on: ubuntu-18.04
    needs:
      - job1
    steps:
      - run: echo ${{ needs.job1.outputs.foo }}