はじめに
GitHub Actions をちゃんと学び直したくなったので、学習用リポジトリを 1 つ作り、同じサイクルを何周も回しながら基本を整理してみました。
ブラウザで Actions タブを開いて結果を待つと手が止まりがちなので、書く → ローカルで lint → push → ターミナルで見守る → 失敗したらログを読むまでをすべてターミナルで完結させています。
この記事で学べることは次の 4 つです。
| 手順 | テーマ | 使うもの |
|---|---|---|
| 1 | ワークフローの最小構成とトリガー |
push / workflow_dispatch、コンテキスト |
| 2 | push 前に間違いを見つける | actionlint |
| 3 | 複数の Node.js バージョンで動かして、落ちたらログを読む |
matrix / --log-failed
|
| 4 | アクセスキーなしで AWS に入る | OIDC、IAM ロール |
完成版は次のリポジトリに置いています。
やってみた
手順 0:準備
GitHub CLI(gh)がログイン済みか確認し(未ログインなら gh auth login)、ワークフローの静的チェックツール actionlint をインストールします。
そのうえで、学習用の public リポジトリを作って clone し、ブランチ名を main にそろえます。
brew install actionlint
gh auth status
gh repo create gha-basics-demo --public --clone
cd gha-basics-demo
git switch -c main
手順 1:最初のワークフロー(hello.yml)
.github/workflows/ に YAML を置くだけで、それがワークフローになります。
中身は「いつ動くか(on)」と「何をするか(jobs)」の 2 つです。
ジョブは 1 台の仮想マシン上で動き、その中のステップが上から順に実行されます。
push に加えて workflow_dispatch(手動実行)も付けておくと、後から gh workflow run で何度でも動かせて便利です(ワークフロー構文)。
mkdir -p .github/workflows
cat > .github/workflows/hello.yml <<'EOF'
name: hello
on:
push:
branches: [main]
workflow_dispatch:
jobs:
greet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Say hello
run: |
echo "Hello from ${{ github.repository }}"
echo "event=${{ github.event_name }} os=${{ runner.os }}"
ls -la
EOF
actionlint
git add . && git commit -m "Add hello workflow" && git push -u origin main
${{ }} は式で、github.event_name のようなコンテキスト(実行時の情報が入った変数)を参照でき、シェルに渡る前に値へ置き換わります(コンテキスト)。
push したら、最新の実行が終わるまでターミナルで見守ります(gh run watch)。
sleep 10
gh run watch $(gh run list --workflow hello.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
gh run view $(gh run list --workflow hello.yml --limit 1 --json databaseId --jq '.[0].databaseId') --log | grep "event="
ログに event=push os=Linux と出ていれば成功です。
手順 2:actionlint で push 前に間違いを見つける
ワークフローの間違いは、push してみるまで気づけないことが多いです。 そこで、わざと 2 か所を間違えて actionlint にかけてみます。
sed -i '' -e 's/github.event_name/github.evnt_name/' -e 's/runs-on: ubuntu-latest/runs_on: ubuntu-latest/' .github/workflows/hello.yml
actionlint
runs_on のキー名ミス(あわせて「runs-on が無い」という指摘も出ます)と、存在しないプロパティ evnt_name の計 3 件が、行番号付きで指摘されます。
では、lint しないで push したらどうなるでしょうか。evnt_name のタイポだけを残して試してみます。
git checkout .github/workflows/hello.yml
sed -i '' 's/github.event_name/github.evnt_name/' .github/workflows/hello.yml
git commit -am "typo test" && git push origin main
sleep 10
gh run watch $(gh run list --workflow hello.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
gh run view $(gh run list --workflow hello.yml --limit 1 --json databaseId --jq '.[0].databaseId') --log | grep "event="
実行は成功扱いになり、ログには event= os=Linux と空の値が出るだけでした。 存在しないプロパティは空文字として評価される仕様で、エラーで止まらないので、push 前の actionlint が効いてくるのはこういうミスです。
確認できたら元に戻します。
git revert --no-edit HEAD && git push origin main
VS Code なら公式の GitHub Actions 拡張機能でも同じミスが編集中に表示されます。actionlint は終了コードで合否が分かるので、CI や AI エージェントに確認させる用途にも向いています。
手順 3:matrix で複数の Node.js バージョンに流し、落ちたログを読む(ci.yml)
matrix を使うと、同じジョブを Node.js のバージョンごとに並べて実行できます(ジョブのバリエーション)。
ライブラリやアプリが「どのバージョンで動くか」を確かめる、matrix の定番の使い方です。
ここでは、配列をグループ分けする Object.groupBy を Node.js 20 / 22 / 24 で動かしてみます。
cat > .github/workflows/ci.yml <<'EOF'
name: ci
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
- name: Group numbers
run: |
node -e 'console.log(JSON.stringify(Object.groupBy([1, 2, 3, 4], (n) => (n % 2 ? "odd" : "even"))))'
EOF
actionlint
git add . && git commit -m "Add matrix CI" && git push origin main
sleep 10
gh run watch $(gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
${{ matrix.node-version }} に 20 / 22 / 24 が順に入り、actions/setup-node がそのバージョンの Node.js を用意します。
fail-fast: false は「1 つ落ちても残りを止めない」設定です。これがないと、1 つが落ちた時点で他がキャンセルされることがあります。
22 と 24 は {"odd":[1,3],"even":[2,4]} と表示されて成功し、20 だけが失敗します。 失敗したステップのログだけを見るには --log-failed を使います(gh run view)。
gh run view $(gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId') --log-failed
TypeError: Object.groupBy is not a function が原因です。 Object.groupBy は Node.js 21 で追加された機能なので、20 には存在しません。
対応は「コードを 20 でも動く書き方に変える」か「20 をサポート対象から外す」かのどちらかです。Node.js 20 は 2026-04-30 にサポートが終了しているので、matrix から外すことが対応として妥当です。
手順 4:アクセスキーなしで AWS に入る(aws.yml)
GitHub Actions から AWS を操作するには、AWS のアクセスキーを GitHub に保存する方法もあります。
ただ、OIDC を使えば、GitHub が実行のたびに発行する「短命の身分証」を AWS に見せるだけで入れます(AWS での OIDC の構成)。
AWS 側には「このリポジトリの main ブランチから来た身分証だけを受け入れる」ロールを作ります。 身分証の中でリポジトリを表す欄を sub と呼び、その値をロールの条件に書きます。
aws iam list-open-id-connect-providers --output text | grep token.actions.githubusercontent.com \
|| aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
SUB=$(gh api repos/{owner}/{repo} --jq '"repo:\(.owner.login)@\(.owner.id)/\(.name)@\(.id):ref:refs/heads/main"')
echo "$SUB"
cat > trust.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "${SUB}"
}}
}]
}
EOF
ROLE_ARN=$(aws iam create-role --role-name gha-basics-demo-role \
--assume-role-policy-document file://trust.json --query Role.Arn --output text)
gh variable set AWS_ROLE_ARN --body "$ROLE_ARN"
rm trust.json
最初の 2 行は、GitHub を AWS に登録する作業です。
作ったロールの ARN はリポジトリ変数に入れて、ワークフローから vars.AWS_ROLE_ARN で読めるようにしています。
ワークフロー側は、id-token: write(身分証をもらう許可)を付けて、公式の Action にロールを渡すだけです。
cat > .github/workflows/aws.yml <<'EOF'
name: aws
on:
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
whoami:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
mask-aws-account-id: true
- run: aws sts get-caller-identity --query Arn --output text
EOF
actionlint
git add . && git commit -m "Add AWS OIDC workflow" && git push origin main
gh workflow run aws.yml
sleep 10
gh run watch $(gh run list --workflow aws.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
最後のステップで assumed-role/gha-basics-demo-role/GitHubActions と出れば、アクセスキーなしで AWS に入れています。
ハマりどころ:新しいリポジトリは sub に ID が付く
ネット上でよく見る書き方では、今回は失敗しました。2026-07-15 より後に作ったリポジトリは、sub に名前だけでなく ID(@数字)も入るためです。
ロールに書いた sub
|
結果 |
|---|---|
repo:leomaro7/gha-basics-demo:ref:refs/heads/main |
失敗 |
repo:leomaro7@50868643/gha-basics-demo@1403381713:ref:refs/heads/main |
成功 |
リポジトリ名は、手放すと他人が同じ名前で作り直せます。名前だけで信頼すると、別人のリポジトリを受け入れてしまうおそれがあるため、ID が加わりました。上の手順で gh api から ID を取っているのはこのためです。
手順 5:後片付け
IAM ロールとリポジトリ変数を削除します。ID プロバイダーは、手順 4 で新しく作った場合だけ削除してください(他のワークフローが使っている可能性があるため)。
aws iam delete-role --role-name gha-basics-demo-role
gh variable delete AWS_ROLE_ARN
# 手順 4 で ID プロバイダーを新規作成した場合のみ
# aws iam delete-open-id-connect-provider --open-id-connect-provider-arn "arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):oidc-provider/token.actions.githubusercontent.com"




