Run your GitHub Actions locally! Why would you want to do this? Two reasons:
Fast Feedback - Rather than having to commit/push every time you want to test out the changes you are making to your .github/workflows/ files (or for any changes to embedded GitHub actions), you can use act to run the actions locally. The environment variables and filesystem are all configured to match what GitHub provides.
Local Task Runner - I love make. However, I also hate repeating myself. With act, you can use the GitHub Actions defined in your .github/workflows/ to replace your Makefile!
$ go install github.com/nektos/[email protected] $ act --version act version 0.2.78
helpを見ると、オプションが豊富なことも分かります。
$ act --help Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.
Usage: act [event name to run] [flags]
If no event name passed, will default to "on: push" If actions handles only one event it will be used as default instead of "on: push"
Flags: --action-cache-path string Defines the path where the actions get cached and host workspaces created. (default "/home/mano/.cache/act") --action-offline-mode If action contents exists, it will not be fetch and pull again. If turn on this, will turn off force pull -a, --actor string user that triggered the event (default "nektos/act") --artifact-server-addr string Defines the address to which the artifact server binds. (default "172.29.0.214") --artifact-server-path string Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start. --artifact-server-port string Defines the port where the artifact server listens. (default "34567") -b, --bindbind working directory to container, rather than copy --bug-report Display system information for bug report --cache-server-addr string Defines the address to which the cache server binds. (default "172.29.0.214") --cache-server-external-url string Defines the external URL forif the cache server is behind a proxy. e.g.: https://act-cache-server.example.com. Be careful that there is no trailing slash. --cache-server-path string Defines the path where the cache server stores caches. (default "/home/mano/.cache/actcache") --cache-server-port uint16 Defines the port where the artifact server listens. 0 means a randomly available port. --concurrent-jobs int Maximum number of concurrent jobs to run. Default is the number of CPUs available. --container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms. --container-cap-add stringArray kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE) --container-cap-drop stringArray kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE) --container-daemon-socket string URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disablebind mounting the socket) --container-options string Custom docker container options for the job container without an options property in the job definition --defaultbranch string the name of the main branch --detect-event Use first event type from workflow as event that triggered the workflow -C, --directory string working directory (default ".") -n, --dryrun disable container creation, validates only workflow correctness --env stringArray env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv) --env-file string environment file to read and use as envin the containers (default ".env") -e, --eventpath string path to event JSON file --github-instance string GitHub instance to use. Only use this when using GitHub Enterprise Server. (default "github.com") -g, --graph draw workflows -h, --helphelpfor act --input stringArray action input to make available to actions (e.g. --input myinput=foo) --input-file string input file to read and use as action input (default ".input") --insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs. -j, --job string run a specific job ID --json Output logs in json format -l, --list list workflows --list-options Print a json structure of compatible options --local-repository stringArray Replaces the specified repository and ref with a local folder (e.g. https://github.com/test/test@v0=/home/act/test or test/test@v0=/home/act/test, the latter matches any hosts or protocols) --log-prefix-job-id Output the job id within non-json logs instead of the entire name --man-page Print a generated manual page to stdout --matrix stringArray specify which matrix configuration to include (e.g. --matrix java:13 --network string Sets a docker network name. Defaults to host. (default "host") --no-cache-server Disable cache server --no-recurse Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag --no-skip-checkout Use actions/checkout instead of copying local files into container -P, --platform stringArray custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04) --privileged use privileged mode -p, --pull pull docker image(s) even if already present (default true) -q, --quiet disable logging of output from steps --rebuild rebuild local action docker image(s) even if already present (default true) --remote-name string git remote name that will be used to retrieve url of git repo (default "origin") --replace-ghe-action-token-with-github-com string If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set personal access token --replace-ghe-action-with-github-com stringArray If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com =github/super-linter) -r, --reuse don't remove container(s) on successfully completed workflow(s) to maintain state between runs --rm automatically remove container(s)/volume(s) after a workflow(s) failure -s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret) --secret-file string file with list of secrets to read from (e.g. --secret-file .secrets) (default ".secrets") --use-gitignore Controls whether paths specified in .gitignore should be copied into container (default true) --use-new-action-cache Enable using the new Action Cache for storing Actions locally --userns string user namespace to use --var stringArray variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar) --var-file string file with list of vars to read from (e.g. --var-file .vars) (default ".vars") -v, --verbose verbose output --version version for act -w, --watch watch the contents of the local repo and run when files change -W, --workflows string path to workflow file(s) (default "./.github/workflows/")
$ time act -j greet INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/greet] ⭐ Run Set up job [Local Development Tasks/greet] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/greet] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true [Local Development Tasks/greet] using DockerAuthConfig authentication for docker pull [Local Development Tasks/greet] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/greet] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/greet] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/greet] ✅ Success - Set up job [Local Development Tasks/greet] ⭐ Run Main Say Hello [Local Development Tasks/greet] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir= | 🐈️こんにちニャン [Local Development Tasks/greet] ✅ Success - Main Say Hello [115.57099ms] [Local Development Tasks/greet] ⭐ Run Complete job [Local Development Tasks/greet] Cleaning up container for job greet [Local Development Tasks/greet] ✅ Success - Complete job [Local Development Tasks/greet] 🏁 Job succeeded
続いて、リポジトリ上で ls -la をします。ローカル実行とは言え、実体はDockerコンテナ上で動作するため actions/checkout@v4 をする必要があります。先程の local-tasks.yaml の最後に以下を追加します。
.github/workflows/local-tasks.yaml
+ list-files: # ファイル一覧を表示するジョブ + runs-on: ubuntu-latest + steps: + - name: Checkout code # コードをチェックアウトしないとプロジェクトファイルにアクセスできない + uses: actions/checkout@v4 + - name: List current directory + run: ls -la
act -j list-files で実行します。
$ time act -j list-files INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/list-files] ⭐ Run Set up job [Local Development Tasks/list-files] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/list-files] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true [Local Development Tasks/list-files] using DockerAuthConfig authentication for docker pull [Local Development Tasks/list-files] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/list-files] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/list-files] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/list-files] ✅ Success - Set up job [Local Development Tasks/list-files] ⭐ Run Main Checkout code [Local Development Tasks/list-files] 🐳 docker cp src=/home/mano/actsample/. dst=/home/mano/actsample [Local Development Tasks/list-files] ✅ Success - Main Checkout code [50.644993ms] [Local Development Tasks/list-files] ⭐ Run Main List current directory [Local Development Tasks/list-files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir= | total 16 | drwxr-xr-x 4 root root 4096 Jun 7 00:33 . | drwxr-xr-x 3 root root 4096 Jun 7 00:33 .. | drwxr-xr-x 7 root root 4096 Jun 7 00:33 .git | drwxr-xr-x 3 root root 4096 Jun 7 00:33 .github [Local Development Tasks/list-files] ✅ Success - Main List current directory [125.864705ms] [Local Development Tasks/list-files] ⭐ Run Complete job [Local Development Tasks/list-files] Cleaning up container for job list-files [Local Development Tasks/list-files] ✅ Success - Complete job [Local Development Tasks/list-files] 🏁 Job succeeded
$ time act -P ubuntu-latest=node:16-buster-slim -j greet INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/greet] ⭐ Run Set up job [Local Development Tasks/greet] 🚀 Start image=node:16-buster-slim [Local Development Tasks/greet] 🐳 docker pull image=node:16-buster-slim platform= username= forcePull=true [Local Development Tasks/greet] using DockerAuthConfig authentication for docker pull [Local Development Tasks/greet] 🐳 docker create image=node:16-buster-slim platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/greet] 🐳 docker run image=node:16-buster-slim platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/greet] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/greet] ✅ Success - Set up job [Local Development Tasks/greet] ⭐ Run Main Say Hello [Local Development Tasks/greet] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir= | 🐈️こんにちニャン [Local Development Tasks/greet] ✅ Success - Main Say Hello [101.2046ms] [Local Development Tasks/greet] ⭐ Run Complete job [Local Development Tasks/greet] Cleaning up container for job greet [Local Development Tasks/greet] ✅ Success - Complete job [Local Development Tasks/greet] 🏁 Job succeeded
$ time act --action-offline-mode -j greet INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/greet] ⭐ Run Set up job [Local Development Tasks/greet] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/greet] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=false [Local Development Tasks/greet] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/greet] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/greet] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/greet] ✅ Success - Set up job [Local Development Tasks/greet] ⭐ Run Main Say Hello [Local Development Tasks/greet] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir= | 🐈️こんにちニャン [Local Development Tasks/greet] ✅ Success - Main Say Hello [119.563033ms] [Local Development Tasks/greet] ⭐ Run Complete job [Local Development Tasks/greet] Cleaning up container for job greet [Local Development Tasks/greet] ✅ Success - Complete job [Local Development Tasks/greet] 🏁 Job succeeded
e$ time act --action-offline-mode -j list-files INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/list-files] ⭐ Run Set up job [Local Development Tasks/list-files] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/list-files] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=false [Local Development Tasks/list-files] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/list-files] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/list-files] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/list-files] ✅ Success - Set up job [Local Development Tasks/list-files] ⭐ Run Main Checkout code [Local Development Tasks/list-files] 🐳 docker cp src=/home/mano/actsample/. dst=/home/mano/actsample [Local Development Tasks/list-files] ✅ Success - Main Checkout code [34.498731ms] [Local Development Tasks/list-files] ⭐ Run Main List current directory [Local Development Tasks/list-files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir= | total 16 | drwxr-xr-x 4 root root 4096 Jun 7 01:29 . | drwxr-xr-x 3 root root 4096 Jun 7 01:29 .. | drwxr-xr-x 7 root root 4096 Jun 7 01:29 .git | drwxr-xr-x 3 root root 4096 Jun 7 01:29 .github [Local Development Tasks/list-files] ✅ Success - Main List current directory [122.866479ms] [Local Development Tasks/list-files] ⭐ Run Complete job [Local Development Tasks/list-files] Cleaning up container for job list-files [Local Development Tasks/list-files] ✅ Success - Complete job [Local Development Tasks/list-files] 🏁 Job succeeded
$ time act --action-offline-mode -j list-files INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/list-files] ⭐ Run Set up job [Local Development Tasks/list-files] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/list-files] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=false [Local Development Tasks/list-files] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/list-files] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/list-files] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/list-files] ✅ Success - Set up job [Local Development Tasks/list-files] ⭐ Run Main Checkout code [Local Development Tasks/list-files] 🐳 docker cp src=/home/mano/myRepo/. dst=/home/mano/myRepo [Local Development Tasks/list-files] ✅ Success - Main Checkout code [5.007896049s] [Local Development Tasks/list-files] ⭐ Run Main List current directory [Local Development Tasks/list-files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir= | total 236 (中略) | drwxr-xr-x 18 root root 4096 Jun 9 01:04 tool [Local Development Tasks/list-files] ✅ Success - Main List current directory [126.408501ms] [Local Development Tasks/list-files] ⭐ Run Complete job [Local Development Tasks/list-files] Cleaning up container for job list-files [Local Development Tasks/list-files] ✅ Success - Complete job [Local Development Tasks/list-files] 🏁 Job succeeded
+ lint: + name: Lint Go Files + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + - name: Run vet + run: go vet ./...
エラーの例です。 failed to verify certificate: x509: certificate signed by unknown authority とカスタム証明書を利用している環境において、あるあるなエラーが出ています。
[Local Development Tasks/Format Go Files] Unable to clone https://github.com/actions/setup-go refs/heads/v5: Get "https://github.com/actions/setup-go/info/refs?service=git-upload-pack": tls: failed to verify certificate: x509: certificate signed by unknown authority
$ time act -j lint INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/Lint Go Files] ⭐ Run Set up job [Local Development Tasks/Lint Go Files] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/Lint Go Files] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true [Local Development Tasks/Lint Go Files] using DockerAuthConfig authentication for docker pull [Local Development Tasks/Lint Go Files] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/Lint Go Files] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/Lint Go Files] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/Lint Go Files] ✅ Success - Set up job [Local Development Tasks/Lint Go Files] ⭐ Run Main Checkout code [Local Development Tasks/Lint Go Files] 🐳 docker cp src=/home/mano/MyRepo/. dst=/home/mano/MyRepo [Local Development Tasks/Lint Go Files] ✅ Success - Main Checkout code [14.494783288s] [Local Development Tasks/Lint Go Files] ⭐ Run Main Install Go [Local Development Tasks/Lint Go Files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir= | /usr/local/go/bin [Local Development Tasks/Lint Go Files] ✅ Success - Main Install Go [16.761998127s] [Local Development Tasks/Lint Go Files] ⚙ ::add-path:: /usr/local/go/bin [Local Development Tasks/Lint Go Files] ⭐ Run Main Run vet [Local Development Tasks/Lint Go Files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/2] user= workdir= | go: downloading github.com/aws/aws-lambda-go v1.41.0 | go: downloading github.com/rs/zerolog v1.29.0 | go: downloading github.com/go-playground/validator/v10 v10.16.0 (中略) [Local Development Tasks/Lint Go Files] ✅ Success - Main Run vet [59.368599077s] [Local Development Tasks/Lint Go Files] ⭐ Run Complete job [Local Development Tasks/Lint Go Files] Cleaning up container for job Lint Go Files [Local Development Tasks/Lint Go Files] ✅ Success - Complete job [Local Development Tasks/Lint Go Files] 🏁 Job succeeded
real 1m40.689s user 0m0.769s sys 0m3.229s
厳しいのは、実行時間が1分40秒かかったというところでしょう。これは go vet を実行するためにコンパイルが必要なので、 go mod download 相当の処理が動くためです。go mod 側のキャッシュをボリュームマウントすれば高速化できると思いますが、逆に言うとそういったチューニングが必要だということです。
ちなみにもし、go vet が失敗(違反コードが存在)した場合は exit 1 でジョブが以下のように失敗します。
失敗例
| # github.com/.../... | app/my_model.go:30:2: struct field myfields has json tag but is not exported [Local Development Tasks/Lint Go Files] ❌ Failure - Main Run go vet [1m2.783279728s] [Local Development Tasks/Lint Go Files] exitcode '1': failure [Local Development Tasks/Lint Go Files] ⭐ Run Complete job [Local Development Tasks/Lint Go Files] ✅ Success - Complete job [Local Development Tasks/Lint Go Files] 🏁 Job failed Error: Job 'Lint Go Files' failed
$ time act -j fmt --container-options "-v $(pwd):/workdir" INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock' [Local Development Tasks/Format Go Files] ⭐ Run Set up job [Local Development Tasks/Format Go Files] 🚀 Start image=catthehacker/ubuntu:act-latest [Local Development Tasks/Format Go Files] 🐳 docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true [Local Development Tasks/Format Go Files] using DockerAuthConfig authentication for docker pull [Local Development Tasks/Format Go Files] 🐳 docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/Format Go Files] 🐳 docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail""-f""/dev/null"] cmd=[] network="host" [Local Development Tasks/Format Go Files] 🐳 docker exec cmd=[node --no-warnings -e console.log(process.execPath)] user= workdir= [Local Development Tasks/Format Go Files] ✅ Success - Set up job [Local Development Tasks/Format Go Files] ⭐ Run Main Install Go [Local Development Tasks/Format Go Files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir=/workdir | /usr/local/go/bin [Local Development Tasks/Format Go Files] ✅ Success - Main Install Go [17.32964836s] [Local Development Tasks/Format Go Files] ⚙ ::add-path:: /usr/local/go/bin [Local Development Tasks/Format Go Files] ⭐ Run Main Run gofmt [Local Development Tasks/Format Go Files] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir=/workdir | backend/my_model.go [Local Development Tasks/Format Go Files] ✅ Success - Main Run gofmt [711.356576ms] [Local Development Tasks/Format Go Files] ⭐ Run Complete job [Local Development Tasks/Format Go Files] Cleaning up container for job Format Go Files [Local Development Tasks/Format Go Files] ✅ Success - Complete job [Local Development Tasks/Format Go Files] 🏁 Job succeeded