-
Notifications
You must be signed in to change notification settings - Fork 70
fix: publish the version the workspace holds, not the changelog's #1458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bcbd635
4eb98dc
3142d3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /** | ||
| * `npm publish --workspace=<pkg>` ships the version in the WORKSPACE's | ||
| * package.json, not the version the changelog file being processed is named | ||
| * for. scripts/publish-npm.js took its idempotency check from the changelog | ||
| * and its publish from the tree, so the two disagreed the moment an older | ||
| * changelog file was republished through the `republish_paths` dispatch input | ||
| * in .github/workflows/release.yml. | ||
| * | ||
| * That is not hypothetical. Republishing changelog/core/0.7.52.md shipped | ||
| * 0.7.53 (the tree's version) while logging `published @webjsdev/core@0.7.52`, | ||
| * and the next file in the batch then failed with | ||
| * `E403 cannot publish over the previously published versions: 0.7.53`, | ||
| * killing every remaining package under `set -e`. | ||
| * | ||
| * This locks the guard: | ||
| * 1. A changelog version the workspace has moved past is SKIPPED (exit 0) | ||
| * with a message naming both versions. | ||
| * 2. Counterfactual: a changelog version that MATCHES the workspace does | ||
| * NOT take that skip path, proving the guard is keyed on the mismatch | ||
| * rather than skipping everything. | ||
| * | ||
| * Both cases are offline. The guard deliberately runs before the `npm view` | ||
| * registry call, since a version whose source is gone cannot be published | ||
| * whatever the registry says, and that ordering is what keeps this test from | ||
| * needing the network. | ||
| */ | ||
| import { test } from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import { spawnSync } from 'node:child_process'; | ||
| import { readFileSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
|
|
||
| const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); | ||
| const SCRIPT = join(ROOT, 'scripts/publish-npm.js'); | ||
|
|
||
| /** The version @webjsdev/core actually carries in this tree. */ | ||
| function treeVersion() { | ||
| return JSON.parse(readFileSync(join(ROOT, 'packages/core/package.json'), 'utf8')).version; | ||
| } | ||
|
|
||
| /** Write a changelog file naming @webjsdev/core at `version`, run the script. */ | ||
| function runFor(version) { | ||
| const dir = mkdtempSync(join(tmpdir(), 'publish-npm-')); | ||
| const file = join(dir, `${version}.md`); | ||
| writeFileSync( | ||
| file, | ||
| `---\npackage: "@webjsdev/core"\nversion: ${version}\ndate: 2026-01-01T00:00:00.000Z\ncommit_count: 1\n---\n## Fixes\n\n- something\n`, | ||
| ); | ||
| try { | ||
| return spawnSync('node', [SCRIPT, file], { | ||
| cwd: ROOT, | ||
| encoding: 'utf8', | ||
| // The deny-live-hosts preload does not reach a spawned child, and on a | ||
| // release branch (where the tree version is not on the registry yet) | ||
| // the script would fall through the view check into a real | ||
| // `npm publish` with whatever auth is ambient on the machine. An | ||
| // unroutable registry fails both npm calls fast, keeping this | ||
| // genuinely offline. `npm pkg get` is a purely local read, so the | ||
| // guard under test is unaffected. | ||
| // | ||
| // fetch-retries MUST be 0: npm's default of 2 retries with exponential | ||
| // backoff turns an instant connection-refused into a ~72 second test. | ||
| env: { | ||
| ...process.env, | ||
| npm_config_registry: 'http://127.0.0.1:1', | ||
| npm_config_fetch_retries: '0', | ||
| npm_config_fetch_timeout: '2000', | ||
| }, | ||
| }); | ||
| } finally { | ||
| rmSync(dir, { recursive: true, force: true }); | ||
| } | ||
| } | ||
|
|
||
| test('a changelog version the workspace has moved past is skipped, not published', () => { | ||
| // 0.0.1 can never be the tree's version, so this is a guaranteed mismatch | ||
| // without depending on which release the repo currently sits on. | ||
| const r = runFor('0.0.1'); | ||
| assert.equal(r.status, 0, `expected a clean skip, got status ${r.status}\n${r.stderr}`); | ||
| assert.match(r.stdout, /skip @webjsdev\/core@0\.0\.1/); | ||
| // The message must name the version that WOULD have shipped, since that is | ||
| // the fact the old log line hid. | ||
| assert.ok( | ||
| r.stdout.includes(treeVersion()), | ||
| `the skip message should name the tree version ${treeVersion()}, got: ${r.stdout}`, | ||
| ); | ||
| }); | ||
|
|
||
| test('counterfactual: a changelog version matching the workspace is not skipped by the guard', () => { | ||
| const r = runFor(treeVersion()); | ||
| // It proceeds past the guard to the registry check, which may then skip as | ||
| // already-published or fail offline. Either is fine. What must NOT appear is | ||
| // the mismatch skip, which would mean the guard fires unconditionally and | ||
| // the first assertion proves nothing. | ||
| assert.doesNotMatch( | ||
| r.stdout, | ||
| /can no longer be published from this tree/, | ||
| `the guard must not fire when the versions agree, got: ${r.stdout}`, | ||
| ); | ||
|
Comment on lines
+97
to
+101
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This asserts only that a string is absent, so it passes for reasons that have nothing to do with the guard: a crash before the guard, a script that exits early, a Since the registry is pinned unroutable, the matching-version case now always runs past the guard into the view and publish attempts, which fail predictably. That gives you something positive to assert on instead, e.g. that the run reached the publish step (non-zero exit with the Not blocking, and the positive test above carries most of the weight. Flagging because a counterfactual that cannot fail for the right reason tends to rot quietly. |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The delta made this reachable in a way it mostly was not before.
treeVersionis now null on any drifted-but-parseable shape, not just on a parse failure, and in that case this falls back toversion, the changelog's number, which is precisely the misleading value this line was changed to stop printing.Narrow, and not wrong exactly, since when the lookup fails there is no better number to hand. But the PR's claim is that the line reports what shipped, and on the fail-open path it reports a guess. Worth either saying so in the line itself or dropping the fallback and printing the package name alone when the version is unknown: