mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
739 lines
20 KiB
TypeScript
739 lines
20 KiB
TypeScript
import { execFileSync, spawnSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, resolve } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { findPreviousTag } from "../src/release-notes/collect.ts";
|
|
import {
|
|
parseSchema,
|
|
releaseManifestSchema,
|
|
} from "../src/release-notes/schema.ts";
|
|
|
|
const repositoryRoot = resolve(import.meta.dirname, "../../..");
|
|
const releaseNotesScript = resolve(
|
|
import.meta.dirname,
|
|
"../src/commands/release-notes.ts",
|
|
);
|
|
|
|
interface CommandResult {
|
|
actionsOutput?: string;
|
|
status: number | null;
|
|
stderr: string;
|
|
stdout: string;
|
|
}
|
|
|
|
interface CollectOptions {
|
|
branch?: string;
|
|
commitRef?: string;
|
|
workspace?: string;
|
|
}
|
|
|
|
function collectReleaseNotes(
|
|
version: string,
|
|
options: CollectOptions = {},
|
|
): CommandResult {
|
|
const workspace = options.workspace ?? repositoryRoot;
|
|
const releaseCommit = spawnSync("git", ["rev-parse", "v" + version], {
|
|
cwd: workspace,
|
|
encoding: "utf-8",
|
|
});
|
|
if (!options.commitRef && releaseCommit.status !== 0) {
|
|
throw new Error(releaseCommit.stderr);
|
|
}
|
|
const commitRef = options.commitRef ?? releaseCommit.stdout.trim();
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
releaseNotesScript,
|
|
"collect",
|
|
"--version",
|
|
version,
|
|
"--branch",
|
|
options.branch ?? "v" + version,
|
|
"--commit-ref",
|
|
commitRef,
|
|
"--dry-run",
|
|
],
|
|
{
|
|
cwd: workspace,
|
|
encoding: "utf-8",
|
|
env: {
|
|
...process.env,
|
|
GH_TOKEN: "",
|
|
GITHUB_TOKEN: "",
|
|
GITHUB_REPOSITORY: "better-auth/better-auth",
|
|
GITHUB_SHA: commitRef,
|
|
GITHUB_WORKSPACE: workspace,
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
status: result.status,
|
|
stderr: result.stderr,
|
|
stdout: result.stdout,
|
|
};
|
|
}
|
|
|
|
function detectReleaseCandidate(
|
|
version: string,
|
|
branch: string,
|
|
workspace: string,
|
|
): CommandResult {
|
|
const outputDirectory = mkdtempSync(resolve(tmpdir(), "release-output-"));
|
|
const outputPath = resolve(outputDirectory, "output");
|
|
writeFileSync(outputPath, "");
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[releaseNotesScript, "candidate", "--version", version, "--branch", branch],
|
|
{
|
|
cwd: workspace,
|
|
encoding: "utf-8",
|
|
env: {
|
|
...process.env,
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_WORKSPACE: workspace,
|
|
},
|
|
},
|
|
);
|
|
try {
|
|
return {
|
|
actionsOutput: existsSync(outputPath)
|
|
? readFileSync(outputPath, "utf-8")
|
|
: "",
|
|
status: result.status,
|
|
stderr: result.stderr,
|
|
stdout: result.stdout,
|
|
};
|
|
} finally {
|
|
rmSync(outputDirectory, { recursive: true });
|
|
}
|
|
}
|
|
|
|
function git(workspace: string, args: string[]): string {
|
|
return execFileSync("git", args, {
|
|
cwd: workspace,
|
|
encoding: "utf-8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim();
|
|
}
|
|
|
|
function checkReleaseChangesets(workspace: string): CommandResult {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[releaseNotesScript, "check-changesets", "--branch", "HEAD"],
|
|
{
|
|
cwd: workspace,
|
|
encoding: "utf-8",
|
|
env: { ...process.env, GITHUB_WORKSPACE: workspace },
|
|
},
|
|
);
|
|
return {
|
|
status: result.status,
|
|
stderr: result.stderr,
|
|
stdout: result.stdout,
|
|
};
|
|
}
|
|
|
|
function writeFixtureFile(
|
|
workspace: string,
|
|
path: string,
|
|
content: string,
|
|
): void {
|
|
const file = resolve(workspace, path);
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, content);
|
|
}
|
|
|
|
function createChangesetStateFixture(
|
|
changesets: string[],
|
|
consumed?: string[],
|
|
): string {
|
|
const workspace = mkdtempSync(resolve(tmpdir(), "release-changesets-"));
|
|
git(workspace, ["init", "--initial-branch=main"]);
|
|
git(workspace, ["config", "user.email", "release-test@example.com"]);
|
|
git(workspace, ["config", "user.name", "Release Test"]);
|
|
git(workspace, ["config", "commit.gpgsign", "false"]);
|
|
writeFixtureFile(workspace, ".changeset/README.md", "# Changesets");
|
|
for (const changeset of changesets) {
|
|
writeFixtureFile(
|
|
workspace,
|
|
`.changeset/${changeset}.md`,
|
|
["---", '"better-auth": patch', "---", "", changeset].join("\n"),
|
|
);
|
|
}
|
|
if (consumed) {
|
|
writeFixtureFile(
|
|
workspace,
|
|
".changeset/pre.json",
|
|
JSON.stringify({ changesets: consumed }),
|
|
);
|
|
}
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: create changeset fixture"]);
|
|
return workspace;
|
|
}
|
|
|
|
interface ReleaseFixtureOptions {
|
|
longOrphanChangeset?: string;
|
|
mergeVersionPR?: boolean;
|
|
consumeLaterChangeset?: boolean;
|
|
includePRWithoutChangeset?: boolean;
|
|
splitVersionCommit?: boolean;
|
|
separateChangesetCommit?: boolean;
|
|
secondChangesetInPR?: string;
|
|
staleReleaseTag?: boolean;
|
|
supersedeChangeset?: boolean;
|
|
uppercaseChangesetId?: boolean;
|
|
}
|
|
|
|
function createReleaseWithFollowUpCommit(options: ReleaseFixtureOptions = {}): {
|
|
commitRef: string;
|
|
workspace: string;
|
|
} {
|
|
const workspace = mkdtempSync(resolve(tmpdir(), "release-collection-"));
|
|
git(workspace, ["init", "--initial-branch=main"]);
|
|
git(workspace, ["config", "user.email", "release-test@example.com"]);
|
|
git(workspace, ["config", "user.name", "Release Test"]);
|
|
git(workspace, ["config", "commit.gpgsign", "false"]);
|
|
|
|
writeFixtureFile(
|
|
workspace,
|
|
"packages/better-auth/package.json",
|
|
JSON.stringify({ name: "better-auth", version: "1.0.0" }),
|
|
);
|
|
writeFixtureFile(
|
|
workspace,
|
|
"packages/better-auth/README.md",
|
|
"# Better Auth",
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: initialize release fixture"]);
|
|
git(workspace, ["tag", "v1.0.0"]);
|
|
if (options.staleReleaseTag) git(workspace, ["tag", "v2.0.0"]);
|
|
git(workspace, ["switch", "-c", "version-pr"]);
|
|
const changesetId = options.uppercaseChangesetId
|
|
? "parseSetCookieHeader-value-validation"
|
|
: options.separateChangesetCommit
|
|
? "standalone-description"
|
|
: "pr-42";
|
|
let changesetPath = `.changeset/${changesetId}.md`;
|
|
const changesetDescription = options.separateChangesetCommit
|
|
? "Require explicit migration"
|
|
: "Require an explicit migration after the release.";
|
|
|
|
writeFixtureFile(
|
|
workspace,
|
|
changesetPath,
|
|
["---", '"better-auth": major', "---", "", changesetDescription].join("\n"),
|
|
);
|
|
if (options.separateChangesetCommit) {
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: add release changeset"]);
|
|
}
|
|
if (options.secondChangesetInPR) {
|
|
writeFixtureFile(
|
|
workspace,
|
|
".changeset/second-intent.md",
|
|
[
|
|
"---",
|
|
'"better-auth": patch',
|
|
"---",
|
|
"",
|
|
options.secondChangesetInPR,
|
|
].join("\n"),
|
|
);
|
|
}
|
|
writeFixtureFile(
|
|
workspace,
|
|
"packages/better-auth/src/session.ts",
|
|
"export const session = true;",
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, [
|
|
"commit",
|
|
"-m",
|
|
"fix(session): require explicit migration (#42)",
|
|
]);
|
|
|
|
if (options.includePRWithoutChangeset) {
|
|
writeFixtureFile(
|
|
workspace,
|
|
"packages/better-auth/vitest.config.ts",
|
|
"export default {};",
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "fix: isolate package tests (#43)"]);
|
|
}
|
|
|
|
if (options.supersedeChangeset) {
|
|
rmSync(resolve(workspace, changesetPath));
|
|
changesetPath = ".changeset/pr-44.md";
|
|
writeFixtureFile(
|
|
workspace,
|
|
changesetPath,
|
|
[
|
|
"---",
|
|
'"better-auth": patch',
|
|
"---",
|
|
"",
|
|
"Replace the superseded release intent.",
|
|
].join("\n"),
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, [
|
|
"commit",
|
|
"-m",
|
|
"revert: replace the superseded change (#44)",
|
|
]);
|
|
}
|
|
|
|
if (options.longOrphanChangeset) {
|
|
writeFixtureFile(
|
|
workspace,
|
|
".changeset/long-orphan.md",
|
|
[
|
|
"---",
|
|
'"better-auth": patch',
|
|
"---",
|
|
"",
|
|
options.longOrphanChangeset,
|
|
].join("\n"),
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: add long orphan changeset"]);
|
|
rmSync(resolve(workspace, ".changeset/long-orphan.md"));
|
|
}
|
|
|
|
if (options.secondChangesetInPR) {
|
|
rmSync(resolve(workspace, ".changeset/second-intent.md"));
|
|
}
|
|
rmSync(resolve(workspace, changesetPath));
|
|
if (options.splitVersionCommit) {
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: consume release changeset"]);
|
|
}
|
|
writeFixtureFile(
|
|
workspace,
|
|
"packages/better-auth/package.json",
|
|
JSON.stringify({ name: "better-auth", version: "2.0.0" }),
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: release v2.0.0"]);
|
|
|
|
if (options.consumeLaterChangeset) {
|
|
writeFixtureFile(
|
|
workspace,
|
|
".changeset/pr-99.md",
|
|
[
|
|
"---",
|
|
'"better-auth": patch',
|
|
"---",
|
|
"",
|
|
"Unrelated follow-up changeset.",
|
|
].join("\n"),
|
|
);
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: add follow-up changeset"]);
|
|
rmSync(resolve(workspace, ".changeset/pr-99.md"));
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "chore: consume follow-up changeset"]);
|
|
}
|
|
|
|
if (options.mergeVersionPR) {
|
|
git(workspace, ["switch", "main"]);
|
|
git(workspace, [
|
|
"merge",
|
|
"--no-ff",
|
|
"version-pr",
|
|
"-m",
|
|
"chore: merge version PR",
|
|
]);
|
|
}
|
|
|
|
writeFixtureFile(workspace, "release-follow-up.md", "Reviewed release copy.");
|
|
git(workspace, ["add", "."]);
|
|
git(workspace, ["commit", "-m", "docs: review release copy"]);
|
|
|
|
return { commitRef: git(workspace, ["rev-parse", "HEAD"]), workspace };
|
|
}
|
|
|
|
function validateReleaseVersion(version: string): CommandResult {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[releaseNotesScript, "validate", "--version", version],
|
|
{ cwd: repositoryRoot, encoding: "utf-8" },
|
|
);
|
|
return {
|
|
status: result.status,
|
|
stderr: result.stderr,
|
|
stdout: result.stdout,
|
|
};
|
|
}
|
|
|
|
describe("release version validation", () => {
|
|
it("uses strict semver validation", () => {
|
|
expect(validateReleaseVersion("1.8.0-beta.1").status).toBe(0);
|
|
expect(validateReleaseVersion("1.8.0-beta.01").status).toBe(1);
|
|
expect(validateReleaseVersion("v1.8.0").status).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("release manifest validation", () => {
|
|
it("rejects releases without visible entries", () => {
|
|
expect(() =>
|
|
parseSchema(
|
|
releaseManifestSchema,
|
|
{
|
|
repository: "better-auth/better-auth",
|
|
version: "1.8.0",
|
|
commitRef: "abc123",
|
|
entries: [],
|
|
previousTag: "v1.7.2",
|
|
packageMetadata: {},
|
|
},
|
|
"Invalid release manifest",
|
|
),
|
|
).toThrow("expected array to have >=1 items");
|
|
});
|
|
});
|
|
|
|
describe("release changeset readiness", () => {
|
|
it("rejects pending stable changesets", () => {
|
|
const workspace = createChangesetStateFixture(["pending-change"]);
|
|
try {
|
|
const result = checkReleaseChangesets(workspace);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("pending-change");
|
|
} finally {
|
|
rmSync(workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("accepts changesets already consumed by a prerelease", () => {
|
|
const workspace = createChangesetStateFixture(
|
|
["included-change"],
|
|
["included-change"],
|
|
);
|
|
try {
|
|
const result = checkReleaseChangesets(workspace);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
} finally {
|
|
rmSync(workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects new changesets added after a prerelease", () => {
|
|
const workspace = createChangesetStateFixture(
|
|
["included-change", "pending-change"],
|
|
["included-change"],
|
|
);
|
|
try {
|
|
const result = checkReleaseChangesets(workspace);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("pending-change");
|
|
expect(result.stderr).not.toContain("included-change");
|
|
} finally {
|
|
rmSync(workspace, { recursive: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("release changeset collection", () => {
|
|
it("selects the previous stable and prerelease tags", () => {
|
|
expect(findPreviousTag("1.7.1", false, "v1.7.1")).toBe("v1.7.0");
|
|
expect(findPreviousTag("1.7.0-beta.0", true, "v1.7.0-beta.0")).toBe(
|
|
"v1.6.2",
|
|
);
|
|
expect(findPreviousTag("1.7.0-rc.0", true, "v1.7.0-rc.0")).toBe(
|
|
"v1.7.0-beta.10",
|
|
);
|
|
expect(findPreviousTag("1.7.0-rc.6", true, "v1.7.0-rc.6")).toBe(
|
|
"v1.7.0-rc.5",
|
|
);
|
|
});
|
|
|
|
/**
|
|
* @see https://github.com/better-auth/better-auth/releases/tag/v1.6.28
|
|
*/
|
|
it("uses only changesets consumed by the release commit", () => {
|
|
const result = collectReleaseNotes("1.6.28");
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain("## `better-auth`");
|
|
expect(result.stdout).toContain("## `@better-auth/electron`");
|
|
expect(result.stdout).toContain("## `@better-auth/expo`");
|
|
expect(result.stdout).not.toContain("## `@better-auth/sso`");
|
|
});
|
|
|
|
/**
|
|
* @see https://github.com/better-auth/better-auth/releases/tag/v1.7.1
|
|
*/
|
|
it("preserves every package declared by a consumed changeset", () => {
|
|
const result = collectReleaseNotes("1.7.1");
|
|
const packageHeadings = result.stdout.match(/^## `[^`]+`(?: ✨)?$/gm);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(packageHeadings).toEqual(["## `better-auth`", "## `auth`"]);
|
|
});
|
|
|
|
it("finds consumed changesets before release follow-up commits", () => {
|
|
const fixture = createReleaseWithFollowUpCommit();
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain("### ❗ Breaking Changes");
|
|
expect(result.stdout).toContain(
|
|
"Require an explicit migration after the release.",
|
|
);
|
|
expect(result.stdout).toContain('"changeType": "breaking"');
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("keeps the PR title for every changeset a PR adds", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
secondChangesetInPR: "Also validate the migration target.",
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
"Loaded 2 changeset descriptions, 0 orphans",
|
|
);
|
|
const context = JSON.parse(
|
|
result.stdout.slice(result.stdout.indexOf("{")),
|
|
);
|
|
expect(context).toMatchObject({
|
|
"pr-42": {
|
|
changesetDescription:
|
|
"Require an explicit migration after the release.",
|
|
prNumber: 42,
|
|
changeType: "breaking",
|
|
},
|
|
"pr-42:second-intent": {
|
|
title: "require explicit migration",
|
|
changesetDescription: "Also validate the migration target.",
|
|
prNumber: 42,
|
|
changeType: "fix",
|
|
},
|
|
});
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("shortens orphan titles taken from a long first line", () => {
|
|
const longLine =
|
|
`Orphan changes ${"keep working across restarts ".repeat(30)}`.trim();
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
longOrphanChangeset: longLine,
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const title = result.stdout.match(
|
|
/"title": "(Orphan changes[^"]*)"/,
|
|
)?.[1];
|
|
expect(title).toBeDefined();
|
|
expect(title!.length).toBeLessThanOrEqual(500);
|
|
expect(title).toMatch(/\w…$/);
|
|
expect(longLine.startsWith(`${title!.slice(0, -1)} `)).toBe(true);
|
|
expect(result.stdout).toContain(`"changesetDescription": "${longLine}"`);
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("ignores later changeset deletions that did not create the version", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
consumeLaterChangeset: true,
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
"Require an explicit migration after the release.",
|
|
);
|
|
expect(result.stdout).toContain('"changeType": "breaking"');
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("finds changesets consumed inside a merged version PR", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({ mergeVersionPR: true });
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
"Require an explicit migration after the release.",
|
|
);
|
|
expect(result.stdout).toContain('"changeType": "breaking"');
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("finds changesets deleted before the version transition", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
splitVersionCommit: true,
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
"Require an explicit migration after the release.",
|
|
);
|
|
expect(result.stdout).toContain('"changeType": "breaking"');
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("uses an explicit commit ref even when a stale version tag exists", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({ staleReleaseTag: true });
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
"Require an explicit migration after the release.",
|
|
);
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("matches standalone changesets to PR subjects without their suffix", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
separateChangesetCommit: true,
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain("Found 1 entries");
|
|
expect(result.stdout).toContain("Require explicit migration");
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("uses consumed changesets as the release entry source", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
includePRWithoutChangeset: true,
|
|
supersedeChangeset: true,
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain("Replace the superseded release intent.");
|
|
expect(result.stdout).toContain("pull/44");
|
|
expect(result.stdout).not.toContain("pull/42");
|
|
expect(result.stdout).not.toContain("pull/43");
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("preserves mixed-case changeset identifiers", () => {
|
|
const fixture = createReleaseWithFollowUpCommit({
|
|
uppercaseChangesetId: true,
|
|
});
|
|
try {
|
|
const result = collectReleaseNotes("2.0.0", {
|
|
branch: "HEAD",
|
|
commitRef: fixture.commitRef,
|
|
workspace: fixture.workspace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
"Require an explicit migration after the release.",
|
|
);
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("keeps an untagged version eligible across later retry commits", () => {
|
|
const fixture = createReleaseWithFollowUpCommit();
|
|
try {
|
|
const candidate = detectReleaseCandidate(
|
|
"2.0.0",
|
|
fixture.commitRef,
|
|
fixture.workspace,
|
|
);
|
|
|
|
expect(candidate.status, candidate.stderr).toBe(0);
|
|
expect(candidate.actionsOutput).toMatch(/release<<[^\n]+\ntrue\n/);
|
|
expect(candidate.actionsOutput).toMatch(
|
|
/version_commit<<[^\n]+\n[a-f0-9]{40}\n/,
|
|
);
|
|
|
|
git(fixture.workspace, ["tag", "v2.0.0", fixture.commitRef]);
|
|
const published = detectReleaseCandidate(
|
|
"2.0.0",
|
|
fixture.commitRef,
|
|
fixture.workspace,
|
|
);
|
|
expect(published.status, published.stderr).toBe(0);
|
|
expect(published.actionsOutput).toMatch(/release<<[^\n]+\nfalse\n/);
|
|
} finally {
|
|
rmSync(fixture.workspace, { recursive: true });
|
|
}
|
|
});
|
|
});
|