diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3a841b..386f63c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,7 +205,8 @@ jobs: } }); ' <<<"$metadata" - - name: Publish npm package + - id: publish + name: Publish npm package run: npm publish --provenance --access public - name: Verify npm publication is publicly discoverable shell: bash @@ -213,6 +214,7 @@ jobs: VERSION: ${{ inputs.release_tag || github.ref_name }} run: node scripts/verify-npm-publication.mjs "${VERSION#v}" - name: Attach npm package to GitHub release + if: always() && steps.publish.outcome == 'success' env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} diff --git a/npm/package.test.mjs b/npm/package.test.mjs index d0f329a..4b7fa85 100644 --- a/npm/package.test.mjs +++ b/npm/package.test.mjs @@ -84,7 +84,7 @@ test("release verifies npm discovery before publishing MCP Registry metadata", a assert.match(workflow, /node scripts\/verify-npm-publication\.mjs/); }); -test("publication verification requires the public latest tag and a fresh npx runtime", async () => { +test("publication verification requires the public latest tag and a resolvable version", async () => { const workspace = await mkdtemp(join(tmpdir(), "gitcontribute-publication-check-")); try { const client = join(workspace, "registry-client"); @@ -95,7 +95,6 @@ const args = process.argv.slice(2); fs.appendFileSync(process.env.GITCONTRIBUTE_TEST_CALL_LOG, JSON.stringify(args) + "\\n"); if (args[0] === "view" && args[2] === "dist-tags.latest") process.stdout.write('"1.2.3"\\n'); else if (args[0] === "view" && args[1] === "gitcontribute@1.2.3" && args[2] === "version") process.stdout.write('"1.2.3"\\n'); -else if (args[0] === "--yes") process.stdout.write('{"version":"1.2.3"}\\n'); else process.exitCode = 1; `); await chmod(client, 0o755); @@ -105,7 +104,6 @@ else process.exitCode = 1; env: { ...process.env, GITCONTRIBUTE_NPM_COMMAND: client, - GITCONTRIBUTE_NPX_COMMAND: client, GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS: "1", GITCONTRIBUTE_TEST_CALL_LOG: log, }, @@ -115,7 +113,6 @@ else process.exitCode = 1; assert.deepEqual(calls, [ ["view", "gitcontribute", "dist-tags.latest", "--json", "--prefer-online", "--registry=https://registry.npmjs.org"], ["view", "gitcontribute@1.2.3", "version", "--json", "--prefer-online", "--registry=https://registry.npmjs.org"], - ["--yes", "--prefer-online", "gitcontribute@latest", "metadata", "--json"], ]); } finally { await rm(workspace, { recursive: true, force: true }); @@ -134,7 +131,6 @@ fs.writeFileSync(process.env.GITCONTRIBUTE_TEST_ATTEMPTS, String(count + 1)); if (count === 0) process.exitCode = 1; else if (process.argv[2] === "view" && process.argv[4] === "dist-tags.latest") process.stdout.write('"1.2.3"\\n'); else if (process.argv[2] === "view" && process.argv[3] === "gitcontribute@1.2.3" && process.argv[4] === "version") process.stdout.write('"1.2.3"\\n'); -else if (process.argv[2] === "--yes") process.stdout.write('{"version":"1.2.3"}\\n'); else process.exitCode = 1; `); await chmod(client, 0o755); @@ -144,14 +140,13 @@ else process.exitCode = 1; env: { ...process.env, GITCONTRIBUTE_NPM_COMMAND: client, - GITCONTRIBUTE_NPX_COMMAND: client, GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS: "2", GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS: "1", GITCONTRIBUTE_TEST_ATTEMPTS: state, }, }); assert.equal(result.status, 0, result.stderr || result.stdout); - assert.equal(await readFile(state, "utf8"), "4"); + assert.equal(await readFile(state, "utf8"), "3"); } finally { await rm(workspace, { recursive: true, force: true }); } @@ -169,7 +164,6 @@ fs.writeFileSync(process.env.GITCONTRIBUTE_TEST_ATTEMPTS, String(count + 1)); if (count === 0) setInterval(() => {}, 1_000); else if (process.argv[2] === "view" && process.argv[4] === "dist-tags.latest") process.stdout.write('"1.2.3"\\n'); else if (process.argv[2] === "view" && process.argv[3] === "gitcontribute@1.2.3" && process.argv[4] === "version") process.stdout.write('"1.2.3"\\n'); -else if (process.argv[2] === "--yes") process.stdout.write('{"version":"1.2.3"}\\n'); else process.exitCode = 1; `); await chmod(client, 0o755); @@ -179,7 +173,6 @@ else process.exitCode = 1; env: { ...process.env, GITCONTRIBUTE_NPM_COMMAND: client, - GITCONTRIBUTE_NPX_COMMAND: client, GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS: "2", GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS: "1", GITCONTRIBUTE_NPM_PUBLICATION_PROBE_TIMEOUT_MS: "500", @@ -187,7 +180,7 @@ else process.exitCode = 1; }, }); assert.equal(result.status, 0, result.stderr || result.stdout); - assert.equal(await readFile(state, "utf8"), "4"); + assert.equal(await readFile(state, "utf8"), "3"); } finally { await rm(workspace, { recursive: true, force: true }); } diff --git a/scripts/verify-npm-publication.mjs b/scripts/verify-npm-publication.mjs index 1f03adc..f9e3aaf 100644 --- a/scripts/verify-npm-publication.mjs +++ b/scripts/verify-npm-publication.mjs @@ -3,28 +3,29 @@ import { spawn } from "node:child_process"; const expectedVersion = process.argv[2]; if (!expectedVersion) throw new Error("expected version argument is required"); +// Public discoverability is a registry-metadata property: the version must be +// resolvable and the `latest` dist-tag must point at it. The published binary +// is already smoke-tested earlier in the release job via the local tarball, so +// re-resolving `@latest` through npx here would only re-download the native +// tarball to re-confirm metadata that `npm view` already proves. That extra +// round-trip was the flaky straggler that failed the 3.0.0 release. const registry = "https://registry.npmjs.org"; -const attempts = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS", 10, 30); -const delayMS = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS", 6_000, 60_000); +const attempts = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS", 30, 60); +const delayMS = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS", 5_000, 60_000); const probeTimeoutMS = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_PROBE_TIMEOUT_MS", 30_000, 120_000); const npm = process.env.GITCONTRIBUTE_NPM_COMMAND || "npm"; -const npx = process.env.GITCONTRIBUTE_NPX_COMMAND || "npx"; for (let attempt = 1; attempt <= attempts; attempt += 1) { try { const latest = await output(npm, ["view", "gitcontribute", "dist-tags.latest", "--json", "--prefer-online", `--registry=${registry}`]); const published = await output(npm, ["view", `gitcontribute@${expectedVersion}`, "version", "--json", "--prefer-online", `--registry=${registry}`]); if (jsonString(latest) === expectedVersion && jsonString(published) === expectedVersion) { - const metadata = await output(npx, ["--yes", "--prefer-online", "gitcontribute@latest", "metadata", "--json"]); - if (JSON.parse(metadata).version === expectedVersion) { - console.log(`npm release ${expectedVersion} is publicly discoverable`); - process.exit(0); - } + console.log(`npm release ${expectedVersion} is publicly discoverable`); + process.exit(0); } } catch { - // Registry propagation and fresh npx resolution are expected to be - // transient immediately after publication. The bounded retry loop owns - // those probes as one operation. + // Registry propagation is expected to be transient immediately after + // publication. The bounded retry loop owns those probes as one operation. } if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, delayMS)); }