feat: add beta release channel
This commit is contained in:
@@ -38,8 +38,9 @@ const entries = [
|
||||
["RELEASE_BASE_VERSION", info.baseVersion],
|
||||
["RELEASE_PRERELEASE", info.prerelease ?? ""],
|
||||
["IS_PRERELEASE", info.isPrerelease ? "true" : "false"],
|
||||
["IS_RELEASE_CANDIDATE", info.isReleaseCandidate ? "true" : "false"],
|
||||
["IS_BETA", info.isBeta ? "true" : "false"],
|
||||
["RELEASE_TYPE", info.releaseType],
|
||||
["RELEASE_CHANNEL", info.releaseChannel],
|
||||
["DESKTOP_VERSION", info.version],
|
||||
["IS_SMOKE_TAG", info.isSmokeTag ? "true" : "false"],
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@ export function parseReleaseVersion(version) {
|
||||
const match = trimmed.match(versionPattern);
|
||||
if (!match?.groups) {
|
||||
throw new Error(
|
||||
`Unsupported release version "${version}". Expected semver like 0.1.41 or 0.1.41-rc.1.`,
|
||||
`Unsupported release version "${version}". Expected semver like 0.1.41 or 0.1.41-beta.1.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,20 @@ export function parseReleaseVersion(version) {
|
||||
const minor = Number.parseInt(match.groups.minor, 10);
|
||||
const patch = Number.parseInt(match.groups.patch, 10);
|
||||
const prerelease = match.groups.prerelease ?? null;
|
||||
const rcMatch = prerelease?.match(/^rc\.(?<rc>\d+)$/) ?? null;
|
||||
const rcNumber = rcMatch?.groups?.rc ? Number.parseInt(rcMatch.groups.rc, 10) : null;
|
||||
const betaMatch = prerelease?.match(/^beta\.(?<beta>\d+)$/) ?? null;
|
||||
const betaNumber = betaMatch?.groups?.beta ? Number.parseInt(betaMatch.groups.beta, 10) : null;
|
||||
|
||||
if (prerelease !== null && betaNumber === null) {
|
||||
throw new Error(
|
||||
`Unsupported release version "${version}". Expected beta prerelease versions like 0.1.41-beta.1.`,
|
||||
);
|
||||
}
|
||||
|
||||
assertInteger(major, "major version");
|
||||
assertInteger(minor, "minor version");
|
||||
assertInteger(patch, "patch version");
|
||||
if (rcNumber !== null) {
|
||||
assertInteger(rcNumber, "release candidate number");
|
||||
if (betaNumber !== null) {
|
||||
assertInteger(betaNumber, "beta number");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -40,8 +46,8 @@ export function parseReleaseVersion(version) {
|
||||
prerelease,
|
||||
baseVersion: `${major}.${minor}.${patch}`,
|
||||
isPrerelease: prerelease !== null,
|
||||
isReleaseCandidate: rcNumber !== null,
|
||||
rcNumber,
|
||||
isBeta: betaNumber !== null,
|
||||
betaNumber,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,7 +63,7 @@ export function normalizeReleaseTag(rawTag) {
|
||||
const match = trimmed.match(sourceTagPattern);
|
||||
if (!match?.groups?.version) {
|
||||
throw new Error(
|
||||
`Unsupported release tag "${rawTag}". Expected vX.Y.Z, vX.Y.Z-rc.N, desktop-v..., or android-v...`,
|
||||
`Unsupported release tag "${rawTag}". Expected vX.Y.Z, vX.Y.Z-beta.N, desktop-v..., or android-v...`,
|
||||
);
|
||||
}
|
||||
return `v${match.groups.version}`;
|
||||
@@ -73,9 +79,10 @@ export function getReleaseInfoFromSourceTag(sourceTag) {
|
||||
baseVersion: parsed.baseVersion,
|
||||
prerelease: parsed.prerelease,
|
||||
isPrerelease: parsed.isPrerelease,
|
||||
isReleaseCandidate: parsed.isReleaseCandidate,
|
||||
rcNumber: parsed.rcNumber,
|
||||
isBeta: parsed.isBeta,
|
||||
betaNumber: parsed.betaNumber,
|
||||
releaseType: parsed.isPrerelease ? "prerelease" : "release",
|
||||
releaseChannel: parsed.isBeta ? "beta" : "latest",
|
||||
isSmokeTag: sourceTag.includes("gha-smoke"),
|
||||
};
|
||||
}
|
||||
@@ -110,54 +117,54 @@ export function computeNextReleaseVersion(currentVersion, mode) {
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "rc-patch" || mode === "rc-minor" || mode === "rc-major") {
|
||||
if (mode === "beta-patch" || mode === "beta-minor" || mode === "beta-major") {
|
||||
if (parsed.isPrerelease) {
|
||||
throw new Error(
|
||||
`Cannot start a new RC line from prerelease version ${currentVersion}. Use rc-next or promote.`,
|
||||
`Cannot start a new beta line from prerelease version ${currentVersion}. Use beta-next or promote.`,
|
||||
);
|
||||
}
|
||||
if (mode === "rc-patch") {
|
||||
if (mode === "beta-patch") {
|
||||
return formatReleaseVersion({
|
||||
major: parsed.major,
|
||||
minor: parsed.minor,
|
||||
patch: parsed.patch + 1,
|
||||
prerelease: "rc.1",
|
||||
prerelease: "beta.1",
|
||||
});
|
||||
}
|
||||
if (mode === "rc-minor") {
|
||||
if (mode === "beta-minor") {
|
||||
return formatReleaseVersion({
|
||||
major: parsed.major,
|
||||
minor: parsed.minor + 1,
|
||||
patch: 0,
|
||||
prerelease: "rc.1",
|
||||
prerelease: "beta.1",
|
||||
});
|
||||
}
|
||||
return formatReleaseVersion({
|
||||
major: parsed.major + 1,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: "rc.1",
|
||||
prerelease: "beta.1",
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "rc-next") {
|
||||
if (!parsed.isReleaseCandidate || parsed.rcNumber === null) {
|
||||
if (mode === "beta-next") {
|
||||
if (!parsed.isBeta || parsed.betaNumber === null) {
|
||||
throw new Error(
|
||||
`Cannot advance RC number from ${currentVersion}. Expected a version like 0.1.41-rc.1.`,
|
||||
`Cannot advance beta number from ${currentVersion}. Expected a version like 0.1.41-beta.1.`,
|
||||
);
|
||||
}
|
||||
return formatReleaseVersion({
|
||||
major: parsed.major,
|
||||
minor: parsed.minor,
|
||||
patch: parsed.patch,
|
||||
prerelease: `rc.${parsed.rcNumber + 1}`,
|
||||
prerelease: `beta.${parsed.betaNumber + 1}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "promote") {
|
||||
if (!parsed.isReleaseCandidate) {
|
||||
if (!parsed.isBeta) {
|
||||
throw new Error(
|
||||
`Cannot promote ${currentVersion}. Expected a release candidate version like 0.1.41-rc.1.`,
|
||||
`Cannot promote ${currentVersion}. Expected a beta version like 0.1.41-beta.1.`,
|
||||
);
|
||||
}
|
||||
return parsed.baseVersion;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
computeNextReleaseVersion,
|
||||
getReleaseInfoFromSourceTag,
|
||||
parseReleaseVersion,
|
||||
} from "./release-version-utils.mjs";
|
||||
|
||||
test("computes the next beta patch from a stable version", () => {
|
||||
assert.equal(computeNextReleaseVersion("0.1.59", "beta-patch"), "0.1.60-beta.1");
|
||||
});
|
||||
|
||||
test("advances beta versions", () => {
|
||||
assert.equal(computeNextReleaseVersion("0.1.60-beta.1", "beta-next"), "0.1.60-beta.2");
|
||||
});
|
||||
|
||||
test("promotes beta versions to stable", () => {
|
||||
assert.equal(computeNextReleaseVersion("0.1.60-beta.2", "promote"), "0.1.60");
|
||||
});
|
||||
|
||||
test("parses beta release metadata", () => {
|
||||
assert.deepEqual(parseReleaseVersion("0.1.60-beta.1"), {
|
||||
version: "0.1.60-beta.1",
|
||||
major: 0,
|
||||
minor: 1,
|
||||
patch: 60,
|
||||
prerelease: "beta.1",
|
||||
baseVersion: "0.1.60",
|
||||
isPrerelease: true,
|
||||
isBeta: true,
|
||||
betaNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("emits beta release info from tags", () => {
|
||||
assert.deepEqual(getReleaseInfoFromSourceTag("v0.1.60-beta.1"), {
|
||||
sourceTag: "v0.1.60-beta.1",
|
||||
releaseTag: "v0.1.60-beta.1",
|
||||
version: "0.1.60-beta.1",
|
||||
baseVersion: "0.1.60",
|
||||
prerelease: "beta.1",
|
||||
isPrerelease: true,
|
||||
isBeta: true,
|
||||
betaNumber: 1,
|
||||
releaseType: "prerelease",
|
||||
releaseChannel: "beta",
|
||||
isSmokeTag: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects non-beta prerelease versions", () => {
|
||||
assert.throws(() => parseReleaseVersion("0.1.60-canary.1"), /Expected beta prerelease versions/);
|
||||
});
|
||||
@@ -11,7 +11,7 @@ const rootPackagePath = path.join(rootDir, "package.json");
|
||||
function usageAndExit(code = 1) {
|
||||
process.stderr.write(`Usage: node scripts/set-release-version.mjs --mode <mode> [--print]\n`);
|
||||
process.stderr.write(
|
||||
"Modes: patch, minor, major, rc-patch, rc-minor, rc-major, rc-next, promote\n",
|
||||
"Modes: patch, minor, major, beta-patch, beta-minor, beta-major, beta-next, promote\n",
|
||||
);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
@@ -139,24 +139,6 @@ function updateReleaseNotes({ releaseId, repo, notesPath }, execFileSync = nodeE
|
||||
);
|
||||
}
|
||||
|
||||
function buildPrereleaseNotes({ releaseTag, version }) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const lines = [
|
||||
`## ${version} - ${today}`,
|
||||
"",
|
||||
"Release candidate build for testing from `main`.",
|
||||
"",
|
||||
`- Tag: \`${releaseTag}\``,
|
||||
];
|
||||
|
||||
if (process.env.GITHUB_SHA) {
|
||||
lines.push(`- Commit: \`${process.env.GITHUB_SHA}\``);
|
||||
}
|
||||
|
||||
lines.push("- This prerelease does not replace the website's latest stable download.");
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export function syncReleaseNotes(argv = process.argv.slice(2), deps = {}) {
|
||||
const execFileSync = deps.execFileSync ?? nodeExecFileSync;
|
||||
const args = parseArgs(argv);
|
||||
@@ -170,13 +152,6 @@ export function syncReleaseNotes(argv = process.argv.slice(2), deps = {}) {
|
||||
|
||||
let notes = targetEntry?.notes ?? null;
|
||||
|
||||
if (!notes && releaseInfo.isPrerelease) {
|
||||
notes = buildPrereleaseNotes({
|
||||
releaseTag: releaseInfo.releaseTag,
|
||||
version: releaseInfo.version,
|
||||
});
|
||||
}
|
||||
|
||||
if (!notes) {
|
||||
console.log(`No matching changelog section found for ${targetTag}. Skipping.`);
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,7 @@ function withTempChangelog(fn) {
|
||||
const previousCwd = process.cwd();
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-release-notes-test-"));
|
||||
process.chdir(tempDir);
|
||||
writeFileSync("CHANGELOG.md", "## 0.1.60 - 2026-04-20\n\n- Stable notes.\n");
|
||||
writeFileSync("CHANGELOG.md", "## 0.1.60-beta.1 - 2026-04-20\n\n- Beta notes.\n");
|
||||
|
||||
try {
|
||||
fn();
|
||||
@@ -26,7 +26,7 @@ test("updates an existing release body through the release id API", () => {
|
||||
const execFileSync = (command, args, options) => {
|
||||
calls.push({ args, command, options });
|
||||
|
||||
if (args[0] === "api" && args[1] === "repos/getpaseo/paseo/releases/tags/v0.1.60-rc.3") {
|
||||
if (args[0] === "api" && args[1] === "repos/getpaseo/paseo/releases/tags/v0.1.60-beta.1") {
|
||||
return JSON.stringify({ id: 311163621 });
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@ test("updates an existing release body through the release id API", () => {
|
||||
const notesArg = args.find((arg) => arg.startsWith("body=@"));
|
||||
assert.ok(notesArg, "PATCH should send the notes body from a file");
|
||||
const notesPath = notesArg.slice("body=@".length);
|
||||
assert.match(notesPath, /v0\.1\.60-rc\.3-notes\.md$/);
|
||||
assert.match(notesPath, /v0\.1\.60-beta\.1-notes\.md$/);
|
||||
return "";
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected gh call: ${command} ${args.join(" ")}`);
|
||||
};
|
||||
|
||||
syncReleaseNotes(["--repo", "getpaseo/paseo", "--tag", "v0.1.60-rc.3"], {
|
||||
syncReleaseNotes(["--repo", "getpaseo/paseo", "--tag", "v0.1.60-beta.1"], {
|
||||
execFileSync,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user