diff --git a/.github/workflows/d1-migrate.yml b/.github/workflows/d1-migrate.yml index eb7709a93..742ba2526 100644 --- a/.github/workflows/d1-migrate.yml +++ b/.github/workflows/d1-migrate.yml @@ -43,8 +43,16 @@ jobs: cache: "npm" cache-dependency-path: smkc-score-app/package-lock.json + - name: Pin npm + run: | + npm install --global --ignore-scripts --no-audit --no-fund npm@10.9.4 + test "$(npm --version)" = "10.9.4" + - name: Install dependencies run: npm ci + env: + # Production migration validation never needs local Git hooks; keep install parity with CI. + HUSKY: "0" - name: List pending migrations run: npm run db:migrations:list diff --git a/docs/ci-npm-toolchain.md b/docs/ci-npm-toolchain.md index b298a1f0c..92800dbf4 100644 --- a/docs/ci-npm-toolchain.md +++ b/docs/ci-npm-toolchain.md @@ -1,10 +1,10 @@ # CI の npm バージョン管理 -JSMKC の `lint-and-test` CI と nightly E2E は npm を **10.9.4** に固定する。 +JSMKC の `lint-and-test` CI、nightly E2E、production D1 migration workflow は npm を **10.9.4** に固定する。 `smkc-score-app/scripts/security-audit.js` は `npm audit --json` の report version、field、severity summary、dependency summary、exit status を fail-closed で検証している。Node.js 22 に同梱される npm をそのまま利用すると、GitHub Actions の runner / Node 配布物更新だけで npm の patch / major が変わり、アプリケーションの変更と無関係に audit の意味論や JSON 形状がドリフトする可能性がある。そのため CI では依存インストールより前に `npm@10.9.4` を明示的に導入し、実際の `npm --version` も確認する。 -通常 CI、Claude Code Review の validation job、nightly E2E は npm 自体の bootstrap を `--ignore-scripts --no-audit --no-fund` 付きで実行する。bootstrap 対象は固定済みの npm CLI そのものなので lifecycle script を実行する必要はなく、この段階で application dependency audit や funding lookup を暗黙に発生させない。実際の application dependency install は後続の `npm ci` に限定し、通常 CI と nightly E2E の `npm ci` では local Git hook を必要としないため `HUSKY=0` で prepare-time の hook setup/noise も抑止する。 +通常 CI、Claude Code Review の validation job、nightly E2E、D1 migration workflow は npm 自体の bootstrap を `--ignore-scripts --no-audit --no-fund` 付きで実行する。bootstrap 対象は固定済みの npm CLI そのものなので lifecycle script を実行する必要はなく、この段階で application dependency audit や funding lookup を暗黙に発生させない。実際の application dependency install は後続の `npm ci` に限定し、通常 CI、nightly E2E、D1 migration workflow の `npm ci` では local Git hook を必要としないため `HUSKY=0` で prepare-time の hook setup/noise も抑止する。 さらに `node scripts/security-audit.js` 自身が audit subprocess を起動する直前に npm runtime verifier を呼び、`package.json` の `packageManager` が exact `npm@x.y.z` 形式であることと、実行時の `npm --version` がその固定値と一致することを再検証する。CI の shell chain に verifier を別コマンドとして置かないため、audit helper をローカル・別CIから直接実行しても runtime guard を迂回できない。 @@ -12,12 +12,12 @@ JSMKC の `lint-and-test` CI と nightly E2E は npm を **10.9.4** に固定す upstream の修正確認や再レビュー期限前の再評価を、アプリ変更用 PR を作らずに実行したい場合は GitHub Actions の **Security audit review** workflow を手動起動する。この workflow は `workflow_dispatch` のみで定期実行は行わず、read-only の repository permission で checkout した後、CI と同じ Node.js 22 / npm 10.9.4 を使用する。ただし application dependency tree はインストールせず、npm 自体の bootstrap も `--ignore-scripts --no-audit --no-fund` 付きで行う。その後、lockfile preflight → temporary exception status → network-backed canonical audit → compatible upstream probe の順に証拠を収集する。review 用 workflow では lockfile preflight が成功している限り、temporary exception status が `context-changed` / `expired` で fail-closed になっても canonical audit を続けて実行する。これにより forward update 候補が現れた回で「例外文脈は変わったが audit は clean か」を同じ run から確認できる一方、status step の失敗は保持されるため workflow 全体は green にならない。各 check の outcome は run summary に残す。結果が green でも #3114 の例外条件を自動変更・延長・削除はしないため、upstream の状態と audit 結果を確認してから明示的に判断する。 -固定値の正本は `smkc-score-app/package.json` の `packageManager` と `.github/workflows/ci.yml` の Pin npm step で、`smkc-score-app/__tests__/docs/ci-config.test.ts` が両者の一致、side-effect-free bootstrap、`npm ci` より前の pin、lockfile preflight → temporary exception status → Security audit entrypoint の順序を回帰テストする。`smkc-score-app/__tests__/docs/claude-code-review-workflow.test.ts` と `smkc-score-app/__tests__/docs/e2e-nightly-npm-toolchain.test.ts` も同じ bootstrap flags と npm pin を固定し、後者は `HUSKY=0` で hook setup を抑止することも検証する。`smkc-score-app/__tests__/docs/security-audit-review-workflow.test.ts` は手動 review workflow が定期起動されないこと、read-only permission、同じ npm pin を lifecycle script / implicit audit / fund 通信なしで bootstrap すること、application dependency tree をインストールしないこと、audit 順序、status が変化しても preflight 成功時は canonical audit の証拠を取得すること、各 outcome を read-only summary に残すことを検証する。`smkc-score-app/__tests__/scripts/verify-npm-version.test.ts` は exact pin の解釈・runtime process failure・version mismatch の fail-closed 条件を検証し、`security-audit-runtime-guard.test.ts` は audit helper が `npm audit` より前に verifier を必ず呼ぶことを固定する。 +固定値の正本は `smkc-score-app/package.json` の `packageManager` と `.github/workflows/ci.yml` の Pin npm step で、`smkc-score-app/__tests__/docs/ci-config.test.ts` が両者の一致、side-effect-free bootstrap、`npm ci` より前の pin、lockfile preflight → temporary exception status → Security audit entrypoint の順序を回帰テストする。`smkc-score-app/__tests__/docs/claude-code-review-workflow.test.ts` と `smkc-score-app/__tests__/docs/e2e-nightly-npm-toolchain.test.ts` も同じ bootstrap flags と npm pin を固定し、後者は `HUSKY=0` で hook setup を抑止することも検証する。`smkc-score-app/__tests__/docs/d1-migrate-npm-toolchain.test.ts` は production D1 migration workflow が同じ npm pin を `npm ci` より前に適用し、`HUSKY=0` を設定し、pending migration の確認 → migration apply という既存順序を変えないことを固定する。`smkc-score-app/__tests__/docs/security-audit-review-workflow.test.ts` は手動 review workflow が定期起動されないこと、read-only permission、同じ npm pin を lifecycle script / implicit audit / fund 通信なしで bootstrap すること、application dependency tree をインストールしないこと、audit 順序、status が変化しても preflight 成功時は canonical audit の証拠を取得すること、各 outcome を read-only summary に残すことを検証する。`smkc-score-app/__tests__/scripts/verify-npm-version.test.ts` は exact pin の解釈・runtime process failure・version mismatch の fail-closed 条件を検証し、`security-audit-runtime-guard.test.ts` は audit helper が `npm audit` より前に verifier を必ず呼ぶことを固定する。 -`d1-migrate.yml` も `npm ci` を実行するが、この workflow 自体の変更は `main` merge 後に production D1 migration job を起動する。そのため npm pin の適用は production 側の pending migration / 実行証跡を確認できる作業回で別途行い、単なる CI 保守変更として自動マージしない。 +`d1-migrate.yml` は自身の変更が `main` に入ると production environment の D1 migration job を起動する。したがってこの workflow の PR は通常の CI が green であっても自動マージせず、merge 前に production D1 の pending migration 状態と実行予定を確認する。merge 後は意図した `D1 Migrate` run が1回だけ起動し、`List pending migrations`、`Apply migrations`、`Confirm no migrations remain pending` が成功したことを実行証跡として残す。 ## 更新手順 npm を更新するときは、単に version を上げず、候補版で `npm ci` と `node scripts/security-audit-lockfile.js && node scripts/security-audit-status.js && node scripts/security-audit.js` を実行し、runtime version guard と audit report shape / exit status の検証が引き続き成立することを確認する。必要なら security-audit helper と policy を同じ PR で更新する。 -確認後、`package.json` の `packageManager` と CI / nightly E2E の Pin npm step を同じ version に更新し、lint、format、unit tests、security audit、Cloudflare build を通す。npm の変更だけを理由に #3114 の high/critical gate や temporary exception 条件を緩めない。 +確認後、`package.json` の `packageManager` と CI / nightly E2E / D1 migration workflow の Pin npm step を同じ version に更新し、lint、format、unit tests、security audit、Cloudflare build を通す。D1 workflow を変更する場合は上記の production merge gate も満たす。npm の変更だけを理由に #3114 の high/critical gate や temporary exception 条件を緩めない。 diff --git a/smkc-score-app/__tests__/docs/d1-migrate-npm-toolchain.test.ts b/smkc-score-app/__tests__/docs/d1-migrate-npm-toolchain.test.ts new file mode 100644 index 000000000..92f399440 --- /dev/null +++ b/smkc-score-app/__tests__/docs/d1-migrate-npm-toolchain.test.ts @@ -0,0 +1,69 @@ +import fs from 'fs'; +import path from 'path'; +import { parse } from 'yaml'; + +interface WorkflowStep { + name?: string; + run?: string; + env?: Record; +} + +interface WorkflowJob { + defaults?: { + run?: { + 'working-directory'?: string; + }; + }; + steps?: WorkflowStep[]; +} + +interface WorkflowDocument { + jobs?: Record; +} + +interface PackageManifest { + packageManager?: string; +} + +describe('D1 migration npm toolchain', () => { + const workflowPath = path.resolve(__dirname, '..', '..', '..', '.github', 'workflows', 'd1-migrate.yml'); + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + + it('pins packageManager npm before npm ci and skips Husky hooks without changing migration order', () => { + const workflow = parse(fs.readFileSync(workflowPath, 'utf8')) as WorkflowDocument; + const manifest = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as PackageManifest; + const applyJob = workflow.jobs?.apply; + const steps = applyJob?.steps; + const packageManager = manifest.packageManager; + + expect(steps?.length).toBeGreaterThan(0); + expect(packageManager).toMatch(/^npm@\d+\.\d+\.\d+$/); + expect(applyJob?.defaults?.run?.['working-directory']).toBe('smkc-score-app'); + + const pinStep = steps?.find((step) => step.name === 'Pin npm'); + const installStep = steps?.find((step) => step.name === 'Install dependencies'); + const listStep = steps?.find((step) => step.name === 'List pending migrations'); + const applyStep = steps?.find((step) => step.name === 'Apply migrations'); + + expect(pinStep).toBeDefined(); + expect(installStep).toBeDefined(); + expect(listStep).toBeDefined(); + expect(applyStep).toBeDefined(); + expect(pinStep?.run).toContain(`npm install --global --ignore-scripts --no-audit --no-fund ${packageManager}`); + + const expectedVersion = packageManager?.replace(/^npm@/, ''); + expect(pinStep?.run).toContain(`test "$(npm --version)" = "${expectedVersion}"`); + expect(installStep?.run?.trim()).toBe('npm ci'); + expect(installStep?.env?.HUSKY).toBe('0'); + + const pinIndex = steps?.indexOf(pinStep as WorkflowStep) ?? -1; + const installIndex = steps?.indexOf(installStep as WorkflowStep) ?? -1; + const listIndex = steps?.indexOf(listStep as WorkflowStep) ?? -1; + const applyIndex = steps?.indexOf(applyStep as WorkflowStep) ?? -1; + + expect(pinIndex).toBeGreaterThanOrEqual(0); + expect(pinIndex).toBeLessThan(installIndex); + expect(installIndex).toBeLessThan(listIndex); + expect(listIndex).toBeLessThan(applyIndex); + }); +});