ローカルPC内でGithub Actionsを実行する – ‘nekos/act’

Git,GitHub,Github Actions

はじめに

Github Actions の設定ファイルを記述した後、正しく動作するかを確認するために毎回リモートリポジトリにプッシュするのが面倒だなと思ったので良い方法が無いかと調べてみました。
すると、ローカルで Github Actions の実行が可能なツールが見つかったので、検証してみました。

検証環境

$ uname -moi
arm64 unknown Darwin

$ bash -version | head -n 1
GNU bash, version 5.2.15(1)-release (aarch64-apple-darwin22.1.0)

見つけたツール : nektos/act

利用するツールは以下になります。

概要を見る限りは自分の目的にフィットしています。

nektos/act ってどんなツール?

Github Actions をローカルで実行するためのツールとのことです。

このツールを導入するメリットは以下となります。

  • .github/workflows ディレクトリ配下に設定ファイルを作成、変更した後、 commit/push しなくてもテストできるため開発効率が上がる
  • Makefile でタスクを管理することもできますが、 .github/worflows とタスクが重複し DRY ではなくなります。
    ローカルで実行できれば Makefile が不要となります。

    • (この点については、 Github Actions から make を呼ぶことで回避できると思いますが公式ページのメリットとして書かれていました )

nektos/act って何をしてくれるに?

act コマンドが提供されます。

act コマンドを実行すると、 .github/workflows/ ディレクトリから設定ファイルを読み込み、実行すべきアクションのセットを決定します。
実行されるアクションを決定するためには「イベント」 ( pushbranch作成など ) を指定しなければいけませんが、明示しなければ push イベントが発生したものとして解釈されます。
ワークフローファイルで定義されているように、Docker API を使用して必要なイメージをプルまたはビルドし、定義された依存関係に基づいて実行パスを最終的に決定します。

インストール

macOS なら、 Homebrew を使うのが最もかんたんです。

brew install act

上記以外の環境の場合は、公式ページを参照するのが良いでしょう。

少し変わったインストール方法として、 Github CLI の拡張としてインストールする方法もあります。

$ gh extension install https://github.com/nektos/gh-act

試してみる前に ( .github/workflow/ 内の設定ファイルの用意 )

自分で Github Actions の学習のために作成した Public リポジトリで試してみることにしました。

まずはリポジトリを用意し、移動します。

$ git clone ssh://git@github.com/genzouw/learning-github-actions

$ cd learning-github-actions

初回実行時の挙動

act コマンドを初めて実行すると、デフォルトイメージとしてどのイメージを利用するかを問われます。
ここで選択した結果は ~/.actrc ファイルに保存され、以降はこのファイルの設定が参照されます。

あまり読んでもわからなかったのですが、 Large だと 20GB と随分巨大なイメージを使うことに加えて、 「only ubuntu-18.04 platform」と書かれていたため、 Medium を選択することにしました。

$ act
? Please choose the default image you want to use with act:

  - Large size image: +20GB Docker image, includes almost all tools used on GitHub Actions (IMPORTANT: currently only ubuntu-18.04 platform is available)
  - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with all actions
  - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions

Default image and other options can be changed manually in ~/.actrc (please refer to https://github.com/nektos/act#configuration for additional information about file structure) Medium

すべてのジョブを一覧表示 : act --list

act --list を実行すると、設定されているすべてのジョブの一覧が表示されます。

$ act --list
WARN  ⚠ You are using Apple M1 chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. ⚠
Stage  Job ID                     Job name                   Workflow name              Workflow file                   Events
0      go-run                     go-run                     go-run                     go-run.yaml                     push
0      hello                      hello                      hello                      hello.yaml                      push
0      if-example                 if-example                 if-example                 if-example.yaml                 push
0      check-bats-version         check-bats-version         learn-github-actions       learn-github-actions.yaml       push
0      upload-artifacts           upload-artifacts           upload-artifacts           upload-artifacts.yaml           push
0      use-environment-variables  use-environment-variables  use-environment-variables  use-environment-variables.yaml  push

Apple M1 チップの PC ではワーニングが出ます。
メッセージの通り --container-architecture linux/amd64 オプションを指定することでワーニングを抑制できます。

$ act --container-architecture linux/amd64 --list
Stage  Job ID                     Job name                   Workflow name              Workflow file                   Events
0      go-run                     go-run                     go-run                     go-run.yaml                     push
0      hello                      hello                      hello                      hello.yaml                      push
0      if-example                 if-example                 if-example                 if-example.yaml                 push
0      check-bats-version         check-bats-version         learn-github-actions       learn-github-actions.yaml       push
0      upload-artifacts           upload-artifacts           upload-artifacts           upload-artifacts.yaml           push
0      use-environment-variables  use-environment-variables  use-environment-variables  use-environment-variables.yaml  push

以下の alias 設定をしておくのが良さそうです。

alias act='act --container-architecture linux/amd64'

ワークフローを実行 : act --workflows <YAML>

--workflows オプションと YAML 設定ファイルを指定して、ワークフローを実行することができます。

以下のような YAML ファイルを指定し実行してみます。

$ cat .github/workflows/if-example.yaml
---
name: if-example
run-name: if-example
on:
  - push
jobs:
  if-example:
    runs-on: ubuntu-22.04
    steps:
      - run: echo "hello false"
        if: ${{ false }}
      - run: echo "hello true"
        if: ${{ true }}
      - run: echo "hello $MY_ENV_VAR"
        env:
          MY_ENV_VAR: ${{ 'development' }}

実行結果、 hello truehello develop という文言が出力されました。
とりあえず動いた模様です。

$ act --workflows .github/workflows/if-example.yaml
[if-example/if-example] 🚀  Start image=node:16-bullseye-slim
[if-example/if-example]   🐳  docker pull image=node:16-bullseye-slim platform=linux/amd64 username= forcePull=true
[if-example/if-example]   🐳  docker create image=node:16-bullseye-slim platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[if-example/if-example]   🐳  docker run image=node:16-bullseye-slim platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[if-example/if-example] ⭐ Run Main echo "hello true"
[if-example/if-example]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/1] user= workdir=
| hello true
[if-example/if-example]   ✅  Success - Main echo "hello true"
[if-example/if-example] ⭐ Run Main echo "hello $MY_ENV_VAR"
[if-example/if-example]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/2] user= workdir=
| hello development
[if-example/if-example]   ✅  Success - Main echo "hello $MY_ENV_VAR"
[if-example/if-example] 🏁  Job succeeded

今度はもう少しだけ複雑なワークフローを実行してみます。

$ cat .github/workflows/go-run.yaml
---
name: go-run
run-name: go-run
on:
  - push
jobs:
  go-run:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v3
      - name: Setup Go environment
        uses: actions/setup-go@v4.0.0
        with:
          go-version: "1.20.3"
      - run: go run main.go

実行した結果、 Go 実行環境のセットアップと実行が成功しました。

たです、実行してみてわかるのですがだいぶ遅いです。
既存の Go Docker イメージを拝借するようにしたほうが早いと思いました。

$ act --workflows .github/workflows/go-run.yaml
[go-run/go-run] 🚀  Start image=node:16-bullseye-slim
[go-run/go-run]   🐳  docker pull image=node:16-bullseye-slim platform=linux/amd64 username= forcePull=true
[go-run/go-run]   🐳  docker create image=node:16-bullseye-slim platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[go-run/go-run]   🐳  docker run image=node:16-bullseye-slim platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[go-run/go-run]   ☁  git clone 'https://github.com/actions/setup-go' # ref=v4.0.0
[go-run/go-run] ⭐ Run Main actions/checkout@v3
[go-run/go-run]   🐳  docker cp src=/Users/genzouw/.ghq/github.com/genzouw/learning-github-actions/. dst=/Users/genzouw/.ghq/github.com/genzouw/learning-github-actions
[go-run/go-run]   ✅  Success - Main actions/checkout@v3
[go-run/go-run] ⭐ Run Main Setup Go environment
...(省略)...
[go-run/go-run] ⭐ Run Main go run main.go
[go-run/go-run]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/2] user= workdir=
| Hello Github Actions !
[go-run/go-run]   ✅  Success - Main go run main.go
[go-run/go-run] ⭐ Run Post Setup Go environment
...(省略)...
[go-run/go-run] 🏁  Job succeeded

Dry Run : act --dryrun

設定ファイルの検証を行うだけなら、Dry Run で十分でしょう。

$ act --dryrun --workflows .github/workflows/hello.yaml
*DRYRUN* [hello/hello] 🚀  Start image=node:16-bullseye-slim
*DRYRUN* [hello/hello]   🐳  docker pull image=node:16-bullseye-slim platform=linux/amd64 username= forcePull=true
*DRYRUN* [hello/hello]   🐳  docker create image=node:16-bullseye-slim platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
*DRYRUN* [hello/hello]   🐳  docker run image=node:16-bullseye-slim platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
*DRYRUN* [hello/hello] ⭐ Run Main actions/checkout@v3
*DRYRUN* [hello/hello]   ✅  Success - Main actions/checkout@v3
*DRYRUN* [hello/hello] ⭐ Run Main ./.github/actions/say-hello
*DRYRUN* [hello/hello]   ✅  Success - Main ./.github/actions/say-hello
*DRYRUN* [hello/hello] 🏁  Job succeeded

特に artifacts に成果物をアップロードするような処理を記述していたり、Github インフラ特有の機能を利用していたりすると、ローカルでは当然動かないわけで、そういうときには --dryrun だけは通しておくと安心できます。

実行時に環境変数を渡す : --env

act コマンド実行時に --env オプションで環境変数を指定できます。

$ cat .github/workflows/use-environment-variables.yaml
---
name: use-environment-variables
run-name: use-environment-variables
on:
  - push
jobs:
  use-environment-variables:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v3
      - run: node .github/scripts/use_environment_variables.js

$ cat .github/scripts/use_environment_variables.js
#!/usr/bin/env node

console.log("START------");
console.log(process.env.GITHUB_ACTOR);
console.log(process.env.POSTGRES_HOST);
console.log(process.env.POSTGRES_PORT);
console.log("END------");

$ act --env POSTGRES_HOST=hello --env POSTGRES_PORT=9999 --workflows .github/workflows/use-environment-variables.yaml
[use-environment-variables/use-environment-variables] 🚀  Start image=catthehacker/ubuntu:act-22.04
[use-environment-variables/use-environment-variables]   🐳  docker pull image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 username= forcePull=true
[use-environment-variables/use-environment-variables]   🐳  docker create image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[use-environment-variables/use-environment-variables]   🐳  docker run image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[use-environment-variables/use-environment-variables] ⭐ Run Main actions/checkout@v3
[use-environment-variables/use-environment-variables]   🐳  docker cp src=/Users/genzouw/.ghq/github.com/genzouw/learning-github-actions/. dst=/Users/genzouw/.ghq/github.com/genzouw/learning-github-actions
[use-environment-variables/use-environment-variables]   ✅  Success - Main actions/checkout@v3
[use-environment-variables/use-environment-variables] ⭐ Run Main node .github/scripts/use_environment_variables.js
[use-environment-variables/use-environment-variables]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/1] user= workdir=
| START------
| nektos/act
| hello
| 9999
| END------
[use-environment-variables/use-environment-variables]   ✅  Success - Main node .github/scripts/use_environment_variables.js
[use-environment-variables/use-environment-variables] 🏁  Job succeeded

あるいは、 .env ファイルが存在している場合はこちらから環境変数が充当されます。

$ cat .env
POSTGRES_HOST=foo
POSTGRES_PORT=8765

$ act --workflows .github/workflows/use-environment-variables.yaml
[use-environment-variables/use-environment-variables] 🚀  Start image=catthehacker/ubuntu:act-22.04
[use-environment-variables/use-environment-variables]   🐳  docker pull image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 username= forcePull=true
[use-environment-variables/use-environment-variables]   🐳  docker create image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[use-environment-variables/use-environment-variables]   🐳  docker run image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[use-environment-variables/use-environment-variables] ⭐ Run Main actions/checkout@v3
[use-environment-variables/use-environment-variables]   🐳  docker cp src=/Users/genzouw/.ghq/github.com/genzouw/learning-github-actions/. dst=/Users/genzouw/.ghq/github.com/genzouw/learning-github-actions
[use-environment-variables/use-environment-variables]   ✅  Success - Main actions/checkout@v3
[use-environment-variables/use-environment-variables] ⭐ Run Main node .github/scripts/use_environment_variables.js
[use-environment-variables/use-environment-variables]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/1] user= workdir=
| START------
| nektos/act
| foo
| 8765
| END------
[use-environment-variables/use-environment-variables]   ✅  Success - Main node .github/scripts/use_environment_variables.js
[use-environment-variables/use-environment-variables] 🏁  Job succeeded

イベント情報の取得

github. プレイックスで始まるイベント情報も取得できます。

$ cat .github/workflows/print-variables.yaml
---
name: Print github contexts
run-name: Print github contexts
on:
  - push

jobs:
  print_github_contexts:
    runs-on: ubuntu-22.04
    env:
      TZ: "Asia/Tokyo"

    steps:
      - name: Print Github Contexts
        run: |
          echo "github.action ${{github.action}}"
          echo "github.action_path ${{github.action_path}}"
          echo "github.actor ${{github.actor}}"
          echo "github.base_ref ${{github.base_ref}}"
          echo "github.event ${{github.event}}"
          echo "github.event_name ${{github.event_name}}"
          echo "github.event_path ${{github.event_path}}"
          echo "github.head_ref ${{github.head_ref}}"
          echo "github.job ${{github.job}}"
          echo "github.ref ${{github.ref}}"
          echo "github.repository ${{github.repository}}"
          echo "github.repository_owner ${{github.repository_owner}}"
          echo "github.run_id ${{github.run_id}}"
          echo "github.run_number ${{github.run_number}}"
          echo "github.sha ${{github.sha}}"
          echo "github.token ${{github.token}}"
          echo "github.workflow ${{github.workflow}}"
          echo "github.workspace ${{github.workspace}}"

$ act --workflows .github/workflows/print-variables.yaml
[Print github contexts/print_github_contexts] 🚀  Start image=catthehacker/ubuntu:act-22.04
[Print github contexts/print_github_contexts]   🐳  docker pull image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 username= forcePull=true
[Print github contexts/print_github_contexts]   🐳  docker create image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[Print github contexts/print_github_contexts]   🐳  docker run image=catthehacker/ubuntu:act-22.04 platform=linux/amd64 entrypoint=["tail" "-f" "/dev/null"] cmd=[]
[Print github contexts/print_github_contexts] ⭐ Run Main Print Github Contexts
[Print github contexts/print_github_contexts]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/0] user= workdir=
| github.action 0
| github.action_path
| github.actor nektos/act
| github.base_ref
| github.event Object
| github.event_name push
| github.event_path /var/run/act/workflow/event.json
| github.head_ref
| github.job print_github_contexts
| github.ref refs/tags/v1.0.0
| github.repository genzouw/learning-github-actions
| github.repository_owner genzouw
| github.run_id 1
| github.run_number 1
| github.sha e0ddc159d2e4dc5047108ed13d88e5c68966e279
| github.token
| github.workflow Print github contexts
| github.workspace /Users/genzouw/.ghq/github.com/genzouw/learning-github-actions
[Print github contexts/print_github_contexts]   ✅  Success - Main Print Github Contexts
[Print github contexts/print_github_contexts] 🏁  Job succeeded

ひとこと

シークレットを利用する事もできそうですし、もう少し使ってみたいと思います。