Skip to content

Commit b710e0f

Browse files
authored
Merge pull request #288 from morluto/fix/npm-publication-verifier
fix(release): stop gating npm discovery on redundant npx round-trip
2 parents 74c8da3 + 95c3c84 commit b710e0f

3 files changed

Lines changed: 18 additions & 22 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,16 @@ jobs:
205205
}
206206
});
207207
' <<<"$metadata"
208-
- name: Publish npm package
208+
- id: publish
209+
name: Publish npm package
209210
run: npm publish --provenance --access public
210211
- name: Verify npm publication is publicly discoverable
211212
shell: bash
212213
env:
213214
VERSION: ${{ inputs.release_tag || github.ref_name }}
214215
run: node scripts/verify-npm-publication.mjs "${VERSION#v}"
215216
- name: Attach npm package to GitHub release
217+
if: always() && steps.publish.outcome == 'success'
216218
env:
217219
GH_TOKEN: ${{ github.token }}
218220
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}

npm/package.test.mjs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ test("release verifies npm discovery before publishing MCP Registry metadata", a
8484
assert.match(workflow, /node scripts\/verify-npm-publication\.mjs/);
8585
});
8686

87-
test("publication verification requires the public latest tag and a fresh npx runtime", async () => {
87+
test("publication verification requires the public latest tag and a resolvable version", async () => {
8888
const workspace = await mkdtemp(join(tmpdir(), "gitcontribute-publication-check-"));
8989
try {
9090
const client = join(workspace, "registry-client");
@@ -95,7 +95,6 @@ const args = process.argv.slice(2);
9595
fs.appendFileSync(process.env.GITCONTRIBUTE_TEST_CALL_LOG, JSON.stringify(args) + "\\n");
9696
if (args[0] === "view" && args[2] === "dist-tags.latest") process.stdout.write('"1.2.3"\\n');
9797
else if (args[0] === "view" && args[1] === "gitcontribute@1.2.3" && args[2] === "version") process.stdout.write('"1.2.3"\\n');
98-
else if (args[0] === "--yes") process.stdout.write('{"version":"1.2.3"}\\n');
9998
else process.exitCode = 1;
10099
`);
101100
await chmod(client, 0o755);
@@ -105,7 +104,6 @@ else process.exitCode = 1;
105104
env: {
106105
...process.env,
107106
GITCONTRIBUTE_NPM_COMMAND: client,
108-
GITCONTRIBUTE_NPX_COMMAND: client,
109107
GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS: "1",
110108
GITCONTRIBUTE_TEST_CALL_LOG: log,
111109
},
@@ -115,7 +113,6 @@ else process.exitCode = 1;
115113
assert.deepEqual(calls, [
116114
["view", "gitcontribute", "dist-tags.latest", "--json", "--prefer-online", "--registry=https://registry.npmjs.org"],
117115
["view", "gitcontribute@1.2.3", "version", "--json", "--prefer-online", "--registry=https://registry.npmjs.org"],
118-
["--yes", "--prefer-online", "gitcontribute@latest", "metadata", "--json"],
119116
]);
120117
} finally {
121118
await rm(workspace, { recursive: true, force: true });
@@ -134,7 +131,6 @@ fs.writeFileSync(process.env.GITCONTRIBUTE_TEST_ATTEMPTS, String(count + 1));
134131
if (count === 0) process.exitCode = 1;
135132
else if (process.argv[2] === "view" && process.argv[4] === "dist-tags.latest") process.stdout.write('"1.2.3"\\n');
136133
else if (process.argv[2] === "view" && process.argv[3] === "gitcontribute@1.2.3" && process.argv[4] === "version") process.stdout.write('"1.2.3"\\n');
137-
else if (process.argv[2] === "--yes") process.stdout.write('{"version":"1.2.3"}\\n');
138134
else process.exitCode = 1;
139135
`);
140136
await chmod(client, 0o755);
@@ -144,14 +140,13 @@ else process.exitCode = 1;
144140
env: {
145141
...process.env,
146142
GITCONTRIBUTE_NPM_COMMAND: client,
147-
GITCONTRIBUTE_NPX_COMMAND: client,
148143
GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS: "2",
149144
GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS: "1",
150145
GITCONTRIBUTE_TEST_ATTEMPTS: state,
151146
},
152147
});
153148
assert.equal(result.status, 0, result.stderr || result.stdout);
154-
assert.equal(await readFile(state, "utf8"), "4");
149+
assert.equal(await readFile(state, "utf8"), "3");
155150
} finally {
156151
await rm(workspace, { recursive: true, force: true });
157152
}
@@ -169,7 +164,6 @@ fs.writeFileSync(process.env.GITCONTRIBUTE_TEST_ATTEMPTS, String(count + 1));
169164
if (count === 0) setInterval(() => {}, 1_000);
170165
else if (process.argv[2] === "view" && process.argv[4] === "dist-tags.latest") process.stdout.write('"1.2.3"\\n');
171166
else if (process.argv[2] === "view" && process.argv[3] === "gitcontribute@1.2.3" && process.argv[4] === "version") process.stdout.write('"1.2.3"\\n');
172-
else if (process.argv[2] === "--yes") process.stdout.write('{"version":"1.2.3"}\\n');
173167
else process.exitCode = 1;
174168
`);
175169
await chmod(client, 0o755);
@@ -179,15 +173,14 @@ else process.exitCode = 1;
179173
env: {
180174
...process.env,
181175
GITCONTRIBUTE_NPM_COMMAND: client,
182-
GITCONTRIBUTE_NPX_COMMAND: client,
183176
GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS: "2",
184177
GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS: "1",
185178
GITCONTRIBUTE_NPM_PUBLICATION_PROBE_TIMEOUT_MS: "500",
186179
GITCONTRIBUTE_TEST_ATTEMPTS: state,
187180
},
188181
});
189182
assert.equal(result.status, 0, result.stderr || result.stdout);
190-
assert.equal(await readFile(state, "utf8"), "4");
183+
assert.equal(await readFile(state, "utf8"), "3");
191184
} finally {
192185
await rm(workspace, { recursive: true, force: true });
193186
}

scripts/verify-npm-publication.mjs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,29 @@ import { spawn } from "node:child_process";
33
const expectedVersion = process.argv[2];
44
if (!expectedVersion) throw new Error("expected version argument is required");
55

6+
// Public discoverability is a registry-metadata property: the version must be
7+
// resolvable and the `latest` dist-tag must point at it. The published binary
8+
// is already smoke-tested earlier in the release job via the local tarball, so
9+
// re-resolving `@latest` through npx here would only re-download the native
10+
// tarball to re-confirm metadata that `npm view` already proves. That extra
11+
// round-trip was the flaky straggler that failed the 3.0.0 release.
612
const registry = "https://registry.npmjs.org";
7-
const attempts = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS", 10, 30);
8-
const delayMS = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS", 6_000, 60_000);
13+
const attempts = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_ATTEMPTS", 30, 60);
14+
const delayMS = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_DELAY_MS", 5_000, 60_000);
915
const probeTimeoutMS = positiveInteger("GITCONTRIBUTE_NPM_PUBLICATION_PROBE_TIMEOUT_MS", 30_000, 120_000);
1016
const npm = process.env.GITCONTRIBUTE_NPM_COMMAND || "npm";
11-
const npx = process.env.GITCONTRIBUTE_NPX_COMMAND || "npx";
1217

1318
for (let attempt = 1; attempt <= attempts; attempt += 1) {
1419
try {
1520
const latest = await output(npm, ["view", "gitcontribute", "dist-tags.latest", "--json", "--prefer-online", `--registry=${registry}`]);
1621
const published = await output(npm, ["view", `gitcontribute@${expectedVersion}`, "version", "--json", "--prefer-online", `--registry=${registry}`]);
1722
if (jsonString(latest) === expectedVersion && jsonString(published) === expectedVersion) {
18-
const metadata = await output(npx, ["--yes", "--prefer-online", "gitcontribute@latest", "metadata", "--json"]);
19-
if (JSON.parse(metadata).version === expectedVersion) {
20-
console.log(`npm release ${expectedVersion} is publicly discoverable`);
21-
process.exit(0);
22-
}
23+
console.log(`npm release ${expectedVersion} is publicly discoverable`);
24+
process.exit(0);
2325
}
2426
} catch {
25-
// Registry propagation and fresh npx resolution are expected to be
26-
// transient immediately after publication. The bounded retry loop owns
27-
// those probes as one operation.
27+
// Registry propagation is expected to be transient immediately after
28+
// publication. The bounded retry loop owns those probes as one operation.
2829
}
2930
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, delayMS));
3031
}

0 commit comments

Comments
 (0)