Skip to content

Commit 02a6d2e

Browse files
committed
fix(release): align published manifest version
1 parent 8914d6f commit 02a6d2e

6 files changed

Lines changed: 110 additions & 5 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"test:integration": "bun test tests/integration/",
6363
"test:all": "bun test",
6464
"typecheck": "tsc --noEmit",
65+
"prepack": "bun run scripts/verify-manifest-version.ts",
6566
"verify:release-candidate": "bun run scripts/verify-release-candidate.ts",
6667
"generate-readme": "bun run scripts/generate-readme.ts",
6768
"sync": "bun run scripts/sync-models.ts",

‎release.config.cjs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ module.exports = {
77
"@semantic-release/exec",
88
{
99
analyzeCommitsCmd: "bun scripts/analyze-release-scope.ts",
10+
// Stamp manifest.pluginVersion from nextRelease.version before
11+
// the npm plugin packs, so the published tarball embeds the
12+
// version it is published as.
13+
prepareCmd: "bun scripts/prepare-release-manifest.ts ${nextRelease.version}",
1014
},
1115
],
1216
"./scripts/semantic-release-catalog-notes.cjs",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bun
2+
// Semantic Release prepare step: stamp manifest.pluginVersion from
3+
// nextRelease.version before @semantic-release/npm packs. Catalog fields stay
4+
// untouched; the post-release sync PR commits the stamped versions to main.
5+
import { existsSync, readFileSync } from "fs";
6+
import { join } from "path";
7+
import { withPluginVersion, writeManifest, type CatalogManifest } from "../src/manifest.js";
8+
9+
const ROOT = join(import.meta.dir, "..");
10+
11+
function usage(): never {
12+
console.error("usage: prepare-release-manifest.ts <version>");
13+
process.exit(1);
14+
}
15+
16+
const version = process.argv[2]?.trim() || "";
17+
if (!version) usage();
18+
19+
const manifestPath = join(ROOT, "manifest.json");
20+
if (!existsSync(manifestPath)) {
21+
console.error(`manifest not found at ${manifestPath}`);
22+
process.exit(1);
23+
}
24+
25+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as CatalogManifest;
26+
writeManifest(manifestPath, withPluginVersion(manifest, version));
27+
console.log(`stamped manifest pluginVersion to ${version}`);

‎scripts/verify-manifest-version.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bun
2+
// Prepack guard: manifest.pluginVersion must equal package.json version.
3+
// Accepts an optional root path so tests can exercise the mismatch case in a
4+
// temporary directory. Exits non-zero on mismatch, before npm packs/publishes.
5+
import { existsSync, readFileSync } from "fs";
6+
import { join } from "path";
7+
8+
const ROOT = process.argv[2] ? join(process.argv[2]) : join(import.meta.dir, "..");
9+
10+
const pkgPath = join(ROOT, "package.json");
11+
const manifestPath = join(ROOT, "manifest.json");
12+
13+
if (!existsSync(pkgPath) || !existsSync(manifestPath)) {
14+
console.error(`missing package.json or manifest.json under ${ROOT}`);
15+
process.exit(1);
16+
}
17+
18+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
19+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as {
20+
pluginVersion?: string;
21+
};
22+
23+
if (pkg.version !== manifest.pluginVersion) {
24+
console.error(
25+
`manifest pluginVersion ${manifest.pluginVersion ?? "(missing)"} != package.json ${pkg.version ?? "(missing)"}`,
26+
);
27+
process.exit(1);
28+
}
29+
30+
console.log(`manifest version matches package.json (${pkg.version})`);

‎scripts/verify-release-candidate.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,22 @@ try {
3535
throw new Error(`tarball must not include ${banned}`);
3636
}
3737
}
38-
console.log(`verified ${tgz} (${files.length} files)`);
38+
const pkgJson = JSON.parse(
39+
execSync(`tar -xzOf ${JSON.stringify(join(dir, tgz))} package/package.json`, {
40+
encoding: "utf-8",
41+
}),
42+
) as { version?: string };
43+
const packedManifest = JSON.parse(
44+
execSync(`tar -xzOf ${JSON.stringify(join(dir, tgz))} package/manifest.json`, {
45+
encoding: "utf-8",
46+
}),
47+
) as { pluginVersion?: string };
48+
if (pkgJson.version !== packedManifest.pluginVersion) {
49+
throw new Error(
50+
`packed manifest pluginVersion ${packedManifest.pluginVersion ?? "(missing)"} != package.json ${pkgJson.version ?? "(missing)"}`,
51+
);
52+
}
53+
console.log(`verified ${tgz} (${files.length} files, version ${pkgJson.version})`);
3954
} finally {
4055
rmSync(dir, { recursive: true, force: true });
4156
}

‎tests/unit/release-workflow.test.ts‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,34 @@ describe("release.config.cjs", () => {
186186
expect(cfg.indexOf("@semantic-release/exec")).toBeLessThan(
187187
cfg.indexOf("semantic-release-catalog-notes.cjs"),
188188
);
189-
expect(cfg.indexOf("semantic-release-changelog.cjs")).toBeLessThan(
190-
cfg.indexOf("@semantic-release/npm"),
189+
expect(cfg.indexOf("prepare-release-manifest")).toBeGreaterThan(
190+
cfg.indexOf("@semantic-release/exec"),
191191
);
192-
expect(cfg.indexOf("@semantic-release/npm")).toBeLessThan(
193-
cfg.indexOf("@semantic-release/github"),
192+
});
193+
194+
test("sets the manifest version from nextRelease before packing", () => {
195+
const cfg = read("release.config.cjs");
196+
expect(cfg).toContain("prepare-release-manifest");
197+
expect(cfg).toContain("nextRelease.version");
198+
expect(cfg).toContain("prepareCmd");
199+
});
200+
});
201+
202+
describe("prepare-release-manifest.ts", () => {
203+
test("updates only pluginVersion from the release version", () => {
204+
const src = read("scripts/prepare-release-manifest.ts");
205+
expect(src).toContain("withPluginVersion");
206+
expect(src).toContain("process.argv");
207+
});
208+
});
209+
210+
describe("verify-manifest-version.ts", () => {
211+
test("guards the packed manifest against the package version", () => {
212+
const src = read("scripts/verify-manifest-version.ts");
213+
expect(src).toContain("pluginVersion");
214+
expect(src).toContain("process.exit");
215+
expect(json<{ scripts?: Record<string, string> }>("package.json").scripts?.prepack).toContain(
216+
"verify-manifest-version",
194217
);
195218
});
196219
});
@@ -213,4 +236,9 @@ describe("verify-release-candidate.ts", () => {
213236
/\b(?:npm|npx|bun)\s+(?:publish|login|adduser)\b|\bgit\s+(?:push|tag)\b/,
214237
);
215238
});
239+
240+
test("rejects a packed manifest whose pluginVersion differs from package.json", () => {
241+
const src = read("scripts/verify-release-candidate.ts");
242+
expect(src).toContain("pluginVersion");
243+
});
216244
});

0 commit comments

Comments
 (0)