Skip to content

Commit fe82270

Browse files
authored
Merge pull request #8 from nestm-dev/codex/better-auth-alpha-publish
fix: preserve prerelease npm tag
2 parents 44098ee + 3919fb9 commit fe82270

6 files changed

Lines changed: 189 additions & 3 deletions

File tree

.github/workflows/release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ jobs:
5050
# With pending changesets: opens/updates the "Version Packages" PR.
5151
# When that PR is merged (no pending changesets left): publishes to
5252
# npm via pnpm's OIDC trusted publishing (provenance enforced via
53-
# pnpm-workspace.yaml `provenance: true`) and creates the GitHub
53+
# pnpm-workspace.yaml `provenance: true`). The release wrapper keeps
54+
# prereleases on their Changesets dist tag and creates the GitHub
5455
# Release from the changelog.
5556
publish: pnpm run release
5657
version: pnpm changeset version

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"verify:pack": "pnpm run build && publint --strict",
6363
"prepack": "pnpm run build",
6464
"changeset": "changeset",
65-
"release": "changeset publish"
65+
"release": "node scripts/publish.mjs"
6666
},
6767
"peerDependencies": {
6868
"@nestjs/common": "^12.0.0-alpha.5",

scripts/publish-state.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const semverPrereleasePattern = /^[0-9]+\.[0-9]+\.[0-9]+-([0-9A-Za-z-]+)(?:[.+]|$)/;
2+
3+
export function resolvePrereleaseTag(version, preState) {
4+
if (typeof version !== "string") {
5+
throw new TypeError("package.json requires a string version");
6+
}
7+
8+
const prereleaseIdentifier = semverPrereleasePattern.exec(version)?.[1];
9+
10+
if (preState?.mode === "pre") {
11+
if (typeof preState.tag !== "string" || preState.tag.length === 0) {
12+
throw new Error("Changesets prerelease mode requires a non-empty tag");
13+
}
14+
15+
if (prereleaseIdentifier === undefined) {
16+
throw new Error(`Stable version ${version} cannot publish in Changesets pre mode`);
17+
}
18+
19+
if (prereleaseIdentifier !== preState.tag) {
20+
throw new Error(
21+
`Version prerelease ${prereleaseIdentifier} does not match Changesets tag ${preState.tag}`,
22+
);
23+
}
24+
25+
return preState.tag;
26+
}
27+
28+
if (prereleaseIdentifier !== undefined) {
29+
throw new Error(`Prerelease version ${version} requires Changesets pre mode`);
30+
}
31+
32+
return undefined;
33+
}

scripts/publish-state.spec.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { resolvePrereleaseTag } from "./publish-state.mjs";
4+
5+
describe("resolvePrereleaseTag", () => {
6+
it("returns the matching Changesets pre-mode tag", () => {
7+
expect(
8+
resolvePrereleaseTag("0.1.0-alpha.2", {
9+
mode: "pre",
10+
tag: "alpha",
11+
}),
12+
).toBe("alpha");
13+
expect(
14+
resolvePrereleaseTag("0.1.0-alpha+build.1", {
15+
mode: "pre",
16+
tag: "alpha",
17+
}),
18+
).toBe("alpha");
19+
});
20+
21+
it("allows a stable version outside pre mode", () => {
22+
expect(resolvePrereleaseTag("1.0.0", undefined)).toBeUndefined();
23+
expect(
24+
resolvePrereleaseTag("1.0.0", {
25+
mode: "exit",
26+
tag: "alpha",
27+
}),
28+
).toBeUndefined();
29+
});
30+
31+
it("rejects a prerelease without Changesets pre mode", () => {
32+
expect(() => resolvePrereleaseTag("0.1.0-alpha.2", undefined)).toThrow(
33+
"requires Changesets pre mode",
34+
);
35+
});
36+
37+
it("rejects a prerelease identifier that differs from the tag", () => {
38+
expect(() =>
39+
resolvePrereleaseTag("0.1.0-beta.1", {
40+
mode: "pre",
41+
tag: "alpha",
42+
}),
43+
).toThrow("does not match Changesets tag");
44+
});
45+
46+
it("rejects a stable version while pre mode is active", () => {
47+
expect(() =>
48+
resolvePrereleaseTag("1.0.0", {
49+
mode: "pre",
50+
tag: "alpha",
51+
}),
52+
).toThrow("cannot publish in Changesets pre mode");
53+
});
54+
});

scripts/publish.mjs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { spawnSync } from "node:child_process";
2+
import { existsSync, readFileSync, renameSync } from "node:fs";
3+
import { createRequire } from "node:module";
4+
5+
import { resolvePrereleaseTag } from "./publish-state.mjs";
6+
7+
const expectedRef = "refs/heads/main";
8+
9+
if (
10+
process.env.GITHUB_ACTIONS !== "true" ||
11+
process.env.GITHUB_REF !== expectedRef ||
12+
typeof process.env.GITHUB_SHA !== "string"
13+
) {
14+
throw new Error(`Publishing is restricted to GitHub Actions on ${expectedRef}`);
15+
}
16+
17+
const gitStatus = spawnSync("git", ["status", "--porcelain"], {
18+
encoding: "utf8",
19+
});
20+
21+
if (gitStatus.error) {
22+
throw new Error("Could not inspect the git worktree", {
23+
cause: gitStatus.error,
24+
});
25+
}
26+
27+
if (gitStatus.status !== 0) {
28+
throw new Error(`git status exited with status ${gitStatus.status ?? "unknown"}`);
29+
}
30+
31+
if (gitStatus.stdout.trim().length !== 0) {
32+
throw new Error("Refusing to publish from a dirty git worktree");
33+
}
34+
35+
const headResult = spawnSync("git", ["rev-parse", "HEAD"], {
36+
encoding: "utf8",
37+
});
38+
39+
if (headResult.error) {
40+
throw new Error("Could not resolve the git commit", {
41+
cause: headResult.error,
42+
});
43+
}
44+
45+
if (headResult.status !== 0) {
46+
throw new Error(`git rev-parse exited with status ${headResult.status ?? "unknown"}`);
47+
}
48+
49+
if (headResult.stdout.trim() !== process.env.GITHUB_SHA) {
50+
throw new Error("Git HEAD does not match the GitHub Actions commit");
51+
}
52+
53+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
54+
const preStateUrl = new URL("../.changeset/pre.json", import.meta.url);
55+
const hiddenPreStateUrl = new URL("../.changeset/pre.json.publish", import.meta.url);
56+
const preState = existsSync(preStateUrl)
57+
? JSON.parse(readFileSync(preStateUrl, "utf8"))
58+
: undefined;
59+
const prereleaseTag = resolvePrereleaseTag(packageJson.version, preState);
60+
const require = createRequire(import.meta.url);
61+
const changesetsBin = require.resolve("@changesets/cli/bin.js");
62+
const publishArguments = [changesetsBin, "publish"];
63+
64+
if (prereleaseTag !== undefined) {
65+
if (existsSync(hiddenPreStateUrl)) {
66+
throw new Error("Refusing to overwrite a hidden Changesets pre-state file");
67+
}
68+
69+
publishArguments.push("--tag", prereleaseTag);
70+
renameSync(preStateUrl, hiddenPreStateUrl);
71+
}
72+
73+
let publishResult;
74+
75+
try {
76+
// Changesets forces `latest` while a package has only prerelease history and
77+
// rejects an explicit tag in pre mode. The validated pre-state is hidden only
78+
// for this command so Changesets can publish to that same configured tag.
79+
publishResult = spawnSync(process.execPath, publishArguments, {
80+
stdio: "inherit",
81+
});
82+
} finally {
83+
if (prereleaseTag !== undefined && existsSync(hiddenPreStateUrl)) {
84+
renameSync(hiddenPreStateUrl, preStateUrl);
85+
}
86+
}
87+
88+
if (publishResult.error) {
89+
throw new Error("Could not start Changesets publish", {
90+
cause: publishResult.error,
91+
});
92+
}
93+
94+
if (publishResult.signal !== null) {
95+
throw new Error(`Changesets publish terminated with ${publishResult.signal}`);
96+
}
97+
98+
process.exitCode = publishResult.status ?? 1;

vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config";
22

33
export default defineConfig({
44
test: {
5-
include: ["tests/**/*.test.ts"],
5+
include: ["tests/**/*.test.ts", "scripts/**/*.spec.mjs"],
66
setupFiles: ["tests/setup.ts"],
77
testTimeout: 20_000,
88
hookTimeout: 20_000,

0 commit comments

Comments
 (0)