From 4d44de76362de9bfdd90d3e9351a522f68a34514 Mon Sep 17 00:00:00 2001 From: TensorNull Date: Mon, 19 Oct 2026 19:13:23 +0800 Subject: [PATCH] ci: stabilize dependency updates and packed fixture versions --- .github/ACTIONLINT.md | 46 +++++++++++++++++++++++++++++++ .github/dependabot.yml | 12 ++++++++ fixtures/commonjs/package.json | 2 +- fixtures/esm/package.json | 2 +- fixtures/openai-host/package.json | 2 +- scripts/test-fixtures.mjs | 34 ++++++++++++++++------- 6 files changed, 85 insertions(+), 13 deletions(-) diff --git a/.github/ACTIONLINT.md b/.github/ACTIONLINT.md index be0be2a..aa4606e 100644 --- a/.github/ACTIONLINT.md +++ b/.github/ACTIONLINT.md @@ -20,3 +20,49 @@ ShellCheck is available on the host. A passing local run does not emulate GitHub-hosted runners, exercise repository settings, prove secret or environment configuration, perform a live CometAPI request, or prove npm Trusted Publishing. Those remain separate remote evidence. + +## Dependency update review + +Dependabot keeps production updates on their existing compatibility policy and +limits development version updates to minor and patch releases. Toolchain +major upgrades need a coordinated change that verifies compiler, linter, +bundler, test runner, and Node.js type compatibility with `npm ci` and +`npm run verify`. Do not use `--force` or `--legacy-peer-deps` to suppress peer +dependency conflicts. Security updates remain eligible; the version-update +filter does not suppress them. See the official +[Dependabot allow reference](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#allow). + +Packed fixtures declare the supported OpenAI range and select the exact install +version from the root lock file at runtime. Update their source declarations +only when intentionally changing the minimum supported range; lock-file patch +updates do not require copying the new version into each fixture. + +GitHub Actions version updates are grouped into one review. Every action +remains pinned to a full commit SHA. The Publish contract additionally pins the +parsed executable workflow with `PUBLISH_WORKFLOW_CONTRACT_SHA256` in +`scripts/release-workflow-validation.mjs`. An action SHA change in +`publish.yml` must fail that contract until a maintainer reviews the upstream +action changes and updates the digest in the same change. This is an expected +release safety check, not a transient failure to retry. + +After reviewing the complete Publish workflow diff and each action's upstream +changes, calculate the candidate digest from the repository root: + +```bash +node --input-type=module <<'EOF' +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { parse } from "yaml"; + +const workflow = parse(readFileSync(".github/workflows/publish.yml", "utf8")); +console.log(createHash("sha256").update(JSON.stringify(workflow)).digest("hex")); +EOF +``` + +Update the constant only after confirming that the reviewed action pins and +all release gates remain correct. Run `npm run verify` to cover the workflow +mutation regressions and pinned `actionlint` validation. Do not remove the +digest check or relax release permissions, immutable-tag checks, environment +approvals, live request budgets, or publication replay limits. Existing open +Dependabot branches retain their old contents until refreshed or replaced; +changing the update policy does not repair those branches by itself. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1bee4ce..4f3fc9b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,14 @@ updates: time: "04:00" timezone: Etc/UTC open-pull-requests-limit: 10 + allow: + - dependency-type: production + # Toolchain majors require a coordinated peer-dependency migration. + # This filter affects version updates, not security updates. + - dependency-type: development + update-types: + - version-update:semver-minor + - version-update:semver-patch groups: production-dependencies: dependency-type: production @@ -27,3 +35,7 @@ updates: time: "04:30" timezone: Etc/UTC open-pull-requests-limit: 10 + groups: + github-actions: + patterns: + - "*" diff --git a/fixtures/commonjs/package.json b/fixtures/commonjs/package.json index 33c106a..00cfef7 100644 --- a/fixtures/commonjs/package.json +++ b/fixtures/commonjs/package.json @@ -5,6 +5,6 @@ "test": "node index.cjs" }, "dependencies": { - "openai": "6.47.0" + "openai": "^6.47.0" } } diff --git a/fixtures/esm/package.json b/fixtures/esm/package.json index fc87f9a..1d3025d 100644 --- a/fixtures/esm/package.json +++ b/fixtures/esm/package.json @@ -6,6 +6,6 @@ "test": "node index.mjs" }, "dependencies": { - "openai": "6.47.0" + "openai": "^6.47.0" } } diff --git a/fixtures/openai-host/package.json b/fixtures/openai-host/package.json index 4b078a6..f58aee3 100644 --- a/fixtures/openai-host/package.json +++ b/fixtures/openai-host/package.json @@ -7,7 +7,7 @@ "typecheck": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project tsconfig.nonexact.json" }, "dependencies": { - "openai": "6.47.0" + "openai": "^6.47.0" }, "devDependencies": { "typescript": "5.9.3" diff --git a/scripts/test-fixtures.mjs b/scripts/test-fixtures.mjs index f8ae50d..b084027 100644 --- a/scripts/test-fixtures.mjs +++ b/scripts/test-fixtures.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { cpSync, existsSync } from "node:fs"; +import { cpSync, existsSync, writeFileSync } from "node:fs"; import { basename, join, resolve } from "node:path"; import { @@ -23,15 +23,9 @@ if (tarballArgument !== -1 && !suppliedTarball) { const lock = readJSON(join(ROOT, "package-lock.json")); const lockedOpenAIVersion = lock.packages["node_modules/openai"]?.version; assert.ok(lockedOpenAIVersion, "package-lock.json must lock OpenAI"); -for (const fixture of ["esm", "commonjs", "openai-host"]) { - const manifest = readJSON(join(ROOT, "fixtures", fixture, "package.json")); - assert.equal( - manifest.dependencies?.openai, - lockedOpenAIVersion, - `${fixture} must pin the locked OpenAI version`, - ); -} - +const supportedOpenAIRange = readJSON(join(ROOT, "package.json")).dependencies + ?.openai; +assert.ok(supportedOpenAIRange, "package.json must declare OpenAI support"); try { const tarball = (suppliedTarball ? resolve(ROOT, suppliedTarball) : undefined) ?? @@ -42,11 +36,31 @@ try { cpSync(join(ROOT, "fixtures", fixture), fixtureDirectory, { recursive: true, }); + const manifestPath = join(fixtureDirectory, "package.json"); + const manifest = readJSON(manifestPath); + assert.equal( + manifest.dependencies?.openai, + supportedOpenAIRange, + `${fixture} must declare the supported OpenAI range before installation`, + ); + manifest.dependencies.openai = lockedOpenAIVersion; + writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); run( "npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], { cwd: fixtureDirectory }, ); + const dependencyTree = JSON.parse( + run("npm", ["ls", "openai", "--all", "--json"], { + capture: true, + cwd: fixtureDirectory, + }), + ); + assert.equal( + dependencyTree.dependencies?.openai?.version, + lockedOpenAIVersion, + `${fixture} must install the locked OpenAI version`, + ); if (fixture === "openai-host") { const nestedOpenAI = join(