Skip to content

Commit 8e3d58c

Browse files
fix(release): restrict prereleases to -preview.N and release from main only
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
1 parent 7d9ebfb commit 8e3d58c

4 files changed

Lines changed: 34 additions & 13 deletions

File tree

.github/workflows/release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ jobs:
116116
case "$GITHUB_REF" in
117117
refs/heads/main)
118118
if [[ "$RELEASE_VERSION" == *-* ]]; then
119+
# The only supported prerelease shape is X.Y.Z-preview.N: the update
120+
# client (src/update/notify.ts, bin/ocx.mjs) and the release-notes
121+
# helper both key off that exact suffix, so an -alpha/-beta/-rc build
122+
# would publish to preview and then never be seen as an update.
123+
if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-preview\.[0-9]+$ ]]; then
124+
echo "::error::Pre-release versions must be X.Y.Z-preview.N (got ${RELEASE_VERSION})"
125+
exit 1
126+
fi
119127
# Pre-release version — must use "preview" dist-tag
120128
if [ "$NPM_DIST_TAG" != "preview" ]; then
121129
echo "::error::Pre-release versions (${RELEASE_VERSION}) must use dist-tag 'preview', got '${NPM_DIST_TAG}'"

ROADMAP.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ no longer dependent on upstream decisions or timelines.
1212
- [x] **Independent versioning** — fully detached from upstream, own semver scheme
1313
- [x] **All closed PRs merged** — Dutch GUI, Claude Desktop, combo/alias, cursor fixes, etc.
1414
- [x] **Enhanced CI** — CodeQL, dependabot, security audit, workflow linting
15-
- [x] **Release process** — VERSIONING.md, RELEASE_PROCESS.md, CHANGELOG.md
15+
- [x] **Release process**: main-only publishing via `.github/workflows/release.yml`
16+
and `scripts/release.ts`, with review and promotion policy in `MAINTAINERS.md`
1617

1718
## Near term (next)
1819

1920
- [ ] **First fork release (`2.7.43`)**: publish from `main` on npm dist-tag `latest`
20-
(any pre-release suffix ships on `preview` instead)
21+
(prereleases use `X.Y.Z-preview.N` and ship on `preview`)
22+
- [ ] **Release documentation**: add `VERSIONING.md`, `RELEASE_PROCESS.md`, and
23+
`CHANGELOG.md` for the fork's release model
2124
- [ ] **Update READMEs** — ensure all translated READMEs (ko, zh, ru, ja) reflect fork status
2225
- [ ] **Clean up old tags** — remove stale upstream tags that don't point to our commits
2326
- [ ] **Dependency audit** — review and update all dependencies (gui + root)

scripts/release.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -224,24 +224,25 @@ if (!version || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
224224
}
225225
const dryRun = !args.includes("--publish");
226226

227-
// 1. Preflight — must be on main or preview, and local verification must pass.
227+
// 1. Preflight — every release runs from main (release.yml rejects any other ref),
228+
// and local verification must pass. Stable versions publish to the `latest` dist-tag,
229+
// prereleases to `preview`; the only supported prerelease shape is X.Y.Z-preview.N,
230+
// which is what the update client and the release-notes helper recognize.
228231
const branch = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
229-
const allowedBranches = ["main", "preview"];
230-
const expectedTag = branch === "preview" ? "preview" : "latest";
232+
const releaseBranch = "main";
233+
const isPrerelease = version.includes("-");
234+
const expectedTag = isPrerelease ? "preview" : "latest";
231235
const tag = args.includes("--tag") ? (args[args.indexOf("--tag") + 1] ?? expectedTag) : expectedTag;
232236
if (tag !== expectedTag) {
233-
console.error(`Release tag mismatch: ${branch} releases must use npm dist-tag '${expectedTag}' (got '${tag}').`);
237+
const kind = isPrerelease ? "Pre-release" : "Stable";
238+
console.error(`Release tag mismatch: ${kind} versions must use npm dist-tag '${expectedTag}' (got '${tag}').`);
234239
process.exit(1);
235240
}
236-
if (branch === "preview" && !version.includes("-preview.")) {
237-
console.error(`Preview releases must use a preview prerelease version (got ${version}).`);
241+
if (isPrerelease && !/^\d+\.\d+\.\d+-preview\.\d+$/.test(version)) {
242+
console.error(`Pre-release versions must be X.Y.Z-preview.N (got ${version}).`);
238243
process.exit(1);
239244
}
240-
if (branch === "main" && version.includes("-")) {
241-
console.error(`Main releases must use a stable semver version (got ${version}).`);
242-
process.exit(1);
243-
}
244-
if (!allowedBranches.includes(branch)) { console.error(`✗ must be on ${allowedBranches.join(" or ")} (currently ${branch}).`); process.exit(1); }
245+
if (branch !== releaseBranch) { console.error(`✗ must be on ${releaseBranch} (currently ${branch}).`); process.exit(1); }
245246
if ((await $`git status --porcelain`.text()).trim()) { console.error("✗ working tree not clean — commit or stash first."); process.exit(1); }
246247
const packageName = await readPackageName();
247248
console.log(`→ release metadata preflight (${packageName}@${version})`);

tests/ci-workflows.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,15 @@ describe("GitHub Actions hardening", () => {
271271
expect(workflow).toContain("Pre-release versions (${RELEASE_VERSION}) must use dist-tag 'preview'");
272272
expect(workflow).toContain("Stable releases (${RELEASE_VERSION}) must use dist-tag 'latest'");
273273
expect(workflow).not.toContain("refs/heads/preview)");
274+
// Prereleases are restricted to X.Y.Z-preview.N: the update client only parses
275+
// that suffix, so an -alpha/-beta/-rc publish would never notify preview users.
276+
expect(workflow).toContain("Pre-release versions must be X.Y.Z-preview.N");
277+
expect(workflow).toContain("^[0-9]+\\.[0-9]+\\.[0-9]+-preview\\.[0-9]+$");
278+
// The release helper must dispatch from the only ref the workflow accepts.
279+
const releaseHelper = await readText("scripts/release.ts");
280+
expect(releaseHelper).toContain('const releaseBranch = "main"');
281+
expect(releaseHelper).toContain('const expectedTag = isPrerelease ? "preview" : "latest"');
282+
expect(releaseHelper).not.toContain('["main", "preview"]');
274283

275284
// Release notes must include PR categories and the full channel commit range
276285
// (branch merges + direct commits). Preflight forbids an existing release, so

0 commit comments

Comments
 (0)