From 3a1ecbf7707e6aba6223d2ea5c2b870d86771e98 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Sun, 3 May 2026 10:44:00 -0700 Subject: [PATCH] ci(release): Add prerelease workflow support Allow the manual release workflow to compute beta, rc, and alpha prerelease versions from the current package version. This keeps preview builds distinct from the stable release line while preserving the normal patch, minor, and major flow. Mark scoped harness packages as public in Craft and package manifests so their first npm publishes use public access. Co-Authored-By: OpenAI Codex --- .craft.yml | 2 + .github/workflows/release.yml | 20 ++++- README.md | 12 +++ packages/harness-ai-sdk/package.json | 3 + packages/harness-pi-ai/package.json | 3 + scripts/calculate-release-version.mjs | 103 ++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 scripts/calculate-release-version.mjs diff --git a/.craft.yml b/.craft.yml index c0d5d6a..85a4021 100644 --- a/.craft.yml +++ b/.craft.yml @@ -7,7 +7,9 @@ targets: includeNames: /^vitest-evals-\d.*\.tgz$/ - name: npm id: "@vitest-evals/harness-ai-sdk" + access: public includeNames: /^vitest-evals-harness-ai-sdk-\d.*\.tgz$/ - name: npm id: "@vitest-evals/harness-pi-ai" + access: public includeNames: /^vitest-evals-harness-pi-ai-\d.*\.tgz$/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55ba615..99d749e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,20 @@ on: - minor - patch - major + prerelease: + description: Prepare a prerelease instead of a stable release + required: false + type: boolean + default: false + prerelease_id: + description: Prerelease identifier + required: false + type: choice + default: beta + options: + - beta + - rc + - alpha force: description: Force release (bypass blockers) required: false @@ -53,9 +67,13 @@ jobs: id: version env: BUMP: ${{ inputs.bump }} + PRERELEASE: ${{ inputs.prerelease }} + PRERELEASE_ID: ${{ inputs.prerelease_id }} run: | + set -euo pipefail CURRENT=$(node -p "require('./packages/vitest-evals/package.json').version") - NEW=$(npx semver -i "$BUMP" "$CURRENT") + NEW=$(node ./scripts/calculate-release-version.mjs \ + "$CURRENT" "$BUMP" "$PRERELEASE" "$PRERELEASE_ID") echo "current=$CURRENT" >> "$GITHUB_OUTPUT" echo "new=$NEW" >> "$GITHUB_OUTPUT" diff --git a/README.md b/README.md index b2b041b..808a77b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,18 @@ tables. Pull request CI runs the same core safety checks: release config validation, lint, typecheck, the CI test suite, and the workspace build. +## Releases + +Use the manual Release workflow for stable releases. Select `patch`, `minor`, +or `major` for the normal version bump. + +For a preview release that should not become the main stable version, set +`prerelease` to `true` and leave `prerelease_id` as `beta` unless you want an +`rc` or `alpha` line. From `0.8.0`, `bump=minor` produces `0.9.0-beta.0` and +`bump=major` produces `1.0.0-beta.0`; running prerelease again from +`1.0.0-beta.0` produces `1.0.0-beta.1`. Craft publishes npm prereleases under a +prerelease dist-tag so the stable `latest` line is not moved. + ## Example The `apps/demo-pi` app shows the intended explicit-run flow: diff --git a/packages/harness-ai-sdk/package.json b/packages/harness-ai-sdk/package.json index 9645459..81a3277 100644 --- a/packages/harness-ai-sdk/package.json +++ b/packages/harness-ai-sdk/package.json @@ -6,6 +6,9 @@ "main": "./dist/index.js", "module": "./dist/index.mjs", "files": ["dist"], + "publishConfig": { + "access": "public" + }, "exports": { ".": { "source": "./src/index.ts", diff --git a/packages/harness-pi-ai/package.json b/packages/harness-pi-ai/package.json index 740ca9d..274878c 100644 --- a/packages/harness-pi-ai/package.json +++ b/packages/harness-pi-ai/package.json @@ -6,6 +6,9 @@ "main": "./dist/index.js", "module": "./dist/index.mjs", "files": ["dist"], + "publishConfig": { + "access": "public" + }, "exports": { ".": { "source": "./src/index.ts", diff --git a/scripts/calculate-release-version.mjs b/scripts/calculate-release-version.mjs new file mode 100644 index 0000000..5746075 --- /dev/null +++ b/scripts/calculate-release-version.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node + +const current = process.argv[2]; +const bump = process.argv[3]; +const prerelease = process.argv[4] === "true"; +const prereleaseId = process.argv[5] || "beta"; + +const allowedBumps = new Set(["patch", "minor", "major"]); +const allowedPrereleaseIds = new Set(["beta", "rc", "alpha"]); + +if (!current || !bump) { + console.error( + "Usage: node scripts/calculate-release-version.mjs [prerelease-id]", + ); + process.exit(1); +} + +if (!allowedBumps.has(bump)) { + console.error(`Invalid bump: ${bump}`); + process.exit(1); +} + +if (!allowedPrereleaseIds.has(prereleaseId)) { + console.error(`Invalid prerelease id: ${prereleaseId}`); + process.exit(1); +} + +const version = parseVersion(current); +if (!version) { + console.error(`Invalid current version: ${current}`); + process.exit(1); +} + +const next = prerelease + ? nextPrereleaseVersion(version, bump, prereleaseId) + : nextStableVersion(version, bump); + +console.log(next); + +function parseVersion(value) { + const match = value.match( + /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/, + ); + + if (!match) { + return null; + } + + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]), + prerelease: match[4]?.split(".") ?? [], + }; +} + +function formatVersion({ major, minor, patch }, prereleaseParts = []) { + const base = `${major}.${minor}.${patch}`; + return prereleaseParts.length > 0 + ? `${base}-${prereleaseParts.join(".")}` + : base; +} + +function bumpStableBase(version, bumpType) { + switch (bumpType) { + case "major": + return { major: version.major + 1, minor: 0, patch: 0 }; + case "minor": + return { major: version.major, minor: version.minor + 1, patch: 0 }; + case "patch": + return { + major: version.major, + minor: version.minor, + patch: version.patch + 1, + }; + } +} + +function nextStableVersion(version, bumpType) { + if (version.prerelease.length > 0) { + return formatVersion(version); + } + + return formatVersion(bumpStableBase(version, bumpType)); +} + +function nextPrereleaseVersion(version, bumpType, id) { + if (version.prerelease.length === 0) { + return formatVersion(bumpStableBase(version, bumpType), [id, "0"]); + } + + const [currentId] = version.prerelease; + const lastPart = version.prerelease.at(-1); + + if (currentId !== id || !/^[0-9]+$/.test(lastPart)) { + return formatVersion(version, [id, "0"]); + } + + return formatVersion(version, [ + ...version.prerelease.slice(0, -1), + String(Number(lastPart) + 1), + ]); +}