chore: add release preflight workflow
This commit is contained in:
@@ -8,6 +8,7 @@ For the maintainer release workflow, use [doc/RELEASING.md](RELEASING.md). This
|
||||
|
||||
Use these scripts instead of older one-off publish commands:
|
||||
|
||||
- [`scripts/release-preflight.sh`](../scripts/release-preflight.sh) before any canary or stable release
|
||||
- [`scripts/release.sh`](../scripts/release.sh) for canary and stable npm publishes
|
||||
- [`scripts/rollback-latest.sh`](../scripts/rollback-latest.sh) to repoint `latest` during rollback
|
||||
- [`scripts/create-github-release.sh`](../scripts/create-github-release.sh) after a stable push
|
||||
|
||||
@@ -26,24 +26,20 @@ Treat those as related but separate. npm can succeed while the GitHub Release is
|
||||
Use this when you want an installable prerelease without changing `latest`.
|
||||
|
||||
```bash
|
||||
# 0. Start clean
|
||||
git status --short
|
||||
# 0. Preflight the canary candidate
|
||||
./scripts/release-preflight.sh canary patch
|
||||
|
||||
# 1. Verify the candidate SHA
|
||||
pnpm -r typecheck
|
||||
pnpm test:run
|
||||
pnpm build
|
||||
# 1. Draft or update the stable changelog for the intended stable version
|
||||
VERSION=0.2.8
|
||||
claude -p "Use the release-changelog skill to draft or update releases/v${VERSION}.md for Paperclip. Read doc/RELEASING.md and skills/release-changelog/SKILL.md, then generate the stable changelog for v${VERSION} from commits since the last stable tag. Do not create a canary changelog."
|
||||
|
||||
# 2. Draft or update the stable changelog
|
||||
# releases/vX.Y.Z.md
|
||||
|
||||
# 3. Preview the canary release
|
||||
# 2. Preview the canary release
|
||||
./scripts/release.sh patch --canary --dry-run
|
||||
|
||||
# 4. Publish the canary
|
||||
# 3. Publish the canary
|
||||
./scripts/release.sh patch --canary
|
||||
|
||||
# 5. Smoke test what users will actually install
|
||||
# 4. Smoke test what users will actually install
|
||||
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
|
||||
|
||||
# Users install with:
|
||||
@@ -57,6 +53,7 @@ Result:
|
||||
- no git tag is created
|
||||
- no GitHub Release is created
|
||||
- the working tree returns to clean after the script finishes
|
||||
- after stable `0.2.7`, a patch canary targets `0.2.8-canary.0`, never `0.2.7-canary.N`
|
||||
|
||||
### Stable release
|
||||
|
||||
@@ -66,15 +63,13 @@ Use this only after the canary SHA is good enough to become the public default.
|
||||
# 0. Start from the vetted commit
|
||||
git checkout master
|
||||
git pull
|
||||
git status --short
|
||||
|
||||
# 1. Verify again on the exact release SHA
|
||||
pnpm -r typecheck
|
||||
pnpm test:run
|
||||
pnpm build
|
||||
# 1. Preflight the stable candidate
|
||||
./scripts/release-preflight.sh stable patch
|
||||
|
||||
# 2. Confirm the stable changelog exists
|
||||
ls releases/v*.md
|
||||
VERSION=0.2.8
|
||||
ls "releases/v${VERSION}.md"
|
||||
|
||||
# 3. Preview the stable publish
|
||||
./scripts/release.sh patch --dry-run
|
||||
@@ -174,6 +169,15 @@ pnpm build
|
||||
|
||||
This matches [`.github/workflows/pr-verify.yml`](../.github/workflows/pr-verify.yml). Run it before claiming a release candidate is ready.
|
||||
|
||||
For release work, prefer:
|
||||
|
||||
```bash
|
||||
./scripts/release-preflight.sh canary <patch|minor|major>
|
||||
./scripts/release-preflight.sh stable <patch|minor|major>
|
||||
```
|
||||
|
||||
That script runs the verification gate and prints the computed target versions before you publish anything.
|
||||
|
||||
## Versioning Policy
|
||||
|
||||
### Stable versions
|
||||
@@ -200,6 +204,11 @@ That gives you three useful properties:
|
||||
|
||||
We do **not** create separate changelog files for canary versions.
|
||||
|
||||
Concrete example:
|
||||
|
||||
- if the latest stable release is `0.2.7`, a patch canary is `0.2.8-canary.0`
|
||||
- `0.2.7-canary.0` is invalid, because `0.2.7` is already the shipped stable version
|
||||
|
||||
## Changelog Policy
|
||||
|
||||
The maintainer changelog source of truth is:
|
||||
@@ -222,7 +231,23 @@ Package-level `CHANGELOG.md` files are generated as part of the release mechanic
|
||||
|
||||
### 1. Decide the bump
|
||||
|
||||
Review the range since the last stable tag:
|
||||
Run preflight first:
|
||||
|
||||
```bash
|
||||
./scripts/release-preflight.sh canary <patch|minor|major>
|
||||
# or
|
||||
./scripts/release-preflight.sh stable <patch|minor|major>
|
||||
```
|
||||
|
||||
That command:
|
||||
|
||||
- verifies the worktree is clean, including untracked files
|
||||
- shows the last stable tag and computed next versions
|
||||
- shows the commit range since the last stable tag
|
||||
- highlights migration and breaking-change signals
|
||||
- runs `pnpm -r typecheck`, `pnpm test:run`, and `pnpm build`
|
||||
|
||||
If you want the raw inputs separately, review the range since the last stable tag:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --list 'v*' --sort=-version:refname | head -1)
|
||||
@@ -239,7 +264,8 @@ Use the higher bump if there is any doubt.
|
||||
Create or update:
|
||||
|
||||
```bash
|
||||
releases/vX.Y.Z.md
|
||||
VERSION=X.Y.Z
|
||||
claude -p "Use the release-changelog skill to draft or update releases/v${VERSION}.md for Paperclip. Read doc/RELEASING.md and skills/release-changelog/SKILL.md, then generate the stable changelog for v${VERSION} from commits since the last stable tag. Do not create a canary changelog."
|
||||
```
|
||||
|
||||
This is deliberate. The release notes should describe the stable story, not the canary mechanics.
|
||||
@@ -270,6 +296,12 @@ This means the script is safe to repeat as many times as needed while iterating:
|
||||
|
||||
The target stable release can still remain `1.2.3`.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- the canary is always derived from the **next stable version**
|
||||
- after stable `0.2.7`, the next patch canary is `0.2.8-canary.0`
|
||||
- the scripts refuse to publish `0.2.7-canary.N` once `0.2.7` is already the stable release
|
||||
|
||||
### 4. Smoke test the canary
|
||||
|
||||
Run the actual install path in Docker:
|
||||
@@ -426,6 +458,7 @@ Rollback procedure:
|
||||
## Scripts Reference
|
||||
|
||||
- [`scripts/release.sh`](../scripts/release.sh) — stable and canary npm publish flow
|
||||
- [`scripts/release-preflight.sh`](../scripts/release-preflight.sh) — clean-tree, version-plan, and verification-gate preflight
|
||||
- [`scripts/create-github-release.sh`](../scripts/create-github-release.sh) — create or update the GitHub Release after push
|
||||
- [`scripts/rollback-latest.sh`](../scripts/rollback-latest.sh) — repoint `latest` to the last good stable release
|
||||
- [`scripts/docker-onboard-smoke.sh`](../scripts/docker-onboard-smoke.sh) — Docker smoke test for the installed CLI
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"paperclipai": "node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts",
|
||||
"build:npm": "./scripts/build-npm.sh",
|
||||
"release": "./scripts/release.sh",
|
||||
"release:preflight": "./scripts/release-preflight.sh",
|
||||
"release:github": "./scripts/create-github-release.sh",
|
||||
"release:rollback": "./scripts/rollback-latest.sh",
|
||||
"changeset": "changeset",
|
||||
|
||||
182
scripts/release-preflight.sh
Executable file
182
scripts/release-preflight.sh
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
channel=""
|
||||
bump_type=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release-preflight.sh <canary|stable> <patch|minor|major>
|
||||
|
||||
Examples:
|
||||
./scripts/release-preflight.sh canary patch
|
||||
./scripts/release-preflight.sh stable minor
|
||||
|
||||
What it does:
|
||||
- verifies the git worktree is clean, including untracked files
|
||||
- shows the last stable tag and the target version(s)
|
||||
- shows commits since the last stable tag
|
||||
- highlights migration/schema/breaking-change signals
|
||||
- runs the verification gate:
|
||||
pnpm -r typecheck
|
||||
pnpm test:run
|
||||
pnpm build
|
||||
EOF
|
||||
}
|
||||
|
||||
if [ $# -eq 1 ] && [[ "$1" =~ ^(-h|--help)$ ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
channel="$1"
|
||||
bump_type="$2"
|
||||
|
||||
if [[ ! "$channel" =~ ^(canary|stable)$ ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$bump_type" =~ ^(patch|minor|major)$ ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
compute_bumped_version() {
|
||||
node - "$1" "$2" <<'NODE'
|
||||
const current = process.argv[2];
|
||||
const bump = process.argv[3];
|
||||
const match = current.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`invalid semver version: ${current}`);
|
||||
}
|
||||
|
||||
let [major, minor, patch] = match.slice(1).map(Number);
|
||||
|
||||
if (bump === 'patch') {
|
||||
patch += 1;
|
||||
} else if (bump === 'minor') {
|
||||
minor += 1;
|
||||
patch = 0;
|
||||
} else if (bump === 'major') {
|
||||
major += 1;
|
||||
minor = 0;
|
||||
patch = 0;
|
||||
} else {
|
||||
throw new Error(`unsupported bump type: ${bump}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`${major}.${minor}.${patch}`);
|
||||
NODE
|
||||
}
|
||||
|
||||
next_canary_version() {
|
||||
local stable_version="$1"
|
||||
local versions_json
|
||||
|
||||
versions_json="$(npm view paperclipai versions --json 2>/dev/null || echo '[]')"
|
||||
|
||||
node - "$stable_version" "$versions_json" <<'NODE'
|
||||
const stable = process.argv[2];
|
||||
const versionsArg = process.argv[3];
|
||||
|
||||
let versions = [];
|
||||
try {
|
||||
const parsed = JSON.parse(versionsArg);
|
||||
versions = Array.isArray(parsed) ? parsed : [parsed];
|
||||
} catch {
|
||||
versions = [];
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`^${stable.replace(/\./g, '\\.')}-canary\\.(\\d+)$`);
|
||||
let max = -1;
|
||||
|
||||
for (const version of versions) {
|
||||
const match = version.match(pattern);
|
||||
if (!match) continue;
|
||||
max = Math.max(max, Number(match[1]));
|
||||
}
|
||||
|
||||
process.stdout.write(`${stable}-canary.${max + 1}`);
|
||||
NODE
|
||||
}
|
||||
|
||||
LAST_STABLE_TAG="$(git -C "$REPO_ROOT" tag --list 'v*' --sort=-version:refname | head -1)"
|
||||
CURRENT_STABLE_VERSION="${LAST_STABLE_TAG#v}"
|
||||
if [ -z "$CURRENT_STABLE_VERSION" ]; then
|
||||
CURRENT_STABLE_VERSION="0.0.0"
|
||||
fi
|
||||
|
||||
TARGET_STABLE_VERSION="$(compute_bumped_version "$CURRENT_STABLE_VERSION" "$bump_type")"
|
||||
TARGET_CANARY_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
||||
|
||||
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
|
||||
echo "Error: working tree is not clean. Commit, stash, or remove changes before releasing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$TARGET_STABLE_VERSION" = "$CURRENT_STABLE_VERSION" ]; then
|
||||
echo "Error: next stable version matches the current stable version." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$TARGET_CANARY_VERSION" == "${CURRENT_STABLE_VERSION}-canary."* ]]; then
|
||||
echo "Error: canary target was derived from the current stable version, which is not allowed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Release preflight"
|
||||
echo " Channel: $channel"
|
||||
echo " Bump: $bump_type"
|
||||
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
||||
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
||||
echo " Next stable version: $TARGET_STABLE_VERSION"
|
||||
if [ "$channel" = "canary" ]; then
|
||||
echo " Next canary version: $TARGET_CANARY_VERSION"
|
||||
echo " Guard: canaries are always derived from the next stable version, never ${CURRENT_STABLE_VERSION}-canary.N"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Working tree"
|
||||
echo " ✓ Clean"
|
||||
|
||||
echo ""
|
||||
echo "==> Commits since last stable tag"
|
||||
if [ -n "$LAST_STABLE_TAG" ]; then
|
||||
git -C "$REPO_ROOT" log "${LAST_STABLE_TAG}..HEAD" --oneline --no-merges || true
|
||||
else
|
||||
git -C "$REPO_ROOT" log --oneline --no-merges || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Migration / breaking change signals"
|
||||
if [ -n "$LAST_STABLE_TAG" ]; then
|
||||
echo "-- migrations --"
|
||||
git -C "$REPO_ROOT" diff --name-only "${LAST_STABLE_TAG}..HEAD" -- packages/db/src/migrations/ || true
|
||||
echo "-- schema --"
|
||||
git -C "$REPO_ROOT" diff "${LAST_STABLE_TAG}..HEAD" -- packages/db/src/schema/ || true
|
||||
echo "-- breaking commit messages --"
|
||||
git -C "$REPO_ROOT" log "${LAST_STABLE_TAG}..HEAD" --format="%s" | grep -E 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true
|
||||
else
|
||||
echo "No stable tag exists yet. Review the full current tree manually."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Verification gate"
|
||||
cd "$REPO_ROOT"
|
||||
pnpm -r typecheck
|
||||
pnpm test:run
|
||||
pnpm build
|
||||
|
||||
echo ""
|
||||
echo "Preflight passed for $channel release."
|
||||
@@ -309,6 +309,14 @@ if [ "$canary" = true ]; then
|
||||
TARGET_PUBLISH_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
||||
fi
|
||||
|
||||
if [ "$TARGET_STABLE_VERSION" = "$CURRENT_STABLE_VERSION" ]; then
|
||||
fail "next stable version matches the current stable version. Refusing to publish."
|
||||
fi
|
||||
|
||||
if [[ "$TARGET_PUBLISH_VERSION" == "${CURRENT_STABLE_VERSION}-canary."* ]]; then
|
||||
fail "canary versions must be derived from the next stable version, never ${CURRENT_STABLE_VERSION}-canary.N."
|
||||
fi
|
||||
|
||||
PUBLIC_PACKAGE_INFO="$(list_public_package_info)"
|
||||
PUBLIC_PACKAGE_NAMES="$(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f2)"
|
||||
PUBLIC_PACKAGE_DIRS="$(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f1)"
|
||||
@@ -324,6 +332,7 @@ info " Current stable version: $CURRENT_STABLE_VERSION"
|
||||
if [ "$canary" = true ]; then
|
||||
info " Target stable version: $TARGET_STABLE_VERSION"
|
||||
info " Canary version: $TARGET_PUBLISH_VERSION"
|
||||
info " Guard: canary is derived from next stable version, not ${CURRENT_STABLE_VERSION}-canary.N"
|
||||
else
|
||||
info " Stable version: $TARGET_STABLE_VERSION"
|
||||
fi
|
||||
|
||||
@@ -69,7 +69,15 @@ Critical consequence:
|
||||
|
||||
## Step 1 — Decide the Stable Version
|
||||
|
||||
Use the last stable tag as the base:
|
||||
Run release preflight first:
|
||||
|
||||
```bash
|
||||
./scripts/release-preflight.sh canary {patch|minor|major}
|
||||
# or
|
||||
./scripts/release-preflight.sh stable {patch|minor|major}
|
||||
```
|
||||
|
||||
Then use the last stable tag as the base:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --list 'v*' --sort=-version:refname | head -1)
|
||||
@@ -128,6 +136,11 @@ What this means:
|
||||
- no git tag is created
|
||||
- the script cleans the working tree afterward
|
||||
|
||||
Guard:
|
||||
|
||||
- if the current stable is `0.2.7`, the next patch canary is `0.2.8-canary.0`
|
||||
- the tooling must never publish `0.2.7-canary.N` after `0.2.7` is already stable
|
||||
|
||||
After publish, verify:
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user