ここ半年くらい、GitHubActionを触る機会が多く、そこで学んだ点や躓いた点を列挙していきます。
目次
1.入力への参照はinputsを使うべき
2.environmentとreusableWorkflow併用時の注意点
3.environmentとreusableWorkflow併用時の注意点2
4.awsへのアクセスでoidc利用したときの注意点
5.reusable-workflowのネストは4層まで
6.apiからworkflow実行時に実行したrunの特定ができない
入力への参照はinputsを使うべき
例えば手動実行できるこんなスクリプトがあるとします。
ここでinputへの参照をgithub.event.inputs
で書いてしまうと
あとで、ワークフローから再利用できるようにこうすると、inputにアクセスできず、あれーってなります。
name: foo
on:
workflow_dispatch:
inputs:
foo:
required: true
type: string
bar:
required: true
type: string
+ // ワークフローからの呼び出しを追加。
+ workflow_call:
+ inputs:
+ foo:
+ required: true
+ type: string
env:
FOO_INPUT: ${{ github.event.inputs.foo }}
BAR_INPUT: ${{ github.event.inputs.bar || 'BAR'}}
jobs:
do-something:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
workflow_dispatchとworkflow_callどちらのinputにもアクセスできるinputsコンテキストがあるので、それで参照しましょう。
name: foo
on:
workflow_dispatch:
inputs:
foo:
required: true
type: string
bar:
required: true
type: string
workflow_call:
inputs:
foo:
required: true
type: string
env:
- FOO_INPUT: ${{ github.event.inputs.foo }}
- BAR_INPUT: ${{ github.event.inputs.bar || 'BAR'}}
+ FOO_INPUT: ${{ inputs.foo }}
+ BAR_INPUT: ${{ inputs.bar || 'BAR'}}
jobs:
do-something:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
environmentとreusable-workflow併用時の注意
既存の親スクリプトをEnvironmentに対応させたいというとき、こんな風にワークフロー呼び出しジョブへ直接指定することはできません。(エラーになります)
name: do all
on:
workflow_dispatch:
inputs:
foo:
type: string
default: 'fooo'
+ env:
+ type: environment
+ default: 'dev'
jobs:
call-build:
uses: ./.github/workflows/build.yml
with:
foo: ${{ inputs.foo }}
call-deploy:
+ environment: ${{ inputs.env }}
uses: ./.github/workflows/deploy.yml
with:
foo: ${{ inputs.foo }}
呼び出しジョブではなく呼び出し先のジョブをenvironment対応させて引数を渡す形で対応しましょう。
name: do all
on:
workflow_dispatch:
inputs:
foo:
type: string
default: 'fooo'
+ env:
+ type: environment
+ default: 'dev'
jobs:
call-build:
uses: ./.github/workflows/build.yml
with:
foo: ${{ inputs.foo }}
call-deploy:
uses: ./.github/workflows/deploy.yml
with:
foo: ${{ inputs.foo }}
+ environment: ${{ inputs.foo }}
ジョブ側はこんな感じ
name: deploy
on:
workflow_call:
inputs:
foo:
type: string
required: true
+ environment:
+ type: environment
+ required: true
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
+ environment: ${{ inputs.environment }}
書いてみると、そりゃそうかってなりますね。
environmentとreusable-workflow併用時の注意2
呼び出し先のジョブにenvironmentを指定する場合に、複数のジョブがあると、その度に承認が必要になります。
name: deploy
on:
workflow_dispatch:
inputs:
foo:
type: string
default: 'fooo'
+ env:
+ type: environment
+ default: 'dev'
jobs:
call-pre-deploy:
uses: ./.github/workflows/pre-deploy.yml
with:
foo: ${{ inputs.foo }}
+ environment: ${{ inputs.foo }} // ここで一度承認
call-deploy:
uses: ./.github/workflows/deploy.yml
with:
foo: ${{ inputs.foo }}
+ environment: ${{ inputs.foo }} // さらにもう一度承認
さすがに毎回承認させるのもアレだったんで、environmentを使うジョブでOIDCのロール名を解決してから、それを引き渡す形で対応しました。
OIDCロールの利用可否は、Role側で制約を課している(後述)のでこれ自体が露出するのは問題なし。
name: deploy
on:
workflow_dispatch:
inputs:
foo:
type: string
default: 'fooo'
+ env:
+ type: environment
+ default: 'dev'
jobs:
+ get-aws-role:
+ name: Get AWS Role
+ runs-on: ubuntu-latest
+ environment: ${{ inputs.env }}
+ outputs:
+ role-to-assume: ${{ steps.resolve-role-to-assume.outputs.role-to-assume }}
+ steps:
+ - id: resolve-role-to-assume
+ run: |
+ AWS_ROLE_TO_ASSUME=${{ vars.AWS_ROLE_TO_ASSUME }}
+ echo "role-to-assume=$AWS_ROLE_TO_ASSUME" >> $GITHUB_OUTPUT
call-pre-deploy:
uses: ./.github/workflows/pre-deploy.yml
with:
foo: ${{ inputs.foo }}
+ role-to-assume: ${{ needs.get-aws-role.outputs.role-to-assume }}
call-deploy:
uses: ./.github/workflows/deploy.yml
with:
foo: ${{ inputs.foo }}
+ role-to-assume: ${{ needs.get-aws-role.outputs.role-to-assume }}
こんな感じ
awsへのアクセスでoidc利用したときの注意点
それなに?ってひとはこちらをどうぞ。
注意点としては、OIDCを導入したはいいものの、検証を repo:my-org/my-repo:*
等がばがばにしちゃうと、AWS_SECRET等を直接使う場合とさほど変わりません。
const githubOidcRole = new cdk.aws_iam.Role(this, "MyGithubOidcRole", {
roleName: "MyGithubOidcRole",
assumedBy: new cdk.aws_iam.FederatedPrincipal(
gitHubIdProvider.openIdConnectProviderArn,
{
StringLike: {
"token.actions.githubusercontent.com:sub": [
"repo:my-org/my-repo:*", // ガバガバです。なんでも通します。
],
},
StringEquals: {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
},
},
"sts:AssumeRoleWithWebIdentity",
),
});
この状態だと、プロテクトされていないブランチで好きに使えるようになってしまいます。
そこで、特に商用アカウントへアクセスするロールについては、プロテクトされたブランチもしくは承認が必要なEnvironmentのときのみ利用可能にしましょう。
CDK(on typescript)の例。
const githubOidcRole = new cdk.aws_iam.Role(this, "MyGithubOidcRole", {
roleName: "MyGithubOidcRole",
assumedBy: new cdk.aws_iam.FederatedPrincipal(
gitHubIdProvider.openIdConnectProviderArn,
{
StringLike: {
"token.actions.githubusercontent.com:sub": [
- "repo:my-org/my-repo:*",
+ "repo:my-org/my-repo:ref:refs/heads/main", // mainブランチ上で実行されたものはOK
+ "repo:my-org/my-repo:environment:production", // environment=productionのジョブはOK
],
},
StringEquals: {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
},
},
"sts:AssumeRoleWithWebIdentity",
),
});
ちなみに、同一job内でenvironmentが使っている場合には、repo:my-org/my-repo:environment:production
みたいなenvironmentの値が渡ってきます。
先述のように別ジョブで解決している場合にはenvironmentではなくbranchがくるので、そっちの許可もいれています。
reusable-workflowのネストは4層まで
ちょっとしたことですが、あんまり細かくジョブ分けて再利用するようにしちゃうと、後々詰みますのでお気をつけて。
https://docs.github.com/ja/actions/sharing-automations/reusing-workflows#limitations
apiからworkflow実行時に実行したrunの特定ができない
※修正済みかもしれないので、状況知りたい人はこのIssueをみてください!
octokit使って、workflowをRunして、ステータスを監視して、完了したら通知する、みたいなことがしたかったです。
簡単にできるだろうなって思ったらできませんでした。
なぜならworkflowの実行後、そのジョブを特定する方法(RunId)がないからです。
Issueもあがってました。ので、対応してくださいるのを待ちましょう。
が、待ってられない人もいるかもなので回避方法を下記に説明します。
あんまりきれいではないです。
簡単な説明:ユニークなキーをワークフローに渡して、それを使って特定する。
ステップ1: まずは既存ワークフローに特定用のキー(distinct_id)を受け付けます
name: deploy
on:
workflow_dispatch:
inputs:
env:
type: environment
+ distinct_id:
+ type: string
jobs:
+ distinct-id:
+ name: Workflow ID Provider
+ runs-on: ubuntu-latest
+ steps:
+ - name: ${{inputs.distinct_id}}
+ run: echo run identifier ${{ inputs.distinct_id }}
call-pre-deploy:
uses: ./.github/workflows/migrate.yml
with:
env: ${{ inputs.stage }}
call-deploy:
uses: ./.github/workflows/deploy.yml
with:
env: ${{ inputs.stage }}
ステップ2: ワークフローをディスパッチする側で特定キーを渡します
export const runWorkflow = async (
workflowId: string,
inputs: Record<string, string>,
branch: string,
) => {
const secret = await getGitHubPat();
const octokit: Octokit = new Octokit({ auth: secret });
+ // ユニークな文字列を生成
+ const distinctId =
+ Date.now().toString(36) + Math.random().toString(36).substring(8);
await octokit.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: workflowId,
ref: branch,
- inputs,
+ inputs: { ...inputs, ...{ distinct_id: distinctId } },
});
// この関数の中身は↓に書きます。
const runId = await getRunIdFromDistinctId(
workflowId,
distinctId,
branch,
octokit,
);
// RunIdを使って監視を実装します。
};
ステップ3: 特定キーからRunIdを解決してできあがり
// すぐにはとれないのでとれるまでぐるぐる...
const getRunIdFromDistinctId = async (
workflowId: string,
distinctId: string,
branch: string,
octokit: Octokit,
): Promise<number | null> => {
const interval = 1000;
const timeout = 1000 * 60;
let elapsed = 0;
while (elapsed < timeout) {
try {
const { runId } = await getRunId(
workflowId,
distinctId,
branch,
octokit,
);
if (runId) {
return runId;
}
}
await new Promise((resolve) => setTimeout(resolve, interval));
elapsed += interval;
}
return null;
};
const getRunId = async (
workflowId: string,
distinctId: string,
branch: string,
octokit: Octokit,
) => {
// Runしているワークフローを取得
const runsResponse = await octokit.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: workflowId,
ref: branch,
});
const runs = runsResponse.data.workflow_runs;
const runIds = runs.map((run) => run.id);
for (const runId of runIds) {
// RunIdをもとにジョブ詳細を取得して
const ret = await octokit.request(
"GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
{
owner,
repo,
run_id: runId,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
},
);
const run = ret.data;
for (const job of ret.data.jobs) {
// 発行したdistinctIdを持っているジョブが対象ののジョブ
if (job.steps?.map((s: { name: any }) => s.name).includes(distinctId)) {
const runId = job.run_id;
const status = job.status;
const conclusion = job.conclusion;
console.log(
`実行ID: ${runId}, ステータス: ${status}, 結果: ${conclusion}`,
);
return {
runId,
status,
conclusion,
};
}
}
}
console.log("Runが見つかりませんでした");
return {
runId: null,
status: "",
conclusion: "",
};
};