From 2a12de180fcb5526eed8cade1bb9e2f67f086637 Mon Sep 17 00:00:00 2001 From: ootakazuhiko Date: Sun, 19 Jul 2026 21:34:08 +0900 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20Codex=20Action=E3=82=92=E7=9B=A3?= =?UTF-8?q?=E6=9F=BB=E6=B8=88=E3=81=BFSHA=E3=81=B8=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/book-qa.yml | 5 + docs/chapters/chapter13/index.md | 35 ++- .../workflows/codex-pr-review-comment.yml | 18 +- package.json | 6 +- .../check-chapter13-codex-action-contract.js | 207 ++++++++++++++++++ src/chapters/chapter13/index.md | 35 ++- 6 files changed, 288 insertions(+), 18 deletions(-) create mode 100755 scripts/check-chapter13-codex-action-contract.js diff --git a/.github/workflows/book-qa.yml b/.github/workflows/book-qa.yml index 7a8ef5f..1567c06 100644 --- a/.github/workflows/book-qa.yml +++ b/.github/workflows/book-qa.yml @@ -55,6 +55,11 @@ jobs: node scripts/check-chapter08-dependency-review-contract.js node scripts/check-chapter08-dependency-review-contract.js --self-test + - name: Check Chapter 13 Codex Action supply-chain contract + run: | + node scripts/check-chapter13-codex-action-contract.js + node scripts/check-chapter13-codex-action-contract.js --self-test + - name: Install dependencies (book-formatter) working-directory: book-formatter run: npm ci diff --git a/docs/chapters/chapter13/index.md b/docs/chapters/chapter13/index.md index a62ffab..1c1bc43 100644 --- a/docs/chapters/chapter13/index.md +++ b/docs/chapters/chapter13/index.md @@ -770,7 +770,8 @@ jobs: - **権限を最小化**: 差分レビュー用途なら `contents: read` と、コメント投稿用の `pull-requests: write` 程度に絞る - **Secrets境界を明記**: `secrets.OPENAI_API_KEY` を `with.openai-api-key` に渡し、ログ/PR本文へ出さない - **実行範囲を制御**: ラベル付与などで実行を明示し、意図しないタイミングでの課金・外部送信を避ける -- **安全策を明示**: `sandbox`(FS/network)と `safety-strategy`(OS権限)を明示し、権限境界を固定する +- **安全策を明示**: `permission-profile: ":read-only"`(FS/network)と `safety-strategy: drop-sudo`(OS権限)を明示し、権限境界を固定する +- **供給網を固定**: actionを監査済みfull-length commit SHAへpinし、human-readable tagとCodex CLIのexact versionを併記する 以下はサンプルです(導入時は、モデル/課金/ポリシー・データ送信範囲を組織の基準に合わせて設計してください)。 @@ -794,19 +795,24 @@ jobs: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge + persist-credentials: false - name: Pre-fetch base and head refs + env: + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | git fetch --no-tags origin \ - ${{ github.event.pull_request.base.ref }} \ - +refs/pull/${{ github.event.pull_request.number }}/head + "$PR_BASE_REF" \ + "+refs/pull/$PR_NUMBER/head" - name: Run Codex (read-only) id: run_codex - uses: openai/codex-action@v1 + uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1.11 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} - sandbox: read-only + codex-version: 0.144.6 + permission-profile: ":read-only" safety-strategy: drop-sudo prompt: | Review ONLY the changes introduced by the PR. @@ -841,6 +847,23 @@ jobs: ``` {% endraw %} +#### pin更新手順 + +1. upstreamのversion tagをAPIで取得し、annotated tagならtarget commitまでdereferenceする。tag名だけでなくfull SHAを記録する。 +2. 旧SHAと候補SHAの`action.yml`、`dist/`、依存lockfile、CHANGELOG、open security issueを比較し、upstream CIと依存auditを確認する。 +3. actionのfull SHA、`# v1.11`のようなversion comment、`codex-version`のexact versionを同じPRで更新する。 +4. label gate、`contents: read`、`persist-credentials: false`、`:read-only` permission profile、`drop-sudo`が維持されることをreview/CIで確認する。 +5. test PRでラベル未付与時の非実行と、ラベル付与時のread-only reviewを確認してからmergeする。 + +この例のaction commitは`v1.11`が指す`52fe01ec70a42f454c9d2ebd47598f9fd6893d56`、Codex CLIは`0.144.6`を2026-07-19に監査したものです。同日時点のupstream source checkoutでは`pnpm audit`が10件(high 3 / moderate 5 / low 2)を指摘しましたが、`openai/codex-action`の公開Security Advisoryは確認できず、通常実行経路での到達可能性は未評価です。依存関係と新releaseを定期的に再監査してください。self-hosted/ARC、`unprivileged-user`、同一jobでの複数回実行はupstream既知issueを再確認するまで本例の対象外です。 + +監査時に参照する一次情報: + +- [GitHub Actionsのsecure use](https://docs.github.com/en/actions/reference/security/secure-use) +- [監査済みCodex Action commit](https://github.com/openai/codex-action/commit/52fe01ec70a42f454c9d2ebd47598f9fd6893d56) +- [Codex Actionのsecurity guidance](https://github.com/openai/codex-action/blob/52fe01ec70a42f454c9d2ebd47598f9fd6893d56/docs/security.md) +- [Codex Actionの公開Security Advisory](https://github.com/openai/codex-action/security/advisories) + ### 13.4.2 注意:Secrets/権限/外部送信の線引き CodexのようなエージェントをCIに組み込む場合、失敗の大半は「実装」ではなく「権限境界/Secrets運用」で発生します。 @@ -848,7 +871,7 @@ CodexのようなエージェントをCIに組み込む場合、失敗の大半 - **Secrets**: ログ/Artifacts/コメントへの混入を前提にレビュー観点へ入れる - **外部送信**: どの情報が外部へ送られるか(プロンプト、差分、ログ)を定義し、許容範囲を決める -注: `sandbox` / `safety-strategy` は補助策であり、Secretsの露出(ログ/コメント)を自動でゼロにするものではありません。Secrets運用(出さない/残さない)と監査設計(根拠の記録)を組み合わせて境界を設計してください。 +注: `permission-profile` / `safety-strategy` は補助策であり、Secretsの露出(ログ/コメント)を自動でゼロにするものではありません。Secrets運用(出さない/残さない)と監査設計(根拠の記録)を組み合わせて境界を設計してください。 詳細は第11章(Secrets/権限境界/監査)を参照してください。 diff --git a/examples/workflows/codex-pr-review-comment.yml b/examples/workflows/codex-pr-review-comment.yml index b63f24c..e3d0b8c 100644 --- a/examples/workflows/codex-pr-review-comment.yml +++ b/examples/workflows/codex-pr-review-comment.yml @@ -2,6 +2,11 @@ # - このworkflowは `openai/codex-action` を利用する third-party の例です(GitHub Copilotのネイティブ機能ではありません)。 # - `OPENAI_API_KEY`(または同等のAPIキー)をSecretsに格納し、外部送信範囲と課金を前提に導入可否を判断してください。 # - 意図しない実行を避けるため、ラベル(例: `codex-review`)で実行をゲートしています。 +# - action codeは監査済みv1.11 commit、実行時のCodex CLIもexact versionへ固定しています。 +# - pin更新時はupstream tagをcommitまでdereferenceし、action.yml/dist/lockfile/CI/auditの差分を +# レビューしてから、full SHA・version comment・codex-versionを同じPRで更新します。 +# - GitHub-hosted runnerの単発実行を前提とし、self-hosted/ARCや同一jobでの複数回実行は +# upstream既知issueを再監査するまで対象外です。 name: Codex PR Review Comment (on label) @@ -22,12 +27,16 @@ jobs: with: # Explicitly check out the PR's merge commit. ref: refs/pull/${{ github.event.pull_request.number }}/merge + persist-credentials: false - name: Pre-fetch base and head refs + env: + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | git fetch --no-tags origin \ - ${{ github.event.pull_request.base.ref }} \ - +refs/pull/${{ github.event.pull_request.number }}/head + "$PR_BASE_REF" \ + "+refs/pull/$PR_NUMBER/head" # If you want Codex to build and run code, install any dependencies that # need to be downloaded before the "Run Codex" step because Codex's @@ -35,10 +44,11 @@ jobs: - name: Run Codex (read-only) id: run_codex - uses: openai/codex-action@v1 + uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1.11 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} - sandbox: read-only + codex-version: 0.144.6 + permission-profile: ":read-only" safety-strategy: drop-sudo prompt: | This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}. diff --git a/package.json b/package.json index d66b425..1004500 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "deploy": "bash scripts/deploy.sh", "clean": "rm -rf _site public output temp", "lint:light": "markdownlint --config .markdownlint-src.json 'src/**/*.md' --ignore node_modules", - "test:light": "npm run check:security && npm run check:metadata && npm run check:chapter08-codeql && npm run check:chapter08-codeql:self-test && npm run check:chapter08-dependency-review && npm run check:chapter08-dependency-review:self-test && npm run check:issue-240-ux && npm run lint:light && npm run build", + "test:light": "npm run check:security && npm run check:metadata && npm run check:chapter08-codeql && npm run check:chapter08-codeql:self-test && npm run check:chapter08-dependency-review && npm run check:chapter08-dependency-review:self-test && npm run check:chapter13-codex-action && npm run check:chapter13-codex-action:self-test && npm run check:issue-240-ux && npm run lint:light && npm run build", "help": "echo 'Available commands:\n npm run setup - Interactive setup\n npm run build - Build the book\n npm run preview - Local preview\n npm run deploy - Deploy to GitHub Pages'", "build:generate": "node scripts/build-simple.js", "check:metadata": "node scripts/check-metadata-consistency.js", @@ -28,7 +28,9 @@ "check:chapter08-codeql:self-test": "node scripts/check-chapter08-codeql-contract.js --self-test", "check:chapter08-dependency-review": "node scripts/check-chapter08-dependency-review-contract.js", "check:chapter08-dependency-review:self-test": "node scripts/check-chapter08-dependency-review-contract.js --self-test", - "check:issue-240-ux": "node scripts/check-issue-240-ux.js" + "check:issue-240-ux": "node scripts/check-issue-240-ux.js", + "check:chapter13-codex-action": "node scripts/check-chapter13-codex-action-contract.js", + "check:chapter13-codex-action:self-test": "node scripts/check-chapter13-codex-action-contract.js --self-test" }, "dependencies": { "fs-extra": "^11.3.5", diff --git a/scripts/check-chapter13-codex-action-contract.js b/scripts/check-chapter13-codex-action-contract.js new file mode 100755 index 0000000..fd1fb6e --- /dev/null +++ b/scripts/check-chapter13-codex-action-contract.js @@ -0,0 +1,207 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('node:fs'); +const yamlParser = require('js-yaml'); + +const actionSha = '52fe01ec70a42f454c9d2ebd47598f9fd6893d56'; +const actionRef = `openai/codex-action@${actionSha}`; +const codexVersion = '0.144.6'; +const examplePath = 'examples/workflows/codex-pr-review-comment.yml'; +const sourcePath = 'src/chapters/chapter13/index.md'; +const publicPath = 'docs/chapters/chapter13/index.md'; +const sectionStart = '### 13.4.1 Codex GitHub Action'; +const sectionEnd = '### 13.4.2 注意:Secrets/権限/外部送信の線引き'; + +function normalize(value) { + return value.replace(/\r\n?/g, '\n'); +} + +function readRequired(file) { + try { + return normalize(fs.readFileSync(file, 'utf8')); + } catch (error) { + console.error(`Chapter 13 Codex Action contract failed: ${file}を読み込めません (${error.code ?? error.message})`); + process.exit(1); + } +} + +function extractSection(content, file) { + const start = content.indexOf(sectionStart); + const end = content.indexOf(sectionEnd, start + sectionStart.length); + if (start === -1 || end === -1) throw new Error(`${file}: Codex Action section is missing`); + return content.slice(start, end).trim(); +} + +function extractYaml(section) { + const match = section.match(/```yaml\n([\s\S]*?)\n```/); + return match?.[1] ?? ''; +} + +function exactPermissions(value, expected) { + return value && typeof value === 'object' && !Array.isArray(value) + && Object.keys(value).length === Object.keys(expected).length + && Object.entries(expected).every(([key, permission]) => value[key] === permission); +} + +function validateWorkflowYaml(yaml, label) { + const errors = []; + let workflow; + try { + workflow = yamlParser.load(yaml); + } catch (error) { + return [`${label}: YAMLとしてparseできません: ${error.message.split('\n')[0]}`]; + } + if (!workflow || typeof workflow !== 'object' || Array.isArray(workflow)) { + return [`${label}: YAMLのトップレベルはmappingである必要があります`]; + } + + const pullRequestTypes = workflow.on?.pull_request?.types; + if (!Array.isArray(pullRequestTypes) || pullRequestTypes.length !== 1 || pullRequestTypes[0] !== 'labeled') { + errors.push(`${label}: pull_requestはlabeledだけを購読する必要があります`); + } + + const codexJob = workflow.jobs?.codex_review; + if (!codexJob) { + errors.push(`${label}: codex_review jobがありません`); + } else { + if (codexJob.if !== "github.event.label.name == 'codex-review'") { + errors.push(`${label}: codex-review label gateが固定されていません`); + } + if (!exactPermissions(codexJob.permissions, { contents: 'read' })) { + errors.push(`${label}: codex_review permissionsはcontents: readだけへ限定する必要があります`); + } + const steps = Array.isArray(codexJob.steps) ? codexJob.steps : []; + const checkout = steps.find((step) => step?.uses === 'actions/checkout@v4'); + if (!checkout) { + errors.push(`${label}: actions/checkout@v4がありません`); + } else if (checkout.with?.['persist-credentials'] !== false) { + errors.push(`${label}: checkoutはpersist-credentials: falseが必要です`); + } + const preFetch = steps.find((step) => step?.name === 'Pre-fetch base and head refs'); + if (!preFetch) { + errors.push(`${label}: pre-fetch stepがありません`); + } else { + const expectedEnv = { + PR_BASE_REF: '${{ github.event.pull_request.base.ref }}', + PR_NUMBER: '${{ github.event.pull_request.number }}', + }; + if (!exactPermissions(preFetch.env, expectedEnv)) { + errors.push(`${label}: pre-fetchのPR値は固定したenv境界が必要です`); + } + if (typeof preFetch.run !== 'string' || preFetch.run.includes('${{')) { + errors.push(`${label}: pre-fetch runへGitHub expressionを直接展開できません`); + } + if (!preFetch.run?.includes('"$PR_BASE_REF"') || !preFetch.run?.includes('"+refs/pull/$PR_NUMBER/head"')) { + errors.push(`${label}: pre-fetchのshell変数はquoteする必要があります`); + } + } + const codexStep = steps.find((step) => step?.uses?.startsWith('openai/codex-action@')); + if (!codexStep) { + errors.push(`${label}: openai/codex-action stepがありません`); + } else { + if (codexStep.uses !== actionRef) errors.push(`${label}: Codex Actionは監査済みfull SHAへpinする必要があります`); + if (String(codexStep.with?.['codex-version']) !== codexVersion) errors.push(`${label}: codex-versionは${codexVersion}へ固定する必要があります`); + if (codexStep.with?.['permission-profile'] !== ':read-only') errors.push(`${label}: permission-profileは:read-onlyが必要です`); + if (Object.hasOwn(codexStep.with ?? {}, 'sandbox')) errors.push(`${label}: legacy sandboxとpermission-profileを併用できません`); + if (codexStep.with?.['safety-strategy'] !== 'drop-sudo') errors.push(`${label}: safety-strategyはdrop-sudoが必要です`); + if (codexStep.with?.['openai-api-key'] !== '${{ secrets.OPENAI_API_KEY }}') errors.push(`${label}: OPENAI_API_KEYのSecrets境界が変わっています`); + } + } + + const postJob = workflow.jobs?.post_comment; + if (!postJob || !exactPermissions(postJob.permissions, { issues: 'write', 'pull-requests': 'write' })) { + errors.push(`${label}: post_commentだけにissues/pull-requests writeを限定する必要があります`); + } + + if (!yaml.includes(`${actionRef} # v1.11`)) errors.push(`${label}: human-readable version comment # v1.11がありません`); + return errors; +} + +function validateGuidance(section) { + const errors = []; + for (const marker of [ + '監査済みfull-length commit SHA', + '`codex-version`のexact version', + '#### pin更新手順', + 'annotated tagならtarget commitまでdereference', + '`action.yml`、`dist/`、依存lockfile、CHANGELOG、open security issue', + '`persist-credentials: false`', + '`pnpm audit`が10件(high 3 / moderate 5 / low 2)を指摘', + '公開Security Advisoryは確認できず', + '到達可能性は未評価', + 'self-hosted/ARC', + 'https://docs.github.com/en/actions/reference/security/secure-use', + `https://github.com/openai/codex-action/commit/${actionSha}`, + `https://github.com/openai/codex-action/blob/${actionSha}/docs/security.md`, + 'https://github.com/openai/codex-action/security/advisories', + ]) { + if (!section.includes(marker)) errors.push(`Chapter 13説明に必須markerがありません: ${marker}`); + } + return errors; +} + +function expectRejected(yaml, name, mutate, expected) { + const mutated = mutate(yaml); + if (mutated === yaml) throw new Error(`self-test ${name}: mutation対象がありません`); + const errors = validateWorkflowYaml(mutated, `self-test ${name}`); + if (!errors.some((error) => error.includes(expected))) { + throw new Error(`self-test ${name}: 契約違反を拒否できません (${errors.join('; ')})`); + } +} + +const exampleYaml = readRequired(examplePath); +const sourceSection = extractSection(readRequired(sourcePath), sourcePath); +const publicSection = extractSection(readRequired(publicPath), publicPath); +const chapterYaml = extractYaml(sourceSection); +const errors = [ + ...validateWorkflowYaml(exampleYaml, examplePath), + ...validateWorkflowYaml(chapterYaml, sourcePath), + ...validateGuidance(sourceSection).map((error) => `${sourcePath}: ${error}`), +]; +if (sourceSection !== publicSection) errors.push('Codex Action section must match between src and docs'); + +const targetContents = [exampleYaml, sourceSection, publicSection]; +const mutablePattern = /openai\/codex-action@v\d+(?:\.\d+)*\b/g; +const mutableRefs = targetContents.flatMap((content, index) => [...content.matchAll(mutablePattern)].map(() => index)); +if (mutableRefs.length) errors.push(`active workflow/sampleにmutableなCodex Action tagが${mutableRefs.length}件残っています`); +const pinnedCount = targetContents.reduce((count, content) => count + content.split(actionRef).length - 1, 0); +if (pinnedCount !== 3) errors.push(`監査済みCodex Action pinは3件必要です (actual=${pinnedCount})`); + +const packageJson = JSON.parse(readRequired('package.json')); +for (const command of ['npm run check:chapter13-codex-action', 'npm run check:chapter13-codex-action:self-test']) { + if (!packageJson.scripts?.['test:light']?.includes(command)) errors.push(`test:light must run ${command}`); +} +const bookQa = readRequired('.github/workflows/book-qa.yml'); +for (const command of [ + 'node scripts/check-chapter13-codex-action-contract.js', + 'node scripts/check-chapter13-codex-action-contract.js --self-test', +]) { + if (!bookQa.includes(command)) errors.push(`Book QA must run ${command}`); +} + +if (errors.length) { + console.error('Chapter 13 Codex Action contract failed:'); + errors.forEach((error) => console.error(`- ${error}`)); + process.exit(1); +} + +if (process.argv.includes('--self-test')) { + expectRejected(chapterYaml, 'malformed YAML', (value) => value.replace('jobs:', 'jobs'), 'parseできません'); + expectRejected(chapterYaml, 'mutable action tag', (value) => value.replace(`${actionRef} # v1.11`, 'openai/codex-action@v1 # v1'), 'full SHA'); + expectRejected(chapterYaml, 'short action SHA', (value) => value.replace(`${actionRef} # v1.11`, 'openai/codex-action@52fe01e # v1.11'), 'full SHA'); + expectRejected(chapterYaml, 'missing version comment', (value) => value.replace(`${actionRef} # v1.11`, actionRef), 'version comment'); + expectRejected(chapterYaml, 'floating Codex CLI', (value) => value.replace(`codex-version: ${codexVersion}`, 'codex-version: latest'), 'codex-version'); + expectRejected(chapterYaml, 'broad permission', (value) => value.replace('contents: read', 'contents: write'), 'permissions'); + expectRejected(chapterYaml, 'missing label gate', (value) => value.replace("if: github.event.label.name == 'codex-review'", 'if: always()'), 'label gate'); + expectRejected(chapterYaml, 'persisted credentials', (value) => value.replace('persist-credentials: false', 'persist-credentials: true'), 'persist-credentials'); + expectRejected(chapterYaml, 'missing pre-fetch env boundary', (value) => value.replace('PR_BASE_REF: ${{ github.event.pull_request.base.ref }}', 'PR_BASE_REF: main'), 'env境界'); + expectRejected(chapterYaml, 'raw shell expression', (value) => value.replace('"$PR_BASE_REF"', '${{ github.event.pull_request.base.ref }}'), '直接展開'); + expectRejected(chapterYaml, 'unquoted pre-fetch ref', (value) => value.replace('"$PR_BASE_REF"', '$PR_BASE_REF'), 'quote'); + expectRejected(chapterYaml, 'workspace permission', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":workspace"'), 'permission-profile'); + expectRejected(chapterYaml, 'legacy sandbox', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":read-only"\n sandbox: read-only'), 'legacy sandbox'); + expectRejected(chapterYaml, 'unsafe strategy', (value) => value.replace('safety-strategy: drop-sudo', 'safety-strategy: unsafe'), 'drop-sudo'); + console.log('Chapter 13 Codex Action contract self-test passed.'); +} else { + console.log(`Chapter 13 Codex Action contract passed: ${actionSha} (v1.11), Codex ${codexVersion}, label gate, read-only profile, and minimum permissions.`); +} diff --git a/src/chapters/chapter13/index.md b/src/chapters/chapter13/index.md index 2b6c90d..c10cdaa 100644 --- a/src/chapters/chapter13/index.md +++ b/src/chapters/chapter13/index.md @@ -765,7 +765,8 @@ jobs: - **権限を最小化**: 差分レビュー用途なら `contents: read` と、コメント投稿用の `pull-requests: write` 程度に絞る - **Secrets境界を明記**: `secrets.OPENAI_API_KEY` を `with.openai-api-key` に渡し、ログ/PR本文へ出さない - **実行範囲を制御**: ラベル付与などで実行を明示し、意図しないタイミングでの課金・外部送信を避ける -- **安全策を明示**: `sandbox`(FS/network)と `safety-strategy`(OS権限)を明示し、権限境界を固定する +- **安全策を明示**: `permission-profile: ":read-only"`(FS/network)と `safety-strategy: drop-sudo`(OS権限)を明示し、権限境界を固定する +- **供給網を固定**: actionを監査済みfull-length commit SHAへpinし、human-readable tagとCodex CLIのexact versionを併記する 以下はサンプルです(導入時は、モデル/課金/ポリシー・データ送信範囲を組織の基準に合わせて設計してください)。 @@ -789,19 +790,24 @@ jobs: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge + persist-credentials: false - name: Pre-fetch base and head refs + env: + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | git fetch --no-tags origin \ - ${{ github.event.pull_request.base.ref }} \ - +refs/pull/${{ github.event.pull_request.number }}/head + "$PR_BASE_REF" \ + "+refs/pull/$PR_NUMBER/head" - name: Run Codex (read-only) id: run_codex - uses: openai/codex-action@v1 + uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1.11 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} - sandbox: read-only + codex-version: 0.144.6 + permission-profile: ":read-only" safety-strategy: drop-sudo prompt: | Review ONLY the changes introduced by the PR. @@ -836,6 +842,23 @@ jobs: ``` {% endraw %} +#### pin更新手順 + +1. upstreamのversion tagをAPIで取得し、annotated tagならtarget commitまでdereferenceする。tag名だけでなくfull SHAを記録する。 +2. 旧SHAと候補SHAの`action.yml`、`dist/`、依存lockfile、CHANGELOG、open security issueを比較し、upstream CIと依存auditを確認する。 +3. actionのfull SHA、`# v1.11`のようなversion comment、`codex-version`のexact versionを同じPRで更新する。 +4. label gate、`contents: read`、`persist-credentials: false`、`:read-only` permission profile、`drop-sudo`が維持されることをreview/CIで確認する。 +5. test PRでラベル未付与時の非実行と、ラベル付与時のread-only reviewを確認してからmergeする。 + +この例のaction commitは`v1.11`が指す`52fe01ec70a42f454c9d2ebd47598f9fd6893d56`、Codex CLIは`0.144.6`を2026-07-19に監査したものです。同日時点のupstream source checkoutでは`pnpm audit`が10件(high 3 / moderate 5 / low 2)を指摘しましたが、`openai/codex-action`の公開Security Advisoryは確認できず、通常実行経路での到達可能性は未評価です。依存関係と新releaseを定期的に再監査してください。self-hosted/ARC、`unprivileged-user`、同一jobでの複数回実行はupstream既知issueを再確認するまで本例の対象外です。 + +監査時に参照する一次情報: + +- [GitHub Actionsのsecure use](https://docs.github.com/en/actions/reference/security/secure-use) +- [監査済みCodex Action commit](https://github.com/openai/codex-action/commit/52fe01ec70a42f454c9d2ebd47598f9fd6893d56) +- [Codex Actionのsecurity guidance](https://github.com/openai/codex-action/blob/52fe01ec70a42f454c9d2ebd47598f9fd6893d56/docs/security.md) +- [Codex Actionの公開Security Advisory](https://github.com/openai/codex-action/security/advisories) + ### 13.4.2 注意:Secrets/権限/外部送信の線引き CodexのようなエージェントをCIに組み込む場合、失敗の大半は「実装」ではなく「権限境界/Secrets運用」で発生します。 @@ -843,7 +866,7 @@ CodexのようなエージェントをCIに組み込む場合、失敗の大半 - **Secrets**: ログ/Artifacts/コメントへの混入を前提にレビュー観点へ入れる - **外部送信**: どの情報が外部へ送られるか(プロンプト、差分、ログ)を定義し、許容範囲を決める -注: `sandbox` / `safety-strategy` は補助策であり、Secretsの露出(ログ/コメント)を自動でゼロにするものではありません。Secrets運用(出さない/残さない)と監査設計(根拠の記録)を組み合わせて境界を設計してください。 +注: `permission-profile` / `safety-strategy` は補助策であり、Secretsの露出(ログ/コメント)を自動でゼロにするものではありません。Secrets運用(出さない/残さない)と監査設計(根拠の記録)を組み合わせて境界を設計してください。 詳細は第11章(Secrets/権限境界/監査)を参照してください。 From 419ecc0aee8b2438ee68a5a90598ea06ebb6980b Mon Sep 17 00:00:00 2001 From: ootakazuhiko Date: Sun, 19 Jul 2026 21:38:39 +0900 Subject: [PATCH 2/5] =?UTF-8?q?test:=20=E5=85=A8Codex=20Action=E5=8F=82?= =?UTF-8?q?=E7=85=A7=E3=82=92=E6=A4=9C=E8=A8=BC=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../check-chapter13-codex-action-contract.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/scripts/check-chapter13-codex-action-contract.js b/scripts/check-chapter13-codex-action-contract.js index fd1fb6e..92791de 100755 --- a/scripts/check-chapter13-codex-action-contract.js +++ b/scripts/check-chapter13-codex-action-contract.js @@ -96,16 +96,20 @@ function validateWorkflowYaml(yaml, label) { errors.push(`${label}: pre-fetchのshell変数はquoteする必要があります`); } } - const codexStep = steps.find((step) => step?.uses?.startsWith('openai/codex-action@')); - if (!codexStep) { + const codexSteps = steps.filter((step) => step?.uses?.startsWith('openai/codex-action@')); + if (!codexSteps.length) { errors.push(`${label}: openai/codex-action stepがありません`); } else { - if (codexStep.uses !== actionRef) errors.push(`${label}: Codex Actionは監査済みfull SHAへpinする必要があります`); - if (String(codexStep.with?.['codex-version']) !== codexVersion) errors.push(`${label}: codex-versionは${codexVersion}へ固定する必要があります`); - if (codexStep.with?.['permission-profile'] !== ':read-only') errors.push(`${label}: permission-profileは:read-onlyが必要です`); - if (Object.hasOwn(codexStep.with ?? {}, 'sandbox')) errors.push(`${label}: legacy sandboxとpermission-profileを併用できません`); - if (codexStep.with?.['safety-strategy'] !== 'drop-sudo') errors.push(`${label}: safety-strategyはdrop-sudoが必要です`); - if (codexStep.with?.['openai-api-key'] !== '${{ secrets.OPENAI_API_KEY }}') errors.push(`${label}: OPENAI_API_KEYのSecrets境界が変わっています`); + if (codexSteps.length !== 1) errors.push(`${label}: Codex Actionは既知制約により1 job 1 stepへ限定する必要があります`); + codexSteps.forEach((codexStep, index) => { + const stepLabel = `${label}: Codex Action step ${index + 1}`; + if (codexStep.uses !== actionRef) errors.push(`${stepLabel}は監査済みfull SHAへpinする必要があります`); + if (String(codexStep.with?.['codex-version']) !== codexVersion) errors.push(`${stepLabel}のcodex-versionは${codexVersion}へ固定する必要があります`); + if (codexStep.with?.['permission-profile'] !== ':read-only') errors.push(`${stepLabel}のpermission-profileは:read-onlyが必要です`); + if (Object.hasOwn(codexStep.with ?? {}, 'sandbox')) errors.push(`${stepLabel}はlegacy sandboxとpermission-profileを併用できません`); + if (codexStep.with?.['safety-strategy'] !== 'drop-sudo') errors.push(`${stepLabel}のsafety-strategyはdrop-sudoが必要です`); + if (codexStep.with?.['openai-api-key'] !== '${{ secrets.OPENAI_API_KEY }}') errors.push(`${stepLabel}のOPENAI_API_KEYのSecrets境界が変わっています`); + }); } } @@ -162,7 +166,7 @@ const errors = [ if (sourceSection !== publicSection) errors.push('Codex Action section must match between src and docs'); const targetContents = [exampleYaml, sourceSection, publicSection]; -const mutablePattern = /openai\/codex-action@v\d+(?:\.\d+)*\b/g; +const mutablePattern = /openai\/codex-action@(?![0-9a-f]{40}(?:\s|$))[^\s#]+/g; const mutableRefs = targetContents.flatMap((content, index) => [...content.matchAll(mutablePattern)].map(() => index)); if (mutableRefs.length) errors.push(`active workflow/sampleにmutableなCodex Action tagが${mutableRefs.length}件残っています`); const pinnedCount = targetContents.reduce((count, content) => count + content.split(actionRef).length - 1, 0); @@ -190,6 +194,7 @@ if (process.argv.includes('--self-test')) { expectRejected(chapterYaml, 'malformed YAML', (value) => value.replace('jobs:', 'jobs'), 'parseできません'); expectRejected(chapterYaml, 'mutable action tag', (value) => value.replace(`${actionRef} # v1.11`, 'openai/codex-action@v1 # v1'), 'full SHA'); expectRejected(chapterYaml, 'short action SHA', (value) => value.replace(`${actionRef} # v1.11`, 'openai/codex-action@52fe01e # v1.11'), 'full SHA'); + expectRejected(chapterYaml, 'additional mutable action step', (value) => value.replace(' - name: Run Codex (read-only)', ' - uses: openai/codex-action@main\n\n - name: Run Codex (read-only)'), 'full SHA'); expectRejected(chapterYaml, 'missing version comment', (value) => value.replace(`${actionRef} # v1.11`, actionRef), 'version comment'); expectRejected(chapterYaml, 'floating Codex CLI', (value) => value.replace(`codex-version: ${codexVersion}`, 'codex-version: latest'), 'codex-version'); expectRejected(chapterYaml, 'broad permission', (value) => value.replace('contents: read', 'contents: write'), 'permissions'); From 64c69f8d938ef6a9c123bed5333229ed15abc3b9 Mon Sep 17 00:00:00 2001 From: ootakazuhiko Date: Sun, 19 Jul 2026 21:40:55 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20private=20checkout=E3=81=A7=E3=82=82?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E5=8F=96=E5=BE=97=E3=82=92=E5=AE=8C=E7=B5=90?= =?UTF-8?q?=E3=81=95=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/chapters/chapter13/index.md | 11 ++------ .../workflows/codex-pr-review-comment.yml | 11 ++------ .../check-chapter13-codex-action-contract.js | 26 +++---------------- src/chapters/chapter13/index.md | 11 ++------ 4 files changed, 10 insertions(+), 49 deletions(-) diff --git a/docs/chapters/chapter13/index.md b/docs/chapters/chapter13/index.md index 1c1bc43..85e8836 100644 --- a/docs/chapters/chapter13/index.md +++ b/docs/chapters/chapter13/index.md @@ -795,17 +795,10 @@ jobs: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge + # checkoutの認証中に履歴を取得し、その後credentialを除去する + fetch-depth: 0 persist-credentials: false - - name: Pre-fetch base and head refs - env: - PR_BASE_REF: ${{ github.event.pull_request.base.ref }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - git fetch --no-tags origin \ - "$PR_BASE_REF" \ - "+refs/pull/$PR_NUMBER/head" - - name: Run Codex (read-only) id: run_codex uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1.11 diff --git a/examples/workflows/codex-pr-review-comment.yml b/examples/workflows/codex-pr-review-comment.yml index e3d0b8c..83d563f 100644 --- a/examples/workflows/codex-pr-review-comment.yml +++ b/examples/workflows/codex-pr-review-comment.yml @@ -27,17 +27,10 @@ jobs: with: # Explicitly check out the PR's merge commit. ref: refs/pull/${{ github.event.pull_request.number }}/merge + # Fetch history while checkout can authenticate, then remove the credential. + fetch-depth: 0 persist-credentials: false - - name: Pre-fetch base and head refs - env: - PR_BASE_REF: ${{ github.event.pull_request.base.ref }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - git fetch --no-tags origin \ - "$PR_BASE_REF" \ - "+refs/pull/$PR_NUMBER/head" - # If you want Codex to build and run code, install any dependencies that # need to be downloaded before the "Run Codex" step because Codex's # default sandbox disables network access. diff --git a/scripts/check-chapter13-codex-action-contract.js b/scripts/check-chapter13-codex-action-contract.js index 92791de..a1d1fa6 100755 --- a/scripts/check-chapter13-codex-action-contract.js +++ b/scripts/check-chapter13-codex-action-contract.js @@ -75,26 +75,10 @@ function validateWorkflowYaml(yaml, label) { const checkout = steps.find((step) => step?.uses === 'actions/checkout@v4'); if (!checkout) { errors.push(`${label}: actions/checkout@v4がありません`); - } else if (checkout.with?.['persist-credentials'] !== false) { - errors.push(`${label}: checkoutはpersist-credentials: falseが必要です`); - } - const preFetch = steps.find((step) => step?.name === 'Pre-fetch base and head refs'); - if (!preFetch) { - errors.push(`${label}: pre-fetch stepがありません`); } else { - const expectedEnv = { - PR_BASE_REF: '${{ github.event.pull_request.base.ref }}', - PR_NUMBER: '${{ github.event.pull_request.number }}', - }; - if (!exactPermissions(preFetch.env, expectedEnv)) { - errors.push(`${label}: pre-fetchのPR値は固定したenv境界が必要です`); - } - if (typeof preFetch.run !== 'string' || preFetch.run.includes('${{')) { - errors.push(`${label}: pre-fetch runへGitHub expressionを直接展開できません`); - } - if (!preFetch.run?.includes('"$PR_BASE_REF"') || !preFetch.run?.includes('"+refs/pull/$PR_NUMBER/head"')) { - errors.push(`${label}: pre-fetchのshell変数はquoteする必要があります`); - } + if (checkout.with?.ref !== 'refs/pull/${{ github.event.pull_request.number }}/merge') errors.push(`${label}: checkoutはPR merge refへ固定する必要があります`); + if (checkout.with?.['fetch-depth'] !== 0) errors.push(`${label}: private repositoryでもcheckout認証中に履歴を取得するためfetch-depth: 0が必要です`); + if (checkout.with?.['persist-credentials'] !== false) errors.push(`${label}: checkoutはpersist-credentials: falseが必要です`); } const codexSteps = steps.filter((step) => step?.uses?.startsWith('openai/codex-action@')); if (!codexSteps.length) { @@ -200,9 +184,7 @@ if (process.argv.includes('--self-test')) { expectRejected(chapterYaml, 'broad permission', (value) => value.replace('contents: read', 'contents: write'), 'permissions'); expectRejected(chapterYaml, 'missing label gate', (value) => value.replace("if: github.event.label.name == 'codex-review'", 'if: always()'), 'label gate'); expectRejected(chapterYaml, 'persisted credentials', (value) => value.replace('persist-credentials: false', 'persist-credentials: true'), 'persist-credentials'); - expectRejected(chapterYaml, 'missing pre-fetch env boundary', (value) => value.replace('PR_BASE_REF: ${{ github.event.pull_request.base.ref }}', 'PR_BASE_REF: main'), 'env境界'); - expectRejected(chapterYaml, 'raw shell expression', (value) => value.replace('"$PR_BASE_REF"', '${{ github.event.pull_request.base.ref }}'), '直接展開'); - expectRejected(chapterYaml, 'unquoted pre-fetch ref', (value) => value.replace('"$PR_BASE_REF"', '$PR_BASE_REF'), 'quote'); + expectRejected(chapterYaml, 'shallow checkout', (value) => value.replace('fetch-depth: 0', 'fetch-depth: 1'), 'fetch-depth'); expectRejected(chapterYaml, 'workspace permission', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":workspace"'), 'permission-profile'); expectRejected(chapterYaml, 'legacy sandbox', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":read-only"\n sandbox: read-only'), 'legacy sandbox'); expectRejected(chapterYaml, 'unsafe strategy', (value) => value.replace('safety-strategy: drop-sudo', 'safety-strategy: unsafe'), 'drop-sudo'); diff --git a/src/chapters/chapter13/index.md b/src/chapters/chapter13/index.md index c10cdaa..e3fa563 100644 --- a/src/chapters/chapter13/index.md +++ b/src/chapters/chapter13/index.md @@ -790,17 +790,10 @@ jobs: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge + # checkoutの認証中に履歴を取得し、その後credentialを除去する + fetch-depth: 0 persist-credentials: false - - name: Pre-fetch base and head refs - env: - PR_BASE_REF: ${{ github.event.pull_request.base.ref }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - git fetch --no-tags origin \ - "$PR_BASE_REF" \ - "+refs/pull/$PR_NUMBER/head" - - name: Run Codex (read-only) id: run_codex uses: openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56 # v1.11 From 0a53686db3e375f8488148f24a5c09c6e46db1fd Mon Sep 17 00:00:00 2001 From: ootakazuhiko Date: Sun, 19 Jul 2026 21:46:36 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test:=20Codex=20Action=E5=8F=82=E7=85=A7?= =?UTF-8?q?=E3=82=92=E5=85=A8=E5=AF=BE=E8=B1=A1=E3=81=A7=E7=9B=A3=E6=9F=BB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../check-chapter13-codex-action-contract.js | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/scripts/check-chapter13-codex-action-contract.js b/scripts/check-chapter13-codex-action-contract.js index a1d1fa6..88f47b2 100755 --- a/scripts/check-chapter13-codex-action-contract.js +++ b/scripts/check-chapter13-codex-action-contract.js @@ -26,6 +26,39 @@ function readRequired(file) { } } +function collectReferenceFiles() { + const files = []; + const allowedExtensions = new Set(['.md', '.yml', '.yaml']); + const excludedDirectories = new Set(['.git', '_site', 'node_modules']); + + function walk(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!excludedDirectories.has(entry.name)) walk(`${directory}/${entry.name}`); + } else if (entry.isFile() && allowedExtensions.has(entry.name.slice(entry.name.lastIndexOf('.')))) { + const path = `${directory}/${entry.name}`; + files.push({ path, content: readRequired(path) }); + } + } + } + + for (const root of ['.github/workflows', 'examples', 'src', 'docs']) walk(root); + return files.sort((left, right) => left.path.localeCompare(right.path)); +} + +function validateRepositoryActionReferences(files) { + const errors = []; + const usesPattern = /^\s*(?:-\s*)?uses:\s*openai\/codex-action@([^\s#]+)/gm; + for (const file of files) { + for (const match of file.content.matchAll(usesPattern)) { + if (match[1] !== actionSha) { + errors.push(`${file.path}: Codex Action ref ${match[1]} は監査済みfull SHAではありません`); + } + } + } + return errors; +} + function extractSection(content, file) { const start = content.indexOf(sectionStart); const end = content.indexOf(sectionEnd, start + sectionStart.length); @@ -150,11 +183,10 @@ const errors = [ if (sourceSection !== publicSection) errors.push('Codex Action section must match between src and docs'); const targetContents = [exampleYaml, sourceSection, publicSection]; -const mutablePattern = /openai\/codex-action@(?![0-9a-f]{40}(?:\s|$))[^\s#]+/g; -const mutableRefs = targetContents.flatMap((content, index) => [...content.matchAll(mutablePattern)].map(() => index)); -if (mutableRefs.length) errors.push(`active workflow/sampleにmutableなCodex Action tagが${mutableRefs.length}件残っています`); const pinnedCount = targetContents.reduce((count, content) => count + content.split(actionRef).length - 1, 0); if (pinnedCount !== 3) errors.push(`監査済みCodex Action pinは3件必要です (actual=${pinnedCount})`); +const referenceFiles = collectReferenceFiles(); +errors.push(...validateRepositoryActionReferences(referenceFiles)); const packageJson = JSON.parse(readRequired('package.json')); for (const command of ['npm run check:chapter13-codex-action', 'npm run check:chapter13-codex-action:self-test']) { @@ -188,6 +220,18 @@ if (process.argv.includes('--self-test')) { expectRejected(chapterYaml, 'workspace permission', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":workspace"'), 'permission-profile'); expectRejected(chapterYaml, 'legacy sandbox', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":read-only"\n sandbox: read-only'), 'legacy sandbox'); expectRejected(chapterYaml, 'unsafe strategy', (value) => value.replace('safety-strategy: drop-sudo', 'safety-strategy: unsafe'), 'drop-sudo'); + for (const [name, ref] of [ + ['repository-wide mutable ref', 'main'], + ['repository-wide unapproved SHA', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'], + ]) { + const scanErrors = validateRepositoryActionReferences([ + ...referenceFiles, + { path: `examples/workflows/self-test-${name}.yml`, content: `steps:\n - uses: openai/codex-action@${ref}\n` }, + ]); + if (!scanErrors.some((error) => error.includes('監査済みfull SHAではありません'))) { + throw new Error(`self-test ${name}: repository-wide ref違反を拒否できません`); + } + } console.log('Chapter 13 Codex Action contract self-test passed.'); } else { console.log(`Chapter 13 Codex Action contract passed: ${actionSha} (v1.11), Codex ${codexVersion}, label gate, read-only profile, and minimum permissions.`); From e78e0bcd28139b6db5dc3e3dfb9ed43615221dca Mon Sep 17 00:00:00 2001 From: ootakazuhiko Date: Sun, 19 Jul 2026 21:51:26 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=E5=BC=95=E7=94=A8=E7=AC=A6?= =?UTF-8?q?=E4=BB=98=E3=81=8DAction=E5=8F=82=E7=85=A7=E3=82=92=E6=A4=9C?= =?UTF-8?q?=E5=87=BA=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/check-chapter13-codex-action-contract.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/check-chapter13-codex-action-contract.js b/scripts/check-chapter13-codex-action-contract.js index 88f47b2..4496b59 100755 --- a/scripts/check-chapter13-codex-action-contract.js +++ b/scripts/check-chapter13-codex-action-contract.js @@ -48,7 +48,7 @@ function collectReferenceFiles() { function validateRepositoryActionReferences(files) { const errors = []; - const usesPattern = /^\s*(?:-\s*)?uses:\s*openai\/codex-action@([^\s#]+)/gm; + const usesPattern = /^\s*(?:-\s*)?uses\s*:\s*["']?openai\/codex-action@([^\s#"']+)/gm; for (const file of files) { for (const match of file.content.matchAll(usesPattern)) { if (match[1] !== actionSha) { @@ -220,13 +220,15 @@ if (process.argv.includes('--self-test')) { expectRejected(chapterYaml, 'workspace permission', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":workspace"'), 'permission-profile'); expectRejected(chapterYaml, 'legacy sandbox', (value) => value.replace('permission-profile: ":read-only"', 'permission-profile: ":read-only"\n sandbox: read-only'), 'legacy sandbox'); expectRejected(chapterYaml, 'unsafe strategy', (value) => value.replace('safety-strategy: drop-sudo', 'safety-strategy: unsafe'), 'drop-sudo'); - for (const [name, ref] of [ - ['repository-wide mutable ref', 'main'], - ['repository-wide unapproved SHA', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'], + for (const [name, uses] of [ + ['repository-wide mutable ref', 'uses: openai/codex-action@main'], + ['repository-wide double-quoted mutable ref', 'uses: "openai/codex-action@main"'], + ["repository-wide single-quoted mutable ref", "uses: 'openai/codex-action@main'"], + ['repository-wide unapproved SHA', 'uses: openai/codex-action@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'], ]) { const scanErrors = validateRepositoryActionReferences([ ...referenceFiles, - { path: `examples/workflows/self-test-${name}.yml`, content: `steps:\n - uses: openai/codex-action@${ref}\n` }, + { path: `examples/workflows/self-test-${name}.yml`, content: `steps:\n - ${uses}\n` }, ]); if (!scanErrors.some((error) => error.includes('監査済みfull SHAではありません'))) { throw new Error(`self-test ${name}: repository-wide ref違反を拒否できません`);