GitHub Actions ワークフロー内で利用しているアクションの依存関係を再帰的に取得して出力するコマンドラインツール「ghatree」をつくりました。
こういうの
この記事では ghatree の基本的な使い方と活用例を紹介します。
npm パッケージとして配布しているため、 npx
コマンド等で実行できます。
$ npx ghatree@latest
$ bun x ghatree@latest
デフォルトでは ghatree はカレントディレクトリから .github/workflows
配下のワークフローを探索します。
特定のリポジトリを指定したい場合は --repo
フラグを利用します。
$ npx ghatree@latest --repo koki-develop/ghatree
GitHub Token の設定
ghatree
は内部的に GitHub API を利用してワークフローの情報を取得します。
その際に利用するトークンは GITHUB_TOKEN
環境変数で設定できます。
$ GITHUB_TOKEN=your-token> npx ghatree@latest
プライベートなリポジトリを解析する場合や API のレートリミットを回避するために使えます。
JSON 形式で出力する
--json
フラグを利用すると、JSON 形式で出力できます。
$ npx ghatree@latest --json
出力例
{
"type": "repository",
"dependencies": [
{
"type": "workflow",
"path": ".github/workflows/example1.yml",
"dependencies": [
{
"type": "job",
"path": "job1",
"dependencies": [
{
"type": "action",
"repository": {
"owner": "actions",
"name": "checkout"
},
"ref": "v5",
"dependencies": []
},
{
"type": "action",
"repository": {
"owner": "actions",
"name": "setup-node"
},
"ref": "v4",
"dependencies": []
}
]
},
{
"type": "job",
"path": "job2",
"dependencies": [
{
"type": "action",
"repository": {
"owner": "actions",
"name": "checkout"
},
"ref": "v5",
"dependencies": []
},
{
"type": "action",
"repository": {
"owner": "cli",
"name": "gh-extension-precompile"
},
"dependencies": [
{
"type": "action",
"repository": {
"owner": "actions",
"name": "setup-go"
},
"ref": "v5",
"dependencies": []
},
{
"type": "action",
"repository": {
"owner": "actions",
"name": "attest-build-provenance"
},
"ref": "v1",
"dependencies": [
{
"type": "action",
"repository": {
"owner": "actions",
"name": "attest-build-provenance"
},
"path": "predicate",
"ref": "36fa7d009e22618ca7cd599486979b8150596c74",
"dependencies": []
},
{
"type": "action",
"repository": {
"owner": "actions",
"name": "attest"
},
"ref": "67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d",
"dependencies": []
}
]
}
]
}
]
}
]
},
{
"type": "workflow",
"path": ".github/workflows/example2.yml",
"dependencies": [
{
"type": "job",
"path": "job1",
"dependencies": [
{
"type": "action",
"repository": {
"owner": "actions",
"name": "checkout"
},
"ref": "v5",
"dependencies": []
},
{
"type": "action",
"repository": {
"owner": "actions",
"name": "setup-go"
},
"ref": "v5",
"dependencies": []
}
]
},
{
"type": "job",
"path": "job2",
"dependencies": [
{
"type": "action",
"repository": {
"owner": "actions",
"name": "checkout"
},
"ref": "v5",
"dependencies": []
},
{
"type": "action",
"repository": {
"owner": "anthropics",
"name": "claude-code-action"
},
"ref": "beta",
"dependencies": [
{
"type": "action",
"repository": {
"owner": "oven-sh",
"name": "setup-bun"
},
"ref": "735343b667d3e6f658f44d0eca948eb6282f2b76",
"dependencies": []
}
]
}
]
}
]
}
]
}
ここでは、 ghatree を使用して GitHub Actions ワークフロー内でコミット SHA 固定していないアクションの利用を再帰的に探索し、特定する方法を紹介します。
GitHub Actions でのアクション参照にはコミット SHA 固定が推奨されている
GitHub Actions で uses:
キーワードを使用してアクションを参照する際は、 Git タグではなくコミット SHA を指定することがセキュリティの上で推奨されています。
- uses: actions/checkout@v5
- uses: actions/[email protected]
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
リリースされたアクションバージョンのコミットSHAを使用するのが、安定性とセキュリティのうえで最も安全です。
実際、 tj-actions/changed-files
や reviewdog/action-*
などのアクションの Git タグが書き換えられ、これらのアクションを Git タグで参照している GitHub Actions Workflow 内で悪意のあるコードが実行されてしまうという出来事もありました。
Enforce SHA Pinning について
先日、 GitHub にて Enforce SHA Pinning という機能が追加され、アクションの参照にコミット SHA を使用することを強制できるようになりました。
Enforce SHA Pinning を有効にすると、コミット SHA 固定されていないアクションを使用しているワークフローの実行が失敗するようになります。
例えば以下のようなワークフローを実行するとエラーになります。
on: push
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Enforce SHA Pinning についての詳細は以下の記事がわかりやすいので、こちらをご参照ください。
Enforce SHA Pinning の重要な点の 1 つとして、ワークフローが直接使用していないアクションもチェック対象に含まれるというものがあります。
例えば以下のようなワークフローがあったとします。
on: workflow_dispatch
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: cli/gh-extension-precompile@9e2237c30f869ad3bcaed6a4be2cd43564dd421b
一見すると全てのアクションがコミット SHA 固定されていて問題ないように見えますが、 Enforce SHA Pinning を有効にしたリポジトリでこのワークフローを実行するとエラーになります。
The actions actions/setup-go@v5 and actions/attest-build-provenance@v1 are not allowed in koki-develop/enforce-sha-pinning-sandbox because all actions must be pinned to a full-length commit SHA.
actions/setup-go@v5
や actions/attest-build-provenance@v1
などは使用していないのに、なぜエラーになるのでしょうか?
実は上記のワークフローで使用している cli/gh-extension-precompile
アクションが内部的に actions/setup-go
や actions/attest-build-provenance
といったアクションに依存しており、これらはコミット SHA 固定されていません。
このように Enforce SHA Pinning では、ワークフローが直接使用しているアクションだけではなく、あるアクションが内部的に依存しているアクションもチェック対象に含まれます。
上記の例では比較的簡単に依存関係を辿ってコミット SHA 固定されていないアクションを特定できましたが、より大きく複雑なワークフローの場合にまた同じような調査を手動で行うのは結構大変です。
ghatree を使用して、 SHA 固定していないアクションを特定する
ghatree では、ワークフロー内で使用しているアクションの依存関係を再帰的に取得できるため、 SHA 固定していないアクションの特定に活用できます。
以下は ghatree の JSON 出力を再帰的に探索し、 SHA 固定していないアクションをログ出力する Bun Shell スクリプトの例です。
import { $ } from "bun";
type Node = {
type: string;
dependencies: Node[];
[key: string]: any;
};
const tree = await $`bunx ghatree@latest --json`.json();
traverse(tree, []);
function traverse(node: Node, stack: Node[]) {
stack.push(node);
if (node.type === "action" && !node.path?.startsWith(".")) {
if (!node.ref || !isFullSHA(node.ref)) {
console.warn(
stack
.slice(1)
.map((node) => {
if (node.type !== "action") {
return node.path;
}
return `${node.repository.owner}/${node.repository.name}${node.path ? `/${node.path}` : ""}${node.ref ? `@${node.ref}` : ""}`;
})
.join(" > "),
);
}
}
for (const dependency of node.dependencies) {
traverse(dependency, stack);
}
stack.pop();
}
function isFullSHA(version: string): boolean {
return /^[a-f0-9]{40}$/i.test(version);
}
出力例
.github/workflows/example.yml > main > cli/gh-extension-precompile@9e2237c30f869ad3bcaed6a4be2cd43564dd421b > actions/setup-go@v5
.github/workflows/example.yml > main > cli/gh-extension-precompile@9e2237c30f869ad3bcaed6a4be2cd43564dd421b > actions/attest-build-provenance@v1
cli/gh-extension-precompile
アクションが依存している actions/setup-go@v5
や actions/attest-build-provenance@v1
といったアクションがコミット SHA 固定されていないことが確認できます。
便利ですね。
Views: 0