概要
最近CI/CDまわりをリポジトリ内でちゃんと整備していこうかと考えています。GitHub Actionsをうまく使えばいろいろなことができそうなのですが、そもそもActionsで何ができるかもよくわかっていないうえ、毎度GitHubにpushしなければ試せないので、めんどくさい...
また、mainブランチ(デフォルトブランチ)にActionsのファイルがないと手動のワークフローであればそもそも選択すらできない状態だったりと、気軽に試すことへのハードルが高い状態でした。
そんな中、ローカルで手軽にActionsを試す方法がないかと調べていたところ、「act」というツールがあったので、導入して試してみようと思います。
actとは
紹介したとおり、GitHub Actionsのワークフローをローカルで実行してくれるツール。Docker APIを利用して、ワークフロー内で定義されたイメージを構築し、GitHubが提供するものと一致する環境にしたうえでワークフローを実行してくれるみたいです。
試してみる
環境
- OS:Windows 11(WSL導入済み)
- docker導入済み(Docker Desktopは入っていない状態)
導入
DockerコマンドをWindowsから叩けるようにしていないので、WSL上にactはインストールします。WSLに入り、以下コマンドでインストール。
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
一応エイリアスも付けておきます
echo "alias act='{actのファイル場所}'" >> ~/.bashrc
サンプルリポジトリで試す
公式にあった以下サンプルリポジトリで試してみます。
テスト用のActionsはこんな感じ。めちゃシンプルですが、checkoutとnpm testを行います。
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm install
- run: npm test
クローンしたら、 -nオプションをつけて実行してみます。このオプションはdry-runができるオプションです。
papa@ENVY:~/workspace/github-actions-demo$ act -n
INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock'
? Please choose the default image you want to use with act:
- Large size image: ca. 17GB download + 53.1GB storage, you will need 75GB of free disk space, snapshots of GitHub Hosted Runners without snap and pulled docker images
- Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with most 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 /home/papa/.config/act/actrc (please refer to https://nektosact.com/usage/index.html?highlight=configur#configuration-file for additional information about file structure) [Use arrows to move, type to filter, ? for more help]
Large
> Medium
Micro
なんかデフォルトのイメージサイズを選べるみたいなので、とりあえずデフォルトのMediumにしておきます。その後dry-runでActionsファイルの検証をしてくれました。
*DRYRUN* [CI/test] ⭐ Run Set up job
*DRYRUN* [CI/test] 🚀 Start image=catthehacker/ubuntu:act-latest
*DRYRUN* [CI/test] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true
*DRYRUN* [CI/test] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
*DRYRUN* [CI/test] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
*DRYRUN* [CI/test] ✅ Success - Set up job
*DRYRUN* [CI/test] ☁ git clone 'https://github.com/actions/setup-node' # ref=v1
*DRYRUN* [CI/test] ⭐ Run Main actions/checkout@v2
*DRYRUN* [CI/test] ✅ Success - Main actions/checkout@v2 [3.960898ms]
*DRYRUN* [CI/test] ⭐ Run Main actions/setup-node@v1
*DRYRUN* [CI/test] ✅ Success - Main actions/setup-node@v1 [4.383822ms]
*DRYRUN* [CI/test] ⭐ Run Main npm install
*DRYRUN* [CI/test] ✅ Success - Main npm install [15.374775ms]
*DRYRUN* [CI/test] ⭐ Run Main npm test
*DRYRUN* [CI/test] ✅ Success - Main npm test [14.103341ms]
*DRYRUN* [CI/test] ⭐ Run Complete job
*DRYRUN* [CI/test] Cleaning up container for job test
*DRYRUN* [CI/test] ✅ Success - Complete job
*DRYRUN* [CI/test] 🏁 Job succeeded
満を持して本実行もやってみます。
papa@ENVY:~/workspace/github-actions-demo$ act
INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock'
[CI/test] ⭐ Run Set up job
[CI/test] 🚀 Start image=catthehacker/ubuntu:act-latest
[CI/test] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true
[CI/test] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[CI/test] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[CI/test] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir=
[CI/test] ✅ Success - Set up job
[CI/test] ☁ git clone 'https://github.com/actions/setup-node' # ref=v1
[CI/test] ⭐ Run Main actions/checkout@v2
[CI/test] 🐳 docker cp src=/home/papa/workspace/github-actions-demo/. dst=/home/papa/workspace/github-actions-demo
[CI/test] ✅ Success - Main actions/checkout@v2 [21.559058ms]
[CI/test] ⭐ Run Main actions/setup-node@v1
[CI/test] 🐳 docker cp src=/home/papa/.cache/act/actions-setup-node@v1/ dst=/var/run/act/actions/actions-setup-node@v1/
[CI/test] 🐳 docker exec cmd=[/opt/acttoolcache/node/18.20.8/x64/bin/node /var/run/act/actions/actions-setup-node@v1/dist/index.js] user= workdir=
| [command]/usr/bin/tar xz --warning=no-unknown-keyword -C /tmp/0b4179be-4517-414e-9994-baff7e8f6edf -f /tmp/b2a498c2-87dc-4bea-af88-b9a5d6166147
| [command]/opt/hostedtoolcache/node/10.24.1/x64/bin/node --version
| v10.24.1
| [command]/opt/hostedtoolcache/node/10.24.1/x64/bin/npm --version
| 6.14.12
[CI/test] ❓ add-matcher /run/act/actions/actions-setup-node@v1/.github/tsc.json
[CI/test] ❓ add-matcher /run/act/actions/actions-setup-node@v1/.github/eslint-stylish.json
[CI/test] ❓ add-matcher /run/act/actions/actions-setup-node@v1/.github/eslint-compact.json
[CI/test] ✅ Success - Main actions/setup-node@v1 [3.93772134s]
[CI/test] ⚙ ::add-path:: /opt/hostedtoolcache/node/10.24.1/x64/bin
[CI/test] ⭐ Run Main npm install
[CI/test] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/2] user= workdir=
| added 280 packages from 643 contributors and audited 280 packages in 3s
|
| 24 packages are looking for funding
| run `npm fund` for details
|
| found 56 vulnerabilities (12 low, 17 moderate, 23 high, 4 critical)
| run `npm audit fix` to fix them, or `npm audit` for details
[CI/test] ✅ Success - Main npm install [3.333388341s]
[CI/test] ⭐ Run Main npm test
[CI/test] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/3] user= workdir=
|
| > github-actions-demo@1.0.0 test /home/papa/workspace/github-actions-demo
| > mocha ./tests --recursive
|
|
|
| GET /
| ✓ should respond with hello world
|
|
| 1 passing (14ms)
|
[CI/test] ✅ Success - Main npm test [349.237214ms]
[CI/test] ⭐ Run Complete job
[CI/test] Cleaning up container for job test
[CI/test] ✅ Success - Complete job
[CI/test] 🏁 Job succeeded
ActionsでのJSのテストまで正常に実行してくれました。
上の実験では勝手にpushイベントを実行してくれました。デフォルトで、何もイベント名を指定せずにactを実行すると、pushイベントが呼ばれるらしいです。
特定のイベントを実行したい場合は、act pull_requestやact scheduleなどでコマンドを実行すればOK!
オプションなど
特定のワークフローのみ実行(-W)
デフォルトではすべてのワークフローファイルを実行しようとするみたいです。特定のワークフローファイルのみを実行させたい場合は、-Wオプションでファイルを指定します。
act -W '.github/workflows/checks.yml'
特定のジョブのみ実行(-j)
ジョブも同じように、デフォルトではすべてのジョブを実行します。特定のジョブのみを実行させたい場合は、-jオプションでジョブ名を指定します。
act -j 'test'
この例であれば、そのリポジトリ内のすべてのワークフローのうち、pushイベントに紐づいているtestという名前のジョブがすべて起動する、という形になります。
GitHub variablesの置き換え(--var, --var-file)
GitHub variablesで置き換わるはずの値をローカルで再現することも可能です。
# 直接コマンドから指定
act --var VARIABLE=some-value
# ファイルから読み込み
act --var-file my.variables
試しにこんな感じのワークフローと、variableが記述されたファイルを用意してみました。
name: Test Variables
on: push
jobs:
test-variables:
runs-on: ubuntu-latest
steps:
- name: Check GitHub Variables
run: |
echo "=== GitHub Variables ==="
echo "MY_VARIABLE: ${{ vars.MY_VARIABLE }}"
echo "API_ENDPOINT: ${{ vars.API_ENDPOINT }}"
echo "ENVIRONMENT: ${{ vars.ENVIRONMENT }}"
- name: Use Variables
env:
MY_VAR: ${{ vars.MY_VARIABLE }}
MY_ENV: ${{ vars.ENVIRONMENT }}
run: |
echo "=== Combined Usage ==="
echo "Variable MY_VARIABLE is: $MY_VAR"
echo "Environment is: $MY_ENV"
MY_VARIABLE=test-value-123
API_ENDPOINT=https://api.example.com
ENVIRONMENT=development
実行結果がこちら。vars.***の部分が正常に置き換わっていることが確認できます
papa@ENVY:~/workspace/github-actions-demo$ act -W .github/workflows/test-variables.yml --var-file my.variables
INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock'
[Test Variables/test-variables] ⭐ Run Set up job
[Test Variables/test-variables] 🚀 Start image=catthehacker/ubuntu:act-latest
[Test Variables/test-variables] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true
[Test Variables/test-variables] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[Test Variables/test-variables] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[Test Variables/test-variables] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir=
[Test Variables/test-variables] ✅ Success - Set up job
[Test Variables/test-variables] ⭐ Run Main Check GitHub Variables
[Test Variables/test-variables] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir=
| === GitHub Variables ===
| MY_VARIABLE: test-value-123
| API_ENDPOINT: https://api.example.com
| ENVIRONMENT: development
[Test Variables/test-variables] ✅ Success - Main Check GitHub Variables [57.765277ms]
[Test Variables/test-variables] ⭐ Run Main Use Variables
[Test Variables/test-variables] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir=
| === Combined Usage ===
| Variable MY_VARIABLE is: test-value-123
| Environment is: development
[Test Variables/test-variables] ✅ Success - Main Use Variables [69.503679ms]
[Test Variables/test-variables] ⭐ Run Complete job
[Test Variables/test-variables] Cleaning up container for job test-variables
[Test Variables/test-variables] ✅ Success - Complete job
[Test Variables/test-variables] 🏁 Job succeeded
同様に、シークレット情報も同じような形で実行できます。これにより、ワークフロー内のsecrets.***の値が置き換わります。
- コマンドから:
act -s MY_SECRET=some-value - ファイルから:
act --secret-file my.secrets
さらに実験:GitHubの機能も実行できる?
issueへの自動コメント返信やProjectのステータス変更など、GitHubの操作回りなどもローカルで試せるみたい?探り探りやってみます。Issueへの自動コメント返信を試してみましょう。
ワークフローを作る
こんな感じで、Issueがopenになったときに固定のコメントを返却するようなワークフローを試してみます。
name: Auto Comment on Issue
on:
issues:
types: [opened]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Comment on new issue
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '👋 こんにちは!Issueを作成していただきありがとうございます。\n\n担当者が確認次第、対応させていただきます。'
})
いったん実行
何も考えずにact issuesで実行してみます。
| Error: Input required and not supplied: github-token
| at Object.getInput (/run/act/actions/actions-github-script@v7/dist/index.js:212:15)
| at main (/run/act/actions/actions-github-script@v7/dist/index.js:36262:24)
| at /run/act/actions/actions-github-script@v7/dist/index.js:36260:1
| at /run/act/actions/actions-github-script@v7/dist/index.js:36317:3
| at Object.<anonymous> (/run/act/actions/actions-github-script@v7/dist/index.js:36320:12)
| at Module._compile (node:internal/modules/cjs/loader:1364:14)
| at Module._extensions..js (node:internal/modules/cjs/loader:1422:10)
| at Module.load (node:internal/modules/cjs/loader:1203:32)
| at Module._load (node:internal/modules/cjs/loader:1019:12)
| at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12)
なるほど、ローカルからの実行なのでGitHub tokenが必要みたいですね。これはSecretと同じように登録できるみたいなので、別ファイルとして保存してコマンドから読み込めるようにしましょう。
GITHUB_TOKEN=ghp_*************************
以下コマンドで再実行してみます。そもそも紐づけるべきIssueもないはずなのでどうなるんでしょう...
act issues --secret-file my.token
| RequestError [HttpError]: Not Found
| at /run/act/actions/actions-github-script@v7/dist/index.js:9537:21
| at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
| status: 404,
| response: {
| url: 'https://api.github.com/repos/papaHasuo/act-github-test/issues//comments',
| status: 404,
| headers: {
| 'access-control-allow-origin': '*',
| 'access-control-expose-headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset',
| connection: 'close',
| 'content-security-policy': "default-src 'none'",
| 'content-type': 'application/json; charset=utf-8',
| date: 'Sun, 01 Feb 2026 09:01:56 GMT',
| 'referrer-policy': 'origin-when-cross-origin, strict-origin-when-cross-origin',
| server: 'github.com',
| 'strict-transport-security': 'max-age=31536000; includeSubdomains; preload',
| vary: 'Accept-Encoding, Accept, X-Requested-With',
| 'x-content-type-options': 'nosniff',
| 'x-frame-options': 'deny',
| 'x-github-media-type': 'github.v3; format=json',
| 'x-github-request-id': 'EC4D:20C716:3B5415:5010A4:697F1683',
| 'x-xss-protection': '0'
| },
| data: {
| message: 'Not Found',
| documentation_url: 'https://docs.github.com/rest',
| status: '404'
| }
| },
| request: {
| method: 'POST',
| url: 'https://api.github.com/repos/papaHasuo/act-github-test/issues//comments',
| headers: {
| accept: 'application/vnd.github.v3+json',
| 'user-agent': 'actions/github-script octokit-core.js/5.0.1 Node.js/18.20.8 (linux; x64)',
| authorization: 'token [REDACTED]',
| 'content-type': 'application/json; charset=utf-8'
| },
| body: '{"body":"👋 こんにちは!Issueを作成していただきありがとうございます。\\n\\n担当者が確認次第、対応させていただきます。"}',
| request: {
| agent: [Agent],
| fetch: [Function: proxyFetch],
| hook: [Function: bound bound register]
| }
| }
| }
そりゃそう、404エラーが出ちゃいました。ただしよく見ると
request: {
| method: 'POST',
| url: 'https://api.github.com/repos/papaHasuo/act-github-test/issues//comments',
URLが/issue//commentsになっており、Issue番号が何も指定されていないみたいですね。おそらくですが、Actionsファイル内のcontext.issue.number部分がうまく取得できていないのかと思います。逆に言えば、ownerやrepo自体の取得はちゃんとできてそうですね。
script: |
github.rest.issues.createComment({
issue_number: context.issue.number, ← **ここが取得できない**
owner: context.repo.owner,
repo: context.repo.repo,
body: '👋 こんにちは!Issueを作成していただきありがとうございます。\n\n担当者が確認次第、対応させていただきます。'
})
イベントペイロードの指定(-eオプション)
-eをつけることで、イベントの詳細データをJSON形式で読み込んでくれるみたいです。今回で行くと、openになったIssueの情報を前もってJSONの形でローカルに持たせておけば、その情報を読み込めるらしい...今回はIssue番号さえわかればいいので、以下のファイルを用意します。
{
"issue": {
"number": 1
}
}
そのうえで、コマンドを変えて実行してみました。
act issues -e .github/workflows/issue-opened.json --secret-file my.token
| request: {
| method: 'POST',
| url: 'https://api.github.com/repos/papaHasuo/act-github-test/issues/1/comments',
| headers: {
| accept: 'application/vnd.github.v3+json',
| 'user-agent': 'actions/github-script octokit-core.js/5.0.1 Node.js/18.20.8 (linux; x64)',
| authorization: 'token [REDACTED]',
| 'content-type': 'application/json; charset=utf-8'
| },
| body: '{"body":"👋 こんにちは!Issueを作成していただきありがとうございます。\\n\\n担当者が確認次第、対応させていただきます。"}',
| request: {
| agent: [Agent],
| fetch: [Function: proxyFetch],
| hook: [Function: bound bound register]
| }
| }
| }
さっきと違いURL情報がちゃんと作られていそうですね!
もしかしてこれって、該当リポジトリにちゃんとIssueがあれば作ってくれる...?
できました!下準備さえあればGitHubの操作もある程度できそうですね!
まとめのリポジトリはこちら
感想
思ったよりもいろんなことができそうなツールでした。チェックアウトしてビルド、あたりは何となく想像ついていましたが、Issue周りの自動化もローカルで試せるとは...
ただ流石にデプロイ回りなども考え出すと、権限周りとかも含めてローカルで考えるほうが面倒になるので、それに関してはもうGitHubにコミットして直接実験するほうが良いのかな、と思いました。dry-runで構文周りのチェックだけはやってもいいかも。
個人的には、GitHubとの紐づけでどんなことができるのかちょっと試したいときに、毎回試行錯誤しながらのコミットが大量に残るのが嫌だったので、そこをローカルで試せるのは大変ありがたいです。いろんなちまちました作業を自動化できそう!

