chore: add release train workflow
This commit is contained in:
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify:
|
verify:
|
||||||
if: github.ref == 'refs/heads/master'
|
if: startsWith(github.ref, 'refs/heads/release/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
if: github.ref == 'refs/heads/master'
|
if: startsWith(github.ref, 'refs/heads/release/')
|
||||||
needs: verify
|
needs: verify
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
@@ -115,9 +115,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
./scripts/release.sh "${args[@]}"
|
./scripts/release.sh "${args[@]}"
|
||||||
|
|
||||||
- name: Push stable release commit and tag
|
- name: Push stable release branch commit and tag
|
||||||
if: inputs.channel == 'stable' && !inputs.dry_run
|
if: inputs.channel == 'stable' && !inputs.dry_run
|
||||||
run: git push origin HEAD:master --follow-tags
|
run: git push origin "HEAD:${GITHUB_REF_NAME}" --follow-tags
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: inputs.channel == 'stable' && !inputs.dry_run
|
if: inputs.channel == 'stable' && !inputs.dry_run
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ For the maintainer release workflow, use [doc/RELEASING.md](RELEASING.md). This
|
|||||||
|
|
||||||
Use these scripts instead of older one-off publish commands:
|
Use these scripts instead of older one-off publish commands:
|
||||||
|
|
||||||
|
- [`scripts/release-start.sh`](../scripts/release-start.sh) to create or resume `release/X.Y.Z`
|
||||||
- [`scripts/release-preflight.sh`](../scripts/release-preflight.sh) before any canary or stable release
|
- [`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/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/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
|
- [`scripts/create-github-release.sh`](../scripts/create-github-release.sh) after pushing the stable branch tag
|
||||||
|
|
||||||
## Why the CLI needs special packaging
|
## Why the CLI needs special packaging
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ This means:
|
|||||||
|
|
||||||
Stable releases publish normal semver versions such as `1.2.3` under the npm dist-tag `latest`.
|
Stable releases publish normal semver versions such as `1.2.3` under the npm dist-tag `latest`.
|
||||||
|
|
||||||
The stable publish flow also creates the local release commit and git tag. Pushing the commit/tag and creating the GitHub Release happen afterward as separate maintainer steps.
|
The stable publish flow also creates the local release commit and git tag on `release/X.Y.Z`. Pushing that branch commit/tag, creating the GitHub Release, and merging the release branch back to `master` happen afterward as separate maintainer steps.
|
||||||
|
|
||||||
## Rollback model
|
## Rollback model
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ Recommended CI release setup:
|
|||||||
|
|
||||||
- use npm trusted publishing via GitHub OIDC
|
- use npm trusted publishing via GitHub OIDC
|
||||||
- require approval through the `npm-release` environment
|
- require approval through the `npm-release` environment
|
||||||
- run releases from `master`
|
- run releases from `release/X.Y.Z`
|
||||||
- use canary first, then stable
|
- use canary first, then stable
|
||||||
|
|
||||||
## Related Files
|
## Related Files
|
||||||
|
|||||||
594
doc/RELEASING.md
594
doc/RELEASING.md
@@ -2,260 +2,138 @@
|
|||||||
|
|
||||||
Maintainer runbook for shipping a full Paperclip release across npm, GitHub, and the website-facing changelog surface.
|
Maintainer runbook for shipping a full Paperclip release across npm, GitHub, and the website-facing changelog surface.
|
||||||
|
|
||||||
This document is intentionally practical:
|
The release model is branch-driven:
|
||||||
|
|
||||||
- TL;DR command sequences are at the top.
|
1. Start a release train on `release/X.Y.Z`
|
||||||
- Detailed checklists come next.
|
2. Draft the stable changelog on that branch
|
||||||
- Motivation, failure handling, and rollback playbooks follow after that.
|
3. Publish one or more canaries from that branch
|
||||||
|
4. Publish stable from that same branch head
|
||||||
|
5. Push the branch commit and tag
|
||||||
|
6. Create the GitHub Release
|
||||||
|
7. Merge `release/X.Y.Z` back to `master` without squash or rebase
|
||||||
|
|
||||||
## Release Surfaces
|
## Release Surfaces
|
||||||
|
|
||||||
Every Paperclip release has four separate surfaces:
|
Every release has four separate surfaces:
|
||||||
|
|
||||||
1. **Verification** — the exact git SHA must pass typecheck, tests, and build.
|
1. **Verification** — the exact git SHA passes typecheck, tests, and build
|
||||||
2. **npm** — `paperclipai` and the public workspace packages are published.
|
2. **npm** — `paperclipai` and public workspace packages are published
|
||||||
3. **GitHub** — the stable release gets a git tag and a GitHub Release.
|
3. **GitHub** — the stable release gets a git tag and GitHub Release
|
||||||
4. **Website / announcements** — the stable changelog is published externally and announced.
|
4. **Website / announcements** — the stable changelog is published externally and announced
|
||||||
|
|
||||||
Treat those as related but separate. npm can succeed while the GitHub Release is still pending. GitHub can be correct while the website changelog is stale. A maintainer release is done only when all four surfaces are handled.
|
A release is done only when all four surfaces are handled.
|
||||||
|
|
||||||
|
## Core Invariants
|
||||||
|
|
||||||
|
- Canary and stable for `X.Y.Z` must come from the same `release/X.Y.Z` branch.
|
||||||
|
- The release scripts must run from the matching `release/X.Y.Z` branch.
|
||||||
|
- Once `vX.Y.Z` exists locally, on GitHub, or on npm, that release train is frozen.
|
||||||
|
- Do not squash-merge or rebase-merge a release branch PR back to `master`.
|
||||||
|
- The stable changelog is always `releases/vX.Y.Z.md`. Never create canary changelog files.
|
||||||
|
|
||||||
|
The reason for the merge rule is simple: the tag must keep pointing at the exact published commit. Squash or rebase breaks that property.
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
### Canary release
|
### 1. Start the release train
|
||||||
|
|
||||||
Use this when you want an installable prerelease without changing `latest`.
|
Use this to compute the next version, create or resume the branch, create or resume a dedicated worktree, and push the branch to GitHub.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 0. Confirm master already has the CI-owned lockfile refresh merged
|
./scripts/release-start.sh patch
|
||||||
# If package manifests changed recently, wait for the refresh-lockfile PR first.
|
```
|
||||||
|
|
||||||
# 1. Preflight the canary candidate
|
That script:
|
||||||
./scripts/release-preflight.sh canary patch
|
|
||||||
|
|
||||||
# 2. Draft or update the stable changelog for the intended stable version
|
- fetches the release remote and tags
|
||||||
VERSION=0.2.8
|
- computes the next stable version from the latest `v*` tag
|
||||||
|
- creates or resumes `release/X.Y.Z`
|
||||||
|
- creates or resumes a dedicated worktree
|
||||||
|
- pushes the branch to the remote by default
|
||||||
|
- refuses to reuse a frozen release train
|
||||||
|
|
||||||
|
### 2. Draft the stable changelog
|
||||||
|
|
||||||
|
From the release worktree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSION=X.Y.Z
|
||||||
claude --print --output-format stream-json --verbose --dangerously-skip-permissions --model claude-opus-4-6 "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."
|
claude --print --output-format stream-json --verbose --dangerously-skip-permissions --model claude-opus-4-6 "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."
|
||||||
|
```
|
||||||
|
|
||||||
# 3. Preview the canary release
|
### 3. Verify and publish a canary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/release-preflight.sh canary patch
|
||||||
./scripts/release.sh patch --canary --dry-run
|
./scripts/release.sh patch --canary --dry-run
|
||||||
|
|
||||||
# 4. Publish the canary
|
|
||||||
./scripts/release.sh patch --canary
|
./scripts/release.sh patch --canary
|
||||||
|
|
||||||
# 5. Smoke test what users will actually install
|
|
||||||
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
|
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Users install with:
|
Users install canaries with:
|
||||||
|
|
||||||
|
```bash
|
||||||
npx paperclipai@canary onboard
|
npx paperclipai@canary onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
Result:
|
### 4. Publish stable
|
||||||
|
|
||||||
- npm gets a prerelease such as `1.2.3-canary.0` under dist-tag `canary`
|
|
||||||
- `latest` is unchanged
|
|
||||||
- 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
|
|
||||||
|
|
||||||
Use this only after the canary SHA is good enough to become the public default.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 0. Confirm master already has the CI-owned lockfile refresh merged
|
|
||||||
# If package manifests changed recently, wait for the refresh-lockfile PR first.
|
|
||||||
|
|
||||||
# 1. Start from the vetted commit
|
|
||||||
git checkout master
|
|
||||||
git pull
|
|
||||||
|
|
||||||
# 2. Preflight the stable candidate
|
|
||||||
./scripts/release-preflight.sh stable patch
|
./scripts/release-preflight.sh stable patch
|
||||||
|
|
||||||
# 3. Confirm the stable changelog exists
|
|
||||||
VERSION=0.2.8
|
|
||||||
ls "releases/v${VERSION}.md"
|
|
||||||
|
|
||||||
# 4. Preview the stable publish
|
|
||||||
./scripts/release.sh patch --dry-run
|
./scripts/release.sh patch --dry-run
|
||||||
|
|
||||||
# 5. Publish the stable release to npm and create the local release commit + tag
|
|
||||||
./scripts/release.sh patch
|
./scripts/release.sh patch
|
||||||
|
git push public-gh HEAD --follow-tags
|
||||||
# 6. Push the release commit and tag
|
|
||||||
git push public-gh HEAD:master --follow-tags
|
|
||||||
|
|
||||||
# 7. Create or update the GitHub Release from the pushed tag
|
|
||||||
./scripts/create-github-release.sh X.Y.Z
|
./scripts/create-github-release.sh X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Result:
|
Then open a PR from `release/X.Y.Z` to `master` and merge without squash or rebase.
|
||||||
|
|
||||||
- npm gets stable `X.Y.Z` under dist-tag `latest`
|
## Release Branches
|
||||||
- a local git commit and tag `vX.Y.Z` are created
|
|
||||||
- after push, GitHub gets the matching Release
|
|
||||||
- the website and announcement steps still need to be handled manually
|
|
||||||
|
|
||||||
### Emergency rollback
|
Paperclip uses one release branch per target stable version:
|
||||||
|
|
||||||
If `latest` is broken after publish, repoint it to the last known good stable version first, then work on the fix.
|
- `release/0.3.0`
|
||||||
|
- `release/0.3.1`
|
||||||
|
- `release/1.0.0`
|
||||||
|
|
||||||
|
Do not create separate per-canary branches like `canary/0.3.0-1`. A canary is just a prerelease snapshot of the same stable train.
|
||||||
|
|
||||||
|
## Script Entry Points
|
||||||
|
|
||||||
|
- [`scripts/release-start.sh`](../scripts/release-start.sh) — create or resume the release train branch/worktree
|
||||||
|
- [`scripts/release-preflight.sh`](../scripts/release-preflight.sh) — validate branch, version plan, git/npm state, and verification gate
|
||||||
|
- [`scripts/release.sh`](../scripts/release.sh) — publish canary or stable from the release branch
|
||||||
|
- [`scripts/create-github-release.sh`](../scripts/create-github-release.sh) — create or update the GitHub Release after pushing the tag
|
||||||
|
- [`scripts/rollback-latest.sh`](../scripts/rollback-latest.sh) — repoint `latest` to the last good stable version
|
||||||
|
|
||||||
|
## Detailed Workflow
|
||||||
|
|
||||||
|
### 1. Start or resume the release train
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Preview
|
./scripts/release-start.sh <patch|minor|major>
|
||||||
./scripts/rollback-latest.sh X.Y.Z --dry-run
|
|
||||||
|
|
||||||
# Roll back latest for every public package
|
|
||||||
./scripts/rollback-latest.sh X.Y.Z
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This does **not** unpublish anything. It only moves the `latest` dist-tag back to the last good stable release.
|
Useful options:
|
||||||
|
|
||||||
### Standalone onboarding smoke
|
|
||||||
|
|
||||||
You already have a script for isolated onboarding verification:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
HOST_PORT=3232 DATA_DIR=./data/release-smoke-canary PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
|
./scripts/release-start.sh patch --dry-run
|
||||||
HOST_PORT=3233 DATA_DIR=./data/release-smoke-stable PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
|
./scripts/release-start.sh minor --worktree-dir ../paperclip-release-0.4.0
|
||||||
|
./scripts/release-start.sh patch --no-push
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the best existing fit when you want:
|
The script is intentionally idempotent:
|
||||||
|
|
||||||
- a standalone Paperclip data dir
|
- if `release/X.Y.Z` already exists locally, it reuses it
|
||||||
- a dedicated host port
|
- if the branch already exists on the remote, it resumes it locally
|
||||||
- an end-to-end `npx paperclipai ... onboard` check
|
- if the branch is already checked out in another worktree, it points you there
|
||||||
|
- if `vX.Y.Z` already exists locally, remotely, or on npm, it refuses to reuse that train
|
||||||
|
|
||||||
In authenticated/private mode, the expected result is a full authenticated onboarding flow, including printing the bootstrap CEO invite once startup completes.
|
### 2. Write the stable changelog early
|
||||||
|
|
||||||
If you want to exercise onboarding from a fresh local checkout rather than npm, use:
|
Create or update:
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/clean-onboard-git.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
That is not a required release step every time, but it is a useful higher-confidence check when onboarding is the main risk area or when you need to verify what the current codebase does before publishing.
|
|
||||||
|
|
||||||
If you want to exercise onboarding from the current committed ref in your local repo, use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/clean-onboard-ref.sh
|
|
||||||
PAPERCLIP_PORT=3234 ./scripts/clean-onboard-ref.sh
|
|
||||||
./scripts/clean-onboard-ref.sh HEAD
|
|
||||||
```
|
|
||||||
|
|
||||||
This uses the current committed `HEAD` in a detached temp worktree. It does **not** include uncommitted local edits.
|
|
||||||
|
|
||||||
### GitHub Actions release
|
|
||||||
|
|
||||||
There is also a manual workflow at [`.github/workflows/release.yml`](../.github/workflows/release.yml). It is designed for npm trusted publishing via GitHub OIDC instead of long-lived npm tokens.
|
|
||||||
|
|
||||||
Use it from the Actions tab:
|
|
||||||
|
|
||||||
1. Choose `Release`
|
|
||||||
2. Choose `channel`: `canary` or `stable`
|
|
||||||
3. Choose `bump`: `patch`, `minor`, or `major`
|
|
||||||
4. Choose whether this is a `dry_run`
|
|
||||||
5. Run it from `master`
|
|
||||||
|
|
||||||
The workflow:
|
|
||||||
|
|
||||||
- reruns `typecheck`, `test:run`, and `build`
|
|
||||||
- gates publish behind the `npm-release` environment
|
|
||||||
- can publish canaries without touching `latest`
|
|
||||||
- can publish stable, push the release commit and tag, and create the GitHub Release
|
|
||||||
|
|
||||||
## Release Checklist
|
|
||||||
|
|
||||||
### Before any publish
|
|
||||||
|
|
||||||
- [ ] The working tree is clean, including untracked files
|
|
||||||
- [ ] The target branch and SHA are the ones you actually want to release
|
|
||||||
- [ ] If package manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master`
|
|
||||||
- [ ] The required verification gate passed on that exact SHA
|
|
||||||
- [ ] The bump type is correct for the user-visible impact
|
|
||||||
- [ ] The stable changelog file exists or is ready to be written at `releases/vX.Y.Z.md`
|
|
||||||
- [ ] You know which previous stable version you would roll back to if needed
|
|
||||||
|
|
||||||
### Before a canary
|
|
||||||
|
|
||||||
- [ ] You are intentionally testing something that should be installable before it becomes default
|
|
||||||
- [ ] You are comfortable with users installing it via `npx paperclipai@canary onboard`
|
|
||||||
- [ ] You understand that each canary is a new immutable npm version such as `1.2.3-canary.1`
|
|
||||||
|
|
||||||
### Before a stable
|
|
||||||
|
|
||||||
- [ ] The candidate has already passed smoke testing
|
|
||||||
- [ ] The changelog should be the stable version only, for example `v1.2.3`
|
|
||||||
- [ ] You are ready to push the release commit and tag immediately after npm publish
|
|
||||||
- [ ] You are ready to create the GitHub Release immediately after the push
|
|
||||||
- [ ] You have a post-release website / announcement plan
|
|
||||||
|
|
||||||
### After a stable
|
|
||||||
|
|
||||||
- [ ] `npm view paperclipai@latest version` matches the new stable version
|
|
||||||
- [ ] The git tag exists on GitHub
|
|
||||||
- [ ] The GitHub Release exists and uses `releases/vX.Y.Z.md`
|
|
||||||
- [ ] The website changelog is updated
|
|
||||||
- [ ] Any announcement copy matches the shipped release, not the canary
|
|
||||||
|
|
||||||
## Verification Gate
|
|
||||||
|
|
||||||
The repository standard is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm -r typecheck
|
|
||||||
pnpm test:run
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
This matches [`.github/workflows/pr-verify.yml`](../.github/workflows/pr-verify.yml). Run it before claiming a release candidate is ready.
|
|
||||||
|
|
||||||
The release workflow at [`.github/workflows/release.yml`](../.github/workflows/release.yml) installs with `pnpm install --frozen-lockfile`. That is intentional. Releases must use the exact dependency graph already committed on `master`; if manifests changed and the CI-owned lockfile refresh has not landed yet, the release should fail until that prerequisite is merged.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Stable releases use normal semver:
|
|
||||||
|
|
||||||
- `patch` for bug fixes
|
|
||||||
- `minor` for additive features, endpoints, and additive migrations
|
|
||||||
- `major` for destructive migrations, removed APIs, or other breaking behavior
|
|
||||||
|
|
||||||
### Canary versions
|
|
||||||
|
|
||||||
Canaries are semver prereleases of the **intended stable version**:
|
|
||||||
|
|
||||||
- `1.2.3-canary.0`
|
|
||||||
- `1.2.3-canary.1`
|
|
||||||
- `1.2.3-canary.2`
|
|
||||||
|
|
||||||
That gives you three useful properties:
|
|
||||||
|
|
||||||
1. Users can install the prerelease explicitly with `@canary`
|
|
||||||
2. `latest` stays safe
|
|
||||||
3. The stable changelog can remain just `v1.2.3`
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
- `releases/vX.Y.Z.md`
|
- `releases/vX.Y.Z.md`
|
||||||
|
|
||||||
@@ -268,14 +146,13 @@ Recommended structure:
|
|||||||
- `Improvements`
|
- `Improvements`
|
||||||
- `Fixes`
|
- `Fixes`
|
||||||
- `Upgrade Guide` when needed
|
- `Upgrade Guide` when needed
|
||||||
|
- `Contributors` — @-mention every contributor by GitHub username (no emails)
|
||||||
|
|
||||||
Package-level `CHANGELOG.md` files are generated as part of the release mechanics. They are not the main release narrative.
|
Package-level `CHANGELOG.md` files are generated as part of the release mechanics. They are not the main release narrative.
|
||||||
|
|
||||||
## Detailed Workflow
|
### 3. Run release preflight
|
||||||
|
|
||||||
### 1. Decide the bump
|
From the `release/X.Y.Z` worktree:
|
||||||
|
|
||||||
Run preflight first:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/release-preflight.sh canary <patch|minor|major>
|
./scripts/release-preflight.sh canary <patch|minor|major>
|
||||||
@@ -283,70 +160,54 @@ Run preflight first:
|
|||||||
./scripts/release-preflight.sh stable <patch|minor|major>
|
./scripts/release-preflight.sh stable <patch|minor|major>
|
||||||
```
|
```
|
||||||
|
|
||||||
That command:
|
The preflight script now checks all of the following before it runs the verification gate:
|
||||||
|
|
||||||
- verifies the worktree is clean, including untracked files
|
- the worktree is clean, including untracked files
|
||||||
- shows the last stable tag and computed next versions
|
- the current branch matches the computed `release/X.Y.Z`
|
||||||
- shows the commit range since the last stable tag
|
- the release train is not frozen
|
||||||
- highlights migration and breaking-change signals
|
- the target version is still free on npm
|
||||||
- runs `pnpm -r typecheck`, `pnpm test:run`, and `pnpm build`
|
- the target tag does not already exist locally or remotely
|
||||||
|
- whether the remote release branch already exists
|
||||||
|
- whether `releases/vX.Y.Z.md` is present
|
||||||
|
|
||||||
If you want the raw inputs separately, review the range since the last stable tag:
|
Then it runs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LAST_TAG=$(git tag --list 'v*' --sort=-version:refname | head -1)
|
pnpm -r typecheck
|
||||||
git log "${LAST_TAG}..HEAD" --oneline --no-merges
|
pnpm test:run
|
||||||
git diff --name-only "${LAST_TAG}..HEAD" -- packages/db/src/migrations/
|
pnpm build
|
||||||
git diff "${LAST_TAG}..HEAD" -- packages/db/src/schema/
|
|
||||||
git log "${LAST_TAG}..HEAD" --format="%s" | grep -E 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the higher bump if there is any doubt.
|
### 4. Publish one or more canaries
|
||||||
|
|
||||||
### 2. Write the stable changelog first
|
|
||||||
|
|
||||||
Create or update:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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.
|
|
||||||
|
|
||||||
### 3. Publish one or more canaries
|
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
./scripts/release.sh <patch|minor|major> --canary --dry-run
|
||||||
./scripts/release.sh <patch|minor|major> --canary
|
./scripts/release.sh <patch|minor|major> --canary
|
||||||
```
|
```
|
||||||
|
|
||||||
What the script does:
|
Result:
|
||||||
|
|
||||||
1. Verifies the working tree is clean
|
- npm gets a prerelease such as `1.2.3-canary.0` under dist-tag `canary`
|
||||||
2. Computes the intended stable version from the last stable tag
|
- `latest` is unchanged
|
||||||
3. Computes the next canary ordinal from npm
|
- no git tag is created
|
||||||
4. Versions the public packages to `X.Y.Z-canary.N`
|
- no GitHub Release is created
|
||||||
5. Builds the workspace and publishable CLI
|
- the worktree returns to clean after the script finishes
|
||||||
6. Publishes to npm under dist-tag `canary`
|
|
||||||
7. Cleans up the temporary versioning state so your branch returns to clean
|
|
||||||
|
|
||||||
This means the script is safe to repeat as many times as needed while iterating:
|
Guardrails:
|
||||||
|
|
||||||
- `1.2.3-canary.0`
|
- the script refuses to run from the wrong branch
|
||||||
- `1.2.3-canary.1`
|
- the script refuses to publish from a frozen train
|
||||||
- `1.2.3-canary.2`
|
- the canary is always derived from the next stable version
|
||||||
|
- if the stable notes file is missing, the script warns before you forget it
|
||||||
|
|
||||||
The target stable release can still remain `1.2.3`.
|
Concrete example:
|
||||||
|
|
||||||
Guardrail:
|
- if the latest stable is `0.2.7`, a patch canary targets `0.2.8-canary.0`
|
||||||
|
- `0.2.7-canary.N` is invalid because `0.2.7` is already stable
|
||||||
|
|
||||||
- the canary is always derived from the **next stable version**
|
### 5. Smoke test the canary
|
||||||
- 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:
|
Run the actual install path in Docker:
|
||||||
|
|
||||||
@@ -361,165 +222,198 @@ HOST_PORT=3232 DATA_DIR=./data/release-smoke-canary PAPERCLIPAI_VERSION=canary .
|
|||||||
HOST_PORT=3233 DATA_DIR=./data/release-smoke-stable PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
|
HOST_PORT=3233 DATA_DIR=./data/release-smoke-stable PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to smoke onboarding from the current codebase rather than npm, run:
|
If you want to exercise onboarding from the current committed ref instead of npm, use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/clean-onboard-git.sh
|
|
||||||
./scripts/clean-onboard-ref.sh
|
./scripts/clean-onboard-ref.sh
|
||||||
|
PAPERCLIP_PORT=3234 ./scripts/clean-onboard-ref.sh
|
||||||
|
./scripts/clean-onboard-ref.sh HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
Minimum checks:
|
Minimum checks:
|
||||||
|
|
||||||
- [ ] `npx paperclipai@canary onboard` installs
|
- `npx paperclipai@canary onboard` installs
|
||||||
- [ ] onboarding completes without crashes
|
- onboarding completes without crashes
|
||||||
- [ ] the server boots
|
- the server boots
|
||||||
- [ ] the UI loads
|
- the UI loads
|
||||||
- [ ] basic company creation and dashboard load work
|
- basic company creation and dashboard load work
|
||||||
|
|
||||||
### 5. Publish stable from the vetted commit
|
If smoke testing fails:
|
||||||
|
|
||||||
Once the candidate SHA is good, run the stable flow on that exact commit:
|
1. stop the stable release
|
||||||
|
2. fix the issue on the same `release/X.Y.Z` branch
|
||||||
|
3. publish another canary
|
||||||
|
4. rerun smoke testing
|
||||||
|
|
||||||
|
### 6. Publish stable from the same release branch
|
||||||
|
|
||||||
|
Once the branch head is vetted, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
./scripts/release.sh <patch|minor|major> --dry-run
|
||||||
./scripts/release.sh <patch|minor|major>
|
./scripts/release.sh <patch|minor|major>
|
||||||
```
|
```
|
||||||
|
|
||||||
What the script does:
|
Stable publish:
|
||||||
|
|
||||||
1. Verifies the working tree is clean
|
- publishes `X.Y.Z` to npm under `latest`
|
||||||
2. Versions the public packages to the stable semver
|
- creates the local release commit
|
||||||
3. Builds the workspace and CLI publish bundle
|
- creates the local tag `vX.Y.Z`
|
||||||
4. Publishes to npm under `latest`
|
|
||||||
5. Restores temporary publish artifacts
|
|
||||||
6. Creates the local release commit and git tag
|
|
||||||
|
|
||||||
What it does **not** do:
|
Stable publish refuses to proceed if:
|
||||||
|
|
||||||
- it does not push for you
|
- the current branch is not `release/X.Y.Z`
|
||||||
- it does not update the website
|
- the remote release branch does not exist yet
|
||||||
- it does not announce the release for you
|
- the stable notes file is missing
|
||||||
|
- the target tag already exists locally or remotely
|
||||||
|
- the stable version already exists on npm
|
||||||
|
|
||||||
### 6. Push the release and create the GitHub Release
|
Those checks intentionally freeze the train after stable publish.
|
||||||
|
|
||||||
After a stable publish succeeds:
|
### 7. Push the stable branch commit and tag
|
||||||
|
|
||||||
|
After stable publish succeeds:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push public-gh HEAD:master --follow-tags
|
git push public-gh HEAD --follow-tags
|
||||||
./scripts/create-github-release.sh X.Y.Z
|
./scripts/create-github-release.sh X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
The GitHub release notes come from:
|
The GitHub Release notes come from:
|
||||||
|
|
||||||
- `releases/vX.Y.Z.md`
|
- `releases/vX.Y.Z.md`
|
||||||
|
|
||||||
### 7. Complete the external surfaces
|
### 8. Merge the release branch back to `master`
|
||||||
|
|
||||||
|
Open a PR:
|
||||||
|
|
||||||
|
- base: `master`
|
||||||
|
- head: `release/X.Y.Z`
|
||||||
|
|
||||||
|
Merge rule:
|
||||||
|
|
||||||
|
- allowed: merge commit or fast-forward
|
||||||
|
- forbidden: squash merge
|
||||||
|
- forbidden: rebase merge
|
||||||
|
|
||||||
|
Post-merge verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch public-gh --tags
|
||||||
|
git merge-base --is-ancestor "vX.Y.Z" "public-gh/master"
|
||||||
|
```
|
||||||
|
|
||||||
|
That command must succeed. If it fails, the published tagged commit is not reachable from `master`, which means the merge strategy was wrong.
|
||||||
|
|
||||||
|
### 9. Finish the external surfaces
|
||||||
|
|
||||||
After GitHub is correct:
|
After GitHub is correct:
|
||||||
|
|
||||||
- publish the changelog on the website
|
- publish the changelog on the website
|
||||||
- write the announcement copy
|
- write and send the announcement copy
|
||||||
- ensure public docs and install guidance point to the stable version
|
- ensure public docs and install guidance point to the stable version
|
||||||
|
|
||||||
## GitHub Actions and npm Trusted Publishing
|
## GitHub Actions Release
|
||||||
|
|
||||||
If you want GitHub to own the actual npm publish, use [`.github/workflows/release.yml`](../.github/workflows/release.yml) together with npm trusted publishing.
|
There is also a manual workflow at [`.github/workflows/release.yml`](../.github/workflows/release.yml).
|
||||||
|
|
||||||
Recommended setup:
|
Use it from the Actions tab on the relevant `release/X.Y.Z` branch:
|
||||||
|
|
||||||
1. Configure the GitHub Actions workflow as a trusted publisher for **every public package** on npm
|
1. Choose `Release`
|
||||||
2. Use the `npm-release` GitHub environment with required reviewers
|
2. Choose `channel`: `canary` or `stable`
|
||||||
3. Run stable publishes from `master` only
|
3. Choose `bump`: `patch`, `minor`, or `major`
|
||||||
4. Keep the workflow manual via `workflow_dispatch`
|
4. Choose whether this is a `dry_run`
|
||||||
|
5. Run it from the release branch, not from `master`
|
||||||
|
|
||||||
Why this is the right shape:
|
The workflow:
|
||||||
|
|
||||||
- no long-lived npm token needs to live in GitHub secrets
|
- reruns `typecheck`, `test:run`, and `build`
|
||||||
- reviewers can approve the publish step at the environment gate
|
- gates publish behind the `npm-release` environment
|
||||||
- the workflow reruns verification on the release SHA before publish
|
- can publish canaries without touching `latest`
|
||||||
- stable and canary use the same mechanics
|
- can publish stable, push the stable branch commit and tag, and create the GitHub Release
|
||||||
|
|
||||||
|
It does not merge the release branch back to `master` for you.
|
||||||
|
|
||||||
|
## Release Checklist
|
||||||
|
|
||||||
|
### Before any publish
|
||||||
|
|
||||||
|
- [ ] The release train exists on `release/X.Y.Z`
|
||||||
|
- [ ] The working tree is clean, including untracked files
|
||||||
|
- [ ] If package manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master` before the train is cut
|
||||||
|
- [ ] The required verification gate passed on the exact branch head you want to publish
|
||||||
|
- [ ] The bump type is correct for the user-visible impact
|
||||||
|
- [ ] The stable changelog file exists or is ready at `releases/vX.Y.Z.md`
|
||||||
|
- [ ] You know which previous stable version you would roll back to if needed
|
||||||
|
|
||||||
|
### Before a stable
|
||||||
|
|
||||||
|
- [ ] The candidate has already passed smoke testing
|
||||||
|
- [ ] The remote `release/X.Y.Z` branch exists
|
||||||
|
- [ ] You are ready to push the stable branch commit and tag immediately after npm publish
|
||||||
|
- [ ] You are ready to create the GitHub Release immediately after the push
|
||||||
|
- [ ] You are ready to open the PR back to `master`
|
||||||
|
|
||||||
|
### After a stable
|
||||||
|
|
||||||
|
- [ ] `npm view paperclipai@latest version` matches the new stable version
|
||||||
|
- [ ] The git tag exists on GitHub
|
||||||
|
- [ ] The GitHub Release exists and uses `releases/vX.Y.Z.md`
|
||||||
|
- [ ] `vX.Y.Z` is reachable from `master`
|
||||||
|
- [ ] The website changelog is updated
|
||||||
|
- [ ] Announcement copy matches the stable release, not the canary
|
||||||
|
|
||||||
## Failure Playbooks
|
## Failure Playbooks
|
||||||
|
|
||||||
### If the canary fails before publish
|
|
||||||
|
|
||||||
Nothing shipped. Fix the code and rerun the canary workflow.
|
|
||||||
|
|
||||||
### If the canary publishes but the smoke test fails
|
### If the canary publishes but the smoke test fails
|
||||||
|
|
||||||
Do **not** publish stable.
|
Do not publish stable.
|
||||||
|
|
||||||
Instead:
|
Instead:
|
||||||
|
|
||||||
1. Fix the issue
|
1. fix the issue on `release/X.Y.Z`
|
||||||
2. Publish another canary
|
2. publish another canary
|
||||||
3. Re-run smoke testing
|
3. rerun smoke testing
|
||||||
|
|
||||||
The canary version number will increase, but the stable target version can remain the same.
|
### If stable npm publish succeeds but push or GitHub release creation fails
|
||||||
|
|
||||||
### If the stable npm publish succeeds but push fails
|
|
||||||
|
|
||||||
This is a partial release. npm is already live.
|
This is a partial release. npm is already live.
|
||||||
|
|
||||||
Do this immediately:
|
Do this immediately:
|
||||||
|
|
||||||
1. Fix the git issue
|
1. fix the git or GitHub issue from the same checkout
|
||||||
2. Push the release commit and tag from the same checkout
|
2. push the stable branch commit and tag
|
||||||
3. Create the GitHub Release
|
3. create the GitHub Release
|
||||||
|
|
||||||
Do **not** publish the same version again.
|
Do not republish the same version.
|
||||||
|
|
||||||
### If the stable release is bad after `latest` moves
|
### If `latest` is broken after stable publish
|
||||||
|
|
||||||
Use the rollback script first:
|
Preview:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/rollback-latest.sh <last-good-version>
|
./scripts/rollback-latest.sh X.Y.Z --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Then:
|
Roll back:
|
||||||
|
|
||||||
1. open an incident note or maintainer comment
|
```bash
|
||||||
2. fix forward on a new patch release
|
./scripts/rollback-latest.sh X.Y.Z
|
||||||
3. update the changelog / release notes if the user-facing guidance changed
|
```
|
||||||
|
|
||||||
### If the GitHub Release is wrong
|
This does not unpublish anything. It only moves the `latest` dist-tag back to the last good stable release.
|
||||||
|
|
||||||
Edit it by re-running:
|
Then fix forward with a new patch release.
|
||||||
|
|
||||||
|
### If the GitHub Release notes are wrong
|
||||||
|
|
||||||
|
Re-run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/create-github-release.sh X.Y.Z
|
./scripts/create-github-release.sh X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
This updates the release notes if the GitHub Release already exists.
|
If the release already exists, the script updates it.
|
||||||
|
|
||||||
### If the website changelog is wrong
|
|
||||||
|
|
||||||
Fix the website independently. Do not republish npm just to repair the website surface.
|
|
||||||
|
|
||||||
## Rollback Strategy
|
|
||||||
|
|
||||||
The default rollback strategy is **dist-tag rollback, then fix forward**.
|
|
||||||
|
|
||||||
Why:
|
|
||||||
|
|
||||||
- npm versions are immutable
|
|
||||||
- users need `npx paperclipai onboard` to recover quickly
|
|
||||||
- moving `latest` back is faster and safer than trying to delete history
|
|
||||||
|
|
||||||
Rollback procedure:
|
|
||||||
|
|
||||||
1. identify the last known good stable version
|
|
||||||
2. run `./scripts/rollback-latest.sh <version>`
|
|
||||||
3. verify `npm view paperclipai@latest version`
|
|
||||||
4. fix forward with a new stable release
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## Related Docs
|
## Related Docs
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"db:backup": "./scripts/backup-db.sh",
|
"db:backup": "./scripts/backup-db.sh",
|
||||||
"paperclipai": "node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts",
|
"paperclipai": "node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts",
|
||||||
"build:npm": "./scripts/build-npm.sh",
|
"build:npm": "./scripts/build-npm.sh",
|
||||||
|
"release:start": "./scripts/release-start.sh",
|
||||||
"release": "./scripts/release.sh",
|
"release": "./scripts/release.sh",
|
||||||
"release:preflight": "./scripts/release-preflight.sh",
|
"release:preflight": "./scripts/release-preflight.sh",
|
||||||
"release:github": "./scripts/create-github-release.sh",
|
"release:github": "./scripts/create-github-release.sh",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **New adapters: Cursor, OpenCode, and Pi** — Paperclip now supports three additional local coding agents. Cursor and OpenCode integrate as first-class adapters with model discovery, run-log streaming, and skill injection. Pi adds a local RPC mode with cost tracking. All three appear in the onboarding wizard alongside Claude Code and Codex.
|
- **New adapters: Cursor, OpenCode, and Pi** — Paperclip now supports three additional local coding agents. Cursor and OpenCode integrate as first-class adapters with model discovery, run-log streaming, and skill injection. Pi adds a local RPC mode with cost tracking. All three appear in the onboarding wizard alongside Claude Code and Codex. ([#62](https://github.com/paperclipai/paperclip/pull/62), [#141](https://github.com/paperclipai/paperclip/pull/141), [#240](https://github.com/paperclipai/paperclip/pull/240), [#183](https://github.com/paperclipai/paperclip/pull/183), @aaaaron, @Konan69, @richardanaya)
|
||||||
- **OpenClaw gateway adapter** — A new gateway-only OpenClaw flow replaces the legacy adapter. It uses strict SSE streaming, supports device-key pairing, and handles invite-based onboarding with join-token validation.
|
- **OpenClaw gateway adapter** — A new gateway-only OpenClaw flow replaces the legacy adapter. It uses strict SSE streaming, supports device-key pairing, and handles invite-based onboarding with join-token validation. ([#270](https://github.com/paperclipai/paperclip/pull/270))
|
||||||
- **Inbox and unread semantics** — Issues now track per-user read state. Unread indicators appear in the inbox, dashboard, and browser tab (blue dot). The inbox badge includes join requests and approvals, and inbox ordering is alert-focused.
|
- **Inbox and unread semantics** — Issues now track per-user read state. Unread indicators appear in the inbox, dashboard, and browser tab (blue dot). The inbox badge includes join requests and approvals, and inbox ordering is alert-focused. ([#196](https://github.com/paperclipai/paperclip/pull/196), @hougangdev)
|
||||||
- **PWA support** — The UI ships as an installable Progressive Web App with a service worker and enhanced manifest. The service worker uses a network-first strategy to prevent stale content.
|
- **PWA support** — The UI ships as an installable Progressive Web App with a service worker and enhanced manifest. The service worker uses a network-first strategy to prevent stale content.
|
||||||
- **Agent creation wizard** — A new choice modal and full-page configuration flow make it easier to add agents. The sidebar AGENTS header now has a quick-add button.
|
- **Agent creation wizard** — A new choice modal and full-page configuration flow make it easier to add agents. The sidebar AGENTS header now has a quick-add button.
|
||||||
|
|
||||||
@@ -19,29 +19,35 @@
|
|||||||
- **Project status clickable** — The status chip in the project properties pane is now clickable for quick updates.
|
- **Project status clickable** — The status chip in the project properties pane is now clickable for quick updates.
|
||||||
- **Scroll-to-bottom button** — Issue detail and run pages show a floating scroll-to-bottom button when you scroll up.
|
- **Scroll-to-bottom button** — Issue detail and run pages show a floating scroll-to-bottom button when you scroll up.
|
||||||
- **Database backup CLI** — `paperclipai db:backup` lets you snapshot the database on demand, with optional automatic scheduling.
|
- **Database backup CLI** — `paperclipai db:backup` lets you snapshot the database on demand, with optional automatic scheduling.
|
||||||
- **Disable sign-up** — A new `auth.disableSignUp` config option (and `AUTH_DISABLE_SIGNUP` env var) lets operators lock registration.
|
- **Disable sign-up** — A new `auth.disableSignUp` config option (and `AUTH_DISABLE_SIGNUP` env var) lets operators lock registration. ([#279](https://github.com/paperclipai/paperclip/pull/279), @JasonOA888)
|
||||||
- **Deduplicated shortnames** — Agent and project shortnames are now auto-deduplicated on create and update instead of rejecting duplicates.
|
- **Deduplicated shortnames** — Agent and project shortnames are now auto-deduplicated on create and update instead of rejecting duplicates. ([#264](https://github.com/paperclipai/paperclip/pull/264), @mvanhorn)
|
||||||
- **Human-readable role labels** — The agent list and properties pane show friendly role names.
|
- **Human-readable role labels** — The agent list and properties pane show friendly role names. ([#263](https://github.com/paperclipai/paperclip/pull/263), @mvanhorn)
|
||||||
- **Assignee picker sorting** — Recent selections appear first, then alphabetical.
|
- **Assignee picker sorting** — Recent selections appear first, then alphabetical.
|
||||||
- **Mobile layout polish** — Unified GitHub-style issue rows across issues, inbox, and dashboard. Improved popover scrolling, command palette centering, and property toggles on mobile.
|
- **Mobile layout polish** — Unified GitHub-style issue rows across issues, inbox, and dashboard. Improved popover scrolling, command palette centering, and property toggles on mobile. ([#118](https://github.com/paperclipai/paperclip/pull/118), @MumuTW)
|
||||||
- **Invite UX improvements** — Invite links auto-copy to clipboard, snippet-only flow in settings, 10-minute invite TTL, and clearer network-host guidance.
|
- **Invite UX improvements** — Invite links auto-copy to clipboard, snippet-only flow in settings, 10-minute invite TTL, and clearer network-host guidance.
|
||||||
- **Permalink anchors on comments** — Each comment has a stable anchor link and a GET-by-ID API endpoint.
|
- **Permalink anchors on comments** — Each comment has a stable anchor link and a GET-by-ID API endpoint.
|
||||||
- **Docker deployment hardening** — Authenticated deployment mode by default, named data volume, `PAPERCLIP_PUBLIC_URL` and `PAPERCLIP_ALLOWED_HOSTNAMES` exposed in compose files, health-check DB wait, and Node 24 base image.
|
- **Docker deployment hardening** — Authenticated deployment mode by default, named data volume, `PAPERCLIP_PUBLIC_URL` and `PAPERCLIP_ALLOWED_HOSTNAMES` exposed in compose files, health-check DB wait, and Node 24 base image. ([#400](https://github.com/paperclipai/paperclip/pull/400), [#283](https://github.com/paperclipai/paperclip/pull/283), [#284](https://github.com/paperclipai/paperclip/pull/284), @AiMagic5000, @mingfang)
|
||||||
- **Updated model lists** — Added `claude-sonnet-4-6`, `claude-haiku-4-6`, and `gpt-5.4` to adapter model constants.
|
- **Updated model lists** — Added `claude-sonnet-4-6`, `claude-haiku-4-6`, and `gpt-5.4` to adapter model constants. ([#293](https://github.com/paperclipai/paperclip/pull/293), [#110](https://github.com/paperclipai/paperclip/pull/110), @cpfarhood, @artokun)
|
||||||
- **Playwright e2e tests** — New end-to-end test suite covering the onboarding wizard flow.
|
- **Playwright e2e tests** — New end-to-end test suite covering the onboarding wizard flow.
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
- **Secret redaction in run logs** — Env vars sourced from secrets are now redacted by provenance, with consistent `secretKeys` tracking.
|
- **Secret redaction in run logs** — Env vars sourced from secrets are now redacted by provenance, with consistent `secretKeys` tracking. ([#261](https://github.com/paperclipai/paperclip/pull/261), @mvanhorn)
|
||||||
- **SPA catch-all 500s** — The server serves cached `index.html` in the catch-all route and uses `root` in `sendFile`, preventing 500 errors on dotfile paths and SPA refreshes.
|
- **SPA catch-all 500s** — The server serves cached `index.html` in the catch-all route and uses `root` in `sendFile`, preventing 500 errors on dotfile paths and SPA refreshes. ([#269](https://github.com/paperclipai/paperclip/pull/269), [#78](https://github.com/paperclipai/paperclip/pull/78), @mvanhorn, @MumuTW)
|
||||||
- **Unmatched API routes return 404 JSON** — Previously fell through to the SPA handler.
|
- **Unmatched API routes return 404 JSON** — Previously fell through to the SPA handler. ([#269](https://github.com/paperclipai/paperclip/pull/269), @mvanhorn)
|
||||||
- **Agent wake logic** — Agents wake when issues move out of backlog, skip self-wake on own comments, and skip wakeup for backlog-status changes. Pending-approval agents are excluded from heartbeat timers.
|
- **Agent wake logic** — Agents wake when issues move out of backlog, skip self-wake on own comments, and skip wakeup for backlog-status changes. Pending-approval agents are excluded from heartbeat timers. ([#159](https://github.com/paperclipai/paperclip/pull/159), [#154](https://github.com/paperclipai/paperclip/pull/154), [#267](https://github.com/paperclipai/paperclip/pull/267), [#72](https://github.com/paperclipai/paperclip/pull/72), @Logesh-waran2003, @cschneid, @mvanhorn, @STRML)
|
||||||
- **Run log fd leak** — Fixed a file-descriptor leak in log append that caused `spawn EBADF` errors.
|
- **Run log fd leak** — Fixed a file-descriptor leak in log append that caused `spawn EBADF` errors. ([#266](https://github.com/paperclipai/paperclip/pull/266), @mvanhorn)
|
||||||
- **500 error logging** — Error logs now include the actual error message and request context instead of generic pino-http output.
|
- **500 error logging** — Error logs now include the actual error message and request context instead of generic pino-http output.
|
||||||
- **Boolean env parsing** — `parseBooleanFromEnv` no longer silently treats common truthy values as false.
|
- **Boolean env parsing** — `parseBooleanFromEnv` no longer silently treats common truthy values as false. ([#91](https://github.com/paperclipai/paperclip/pull/91), @zvictor)
|
||||||
- **Onboarding env defaults** — `onboard` now correctly derives secrets from env vars and reports ignored exposure settings in `local_trusted` mode.
|
- **Onboarding env defaults** — `onboard` now correctly derives secrets from env vars and reports ignored exposure settings in `local_trusted` mode. ([#91](https://github.com/paperclipai/paperclip/pull/91), @zvictor)
|
||||||
- **Windows path compatibility** — Migration paths use `fileURLToPath` for Windows-safe resolution.
|
- **Windows path compatibility** — Migration paths use `fileURLToPath` for Windows-safe resolution. ([#265](https://github.com/paperclipai/paperclip/pull/265), [#413](https://github.com/paperclipai/paperclip/pull/413), @mvanhorn, @online5880)
|
||||||
- **Secure cookies on HTTP** — Disabled secure cookie flag for plain HTTP deployments to prevent auth failures.
|
- **Secure cookies on HTTP** — Disabled secure cookie flag for plain HTTP deployments to prevent auth failures. ([#376](https://github.com/paperclipai/paperclip/pull/376), @dalestubblefield)
|
||||||
- **URL encoding** — `buildUrl` splits path and query to prevent `%3F` encoding issues.
|
- **URL encoding** — `buildUrl` splits path and query to prevent `%3F` encoding issues. ([#260](https://github.com/paperclipai/paperclip/pull/260), @mvanhorn)
|
||||||
- **Auth trusted origins** — Effective trusted origins and allowed hostnames are now applied correctly in public mode.
|
- **Auth trusted origins** — Effective trusted origins and allowed hostnames are now applied correctly in public mode. ([#99](https://github.com/paperclipai/paperclip/pull/99), @zvictor)
|
||||||
- **UI stability** — Fixed blank screen when prompt templates are emptied, search URL sync causing re-renders, issue title overflow in inbox, and sidebar badge counts including approvals.
|
- **UI stability** — Fixed blank screen when prompt templates are emptied, search URL sync causing re-renders, issue title overflow in inbox, and sidebar badge counts including approvals. ([#262](https://github.com/paperclipai/paperclip/pull/262), [#196](https://github.com/paperclipai/paperclip/pull/196), [#423](https://github.com/paperclipai/paperclip/pull/423), @mvanhorn, @hougangdev, @RememberV)
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thank you to everyone who contributed to this release!
|
||||||
|
|
||||||
|
@aaaaron, @AiMagic5000, @artokun, @cpfarhood, @cschneid, @dalestubblefield, @Dotta, @eltociear, @fahmmin, @gsxdsm, @hougangdev, @JasonOA888, @Konan69, @Logesh-waran2003, @mingfang, @MumuTW, @mvanhorn, @numman-ali, @online5880, @RememberV, @richardanaya, @STRML, @tylerwince, @zvictor
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
PUBLISH_REMOTE="${PUBLISH_REMOTE:-public-gh}"
|
# shellcheck source=./release-lib.sh
|
||||||
|
. "$REPO_ROOT/scripts/release-lib.sh"
|
||||||
|
|
||||||
dry_run=false
|
dry_run=false
|
||||||
version=""
|
version=""
|
||||||
@@ -17,7 +18,7 @@ Examples:
|
|||||||
./scripts/create-github-release.sh 1.2.3 --dry-run
|
./scripts/create-github-release.sh 1.2.3 --dry-run
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Run this after pushing the release commit and tag.
|
- Run this after pushing the stable release branch and tag.
|
||||||
- If the release already exists, this script updates its title and notes.
|
- If the release already exists, this script updates its title and notes.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,7 @@ fi
|
|||||||
|
|
||||||
tag="v$version"
|
tag="v$version"
|
||||||
notes_file="$REPO_ROOT/releases/${tag}.md"
|
notes_file="$REPO_ROOT/releases/${tag}.md"
|
||||||
|
PUBLISH_REMOTE="$(resolve_release_remote)"
|
||||||
|
|
||||||
if ! command -v gh >/dev/null 2>&1; then
|
if ! command -v gh >/dev/null 2>&1; then
|
||||||
echo "Error: gh CLI is required to create GitHub releases." >&2
|
echo "Error: gh CLI is required to create GitHub releases." >&2
|
||||||
|
|||||||
222
scripts/release-lib.sh
Normal file
222
scripts/release-lib.sh
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_info() {
|
||||||
|
echo "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_warn() {
|
||||||
|
echo "Warning: $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
release_fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
git_remote_exists() {
|
||||||
|
git -C "$REPO_ROOT" remote get-url "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_release_remote() {
|
||||||
|
local remote="${RELEASE_REMOTE:-${PUBLISH_REMOTE:-}}"
|
||||||
|
|
||||||
|
if [ -n "$remote" ]; then
|
||||||
|
git_remote_exists "$remote" || release_fail "git remote '$remote' does not exist."
|
||||||
|
printf '%s\n' "$remote"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git_remote_exists public-gh; then
|
||||||
|
printf 'public-gh\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git_remote_exists origin; then
|
||||||
|
printf 'origin\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_fail "no git remote found. Configure RELEASE_REMOTE or PUBLISH_REMOTE."
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_release_remote() {
|
||||||
|
git -C "$REPO_ROOT" fetch "$1" --prune --tags
|
||||||
|
}
|
||||||
|
|
||||||
|
get_last_stable_tag() {
|
||||||
|
git -C "$REPO_ROOT" tag --list 'v*' --sort=-version:refname | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_current_stable_version() {
|
||||||
|
local tag
|
||||||
|
tag="$(get_last_stable_tag)"
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
printf '0.0.0\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' "${tag#v}"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
release_branch_name() {
|
||||||
|
printf 'release/%s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_notes_file() {
|
||||||
|
printf '%s/releases/v%s.md\n' "$REPO_ROOT" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
default_release_worktree_path() {
|
||||||
|
local version="$1"
|
||||||
|
local parent_dir
|
||||||
|
local repo_name
|
||||||
|
|
||||||
|
parent_dir="$(cd "$REPO_ROOT/.." && pwd)"
|
||||||
|
repo_name="$(basename "$REPO_ROOT")"
|
||||||
|
printf '%s/%s-release-%s\n' "$parent_dir" "$repo_name" "$version"
|
||||||
|
}
|
||||||
|
|
||||||
|
git_current_branch() {
|
||||||
|
git -C "$REPO_ROOT" symbolic-ref --quiet --short HEAD 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
git_local_branch_exists() {
|
||||||
|
git -C "$REPO_ROOT" show-ref --verify --quiet "refs/heads/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
git_remote_branch_exists() {
|
||||||
|
git -C "$REPO_ROOT" ls-remote --exit-code --heads "$2" "refs/heads/$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
git_local_tag_exists() {
|
||||||
|
git -C "$REPO_ROOT" show-ref --verify --quiet "refs/tags/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
git_remote_tag_exists() {
|
||||||
|
git -C "$REPO_ROOT" ls-remote --exit-code --tags "$2" "refs/tags/$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
npm_version_exists() {
|
||||||
|
local version="$1"
|
||||||
|
local resolved
|
||||||
|
|
||||||
|
resolved="$(npm view "paperclipai@${version}" version 2>/dev/null || true)"
|
||||||
|
[ "$resolved" = "$version" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
require_clean_worktree() {
|
||||||
|
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
|
||||||
|
release_fail "working tree is not clean. Commit, stash, or remove changes before releasing."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
git_worktree_path_for_branch() {
|
||||||
|
local branch_ref="refs/heads/$1"
|
||||||
|
|
||||||
|
git -C "$REPO_ROOT" worktree list --porcelain | awk -v branch_ref="$branch_ref" '
|
||||||
|
$1 == "worktree" { path = substr($0, 10) }
|
||||||
|
$1 == "branch" && $2 == branch_ref { print path; exit }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
path_is_worktree_for_branch() {
|
||||||
|
local path="$1"
|
||||||
|
local branch="$2"
|
||||||
|
local current_branch
|
||||||
|
|
||||||
|
[ -d "$path" ] || return 1
|
||||||
|
current_branch="$(git -C "$path" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
||||||
|
[ "$current_branch" = "$branch" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_release_branch_for_version() {
|
||||||
|
local stable_version="$1"
|
||||||
|
local current_branch
|
||||||
|
local expected_branch
|
||||||
|
|
||||||
|
current_branch="$(git_current_branch)"
|
||||||
|
expected_branch="$(release_branch_name "$stable_version")"
|
||||||
|
|
||||||
|
if [ -z "$current_branch" ]; then
|
||||||
|
release_fail "release work must run from branch $expected_branch, but HEAD is detached."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$current_branch" != "$expected_branch" ]; then
|
||||||
|
release_fail "release work must run from branch $expected_branch, but current branch is $current_branch."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stable_release_exists_anywhere() {
|
||||||
|
local stable_version="$1"
|
||||||
|
local remote="$2"
|
||||||
|
local tag="v$stable_version"
|
||||||
|
|
||||||
|
git_local_tag_exists "$tag" || git_remote_tag_exists "$tag" "$remote" || npm_version_exists "$stable_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_train_is_frozen() {
|
||||||
|
stable_release_exists_anywhere "$1" "$2"
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=./release-lib.sh
|
||||||
|
. "$REPO_ROOT/scripts/release-lib.sh"
|
||||||
export GIT_PAGER=cat
|
export GIT_PAGER=cat
|
||||||
|
|
||||||
channel=""
|
channel=""
|
||||||
@@ -18,7 +20,9 @@ Examples:
|
|||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
- verifies the git worktree is clean, including untracked files
|
- verifies the git worktree is clean, including untracked files
|
||||||
|
- verifies you are on the matching release/X.Y.Z branch
|
||||||
- shows the last stable tag and the target version(s)
|
- shows the last stable tag and the target version(s)
|
||||||
|
- shows the git/npm/GitHub release-train state
|
||||||
- shows commits since the last stable tag
|
- shows commits since the last stable tag
|
||||||
- highlights migration/schema/breaking-change signals
|
- highlights migration/schema/breaking-change signals
|
||||||
- runs the verification gate:
|
- runs the verification gate:
|
||||||
@@ -63,79 +67,19 @@ if [[ ! "$bump_type" =~ ^(patch|minor|major)$ ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
compute_bumped_version() {
|
RELEASE_REMOTE="$(resolve_release_remote)"
|
||||||
node - "$1" "$2" <<'NODE'
|
fetch_release_remote "$RELEASE_REMOTE"
|
||||||
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
|
|
||||||
|
|
||||||
|
LAST_STABLE_TAG="$(get_last_stable_tag)"
|
||||||
|
CURRENT_STABLE_VERSION="$(get_current_stable_version)"
|
||||||
TARGET_STABLE_VERSION="$(compute_bumped_version "$CURRENT_STABLE_VERSION" "$bump_type")"
|
TARGET_STABLE_VERSION="$(compute_bumped_version "$CURRENT_STABLE_VERSION" "$bump_type")"
|
||||||
TARGET_CANARY_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
TARGET_CANARY_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
||||||
|
EXPECTED_RELEASE_BRANCH="$(release_branch_name "$TARGET_STABLE_VERSION")"
|
||||||
|
CURRENT_BRANCH="$(git_current_branch)"
|
||||||
|
RELEASE_TAG="v$TARGET_STABLE_VERSION"
|
||||||
|
NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")"
|
||||||
|
|
||||||
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
|
require_clean_worktree
|
||||||
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
|
if [ "$TARGET_STABLE_VERSION" = "$CURRENT_STABLE_VERSION" ]; then
|
||||||
echo "Error: next stable version matches the current stable version." >&2
|
echo "Error: next stable version matches the current stable version." >&2
|
||||||
@@ -147,10 +91,41 @@ if [[ "$TARGET_CANARY_VERSION" == "${CURRENT_STABLE_VERSION}-canary."* ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ensure_release_branch_for_version "$TARGET_STABLE_VERSION"
|
||||||
|
|
||||||
|
REMOTE_BRANCH_EXISTS="no"
|
||||||
|
REMOTE_TAG_EXISTS="no"
|
||||||
|
LOCAL_TAG_EXISTS="no"
|
||||||
|
NPM_STABLE_EXISTS="no"
|
||||||
|
|
||||||
|
if git_remote_branch_exists "$EXPECTED_RELEASE_BRANCH" "$RELEASE_REMOTE"; then
|
||||||
|
REMOTE_BRANCH_EXISTS="yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git_local_tag_exists "$RELEASE_TAG"; then
|
||||||
|
LOCAL_TAG_EXISTS="yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git_remote_tag_exists "$RELEASE_TAG" "$RELEASE_REMOTE"; then
|
||||||
|
REMOTE_TAG_EXISTS="yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if npm_version_exists "$TARGET_STABLE_VERSION"; then
|
||||||
|
NPM_STABLE_EXISTS="yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LOCAL_TAG_EXISTS" = "yes" ] || [ "$REMOTE_TAG_EXISTS" = "yes" ] || [ "$NPM_STABLE_EXISTS" = "yes" ]; then
|
||||||
|
echo "Error: release train $EXPECTED_RELEASE_BRANCH is frozen because $RELEASE_TAG already exists locally, remotely, or version $TARGET_STABLE_VERSION is already on npm." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Release preflight"
|
echo "==> Release preflight"
|
||||||
|
echo " Remote: $RELEASE_REMOTE"
|
||||||
echo " Channel: $channel"
|
echo " Channel: $channel"
|
||||||
echo " Bump: $bump_type"
|
echo " Bump: $bump_type"
|
||||||
|
echo " Current branch: ${CURRENT_BRANCH:-<detached>}"
|
||||||
|
echo " Expected branch: $EXPECTED_RELEASE_BRANCH"
|
||||||
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
||||||
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
||||||
echo " Next stable version: $TARGET_STABLE_VERSION"
|
echo " Next stable version: $TARGET_STABLE_VERSION"
|
||||||
@@ -162,6 +137,23 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "==> Working tree"
|
echo "==> Working tree"
|
||||||
echo " ✓ Clean"
|
echo " ✓ Clean"
|
||||||
|
echo " ✓ Branch matches release train"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Release train state"
|
||||||
|
echo " Remote branch exists: $REMOTE_BRANCH_EXISTS"
|
||||||
|
echo " Local stable tag exists: $LOCAL_TAG_EXISTS"
|
||||||
|
echo " Remote stable tag exists: $REMOTE_TAG_EXISTS"
|
||||||
|
echo " Stable version on npm: $NPM_STABLE_EXISTS"
|
||||||
|
if [ -f "$NOTES_FILE" ]; then
|
||||||
|
echo " Release notes: present at $NOTES_FILE"
|
||||||
|
else
|
||||||
|
echo " Release notes: missing at $NOTES_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$REMOTE_BRANCH_EXISTS" = "no" ]; then
|
||||||
|
echo " Warning: remote branch $EXPECTED_RELEASE_BRANCH does not exist on $RELEASE_REMOTE yet."
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Commits since last stable tag"
|
echo "==> Commits since last stable tag"
|
||||||
@@ -193,8 +185,10 @@ pnpm build
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Release preflight summary"
|
echo "==> Release preflight summary"
|
||||||
|
echo " Remote: $RELEASE_REMOTE"
|
||||||
echo " Channel: $channel"
|
echo " Channel: $channel"
|
||||||
echo " Bump: $bump_type"
|
echo " Bump: $bump_type"
|
||||||
|
echo " Release branch: $EXPECTED_RELEASE_BRANCH"
|
||||||
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
||||||
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
||||||
echo " Next stable version: $TARGET_STABLE_VERSION"
|
echo " Next stable version: $TARGET_STABLE_VERSION"
|
||||||
|
|||||||
182
scripts/release-start.sh
Executable file
182
scripts/release-start.sh
Executable file
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=./release-lib.sh
|
||||||
|
. "$REPO_ROOT/scripts/release-lib.sh"
|
||||||
|
|
||||||
|
dry_run=false
|
||||||
|
push_branch=true
|
||||||
|
bump_type=""
|
||||||
|
worktree_path=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./scripts/release-start.sh <patch|minor|major> [--dry-run] [--no-push] [--worktree-dir PATH]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./scripts/release-start.sh patch
|
||||||
|
./scripts/release-start.sh minor --dry-run
|
||||||
|
./scripts/release-start.sh major --worktree-dir ../paperclip-release-1.0.0
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- fetches the release remote and tags
|
||||||
|
- computes the next stable version from the latest stable tag
|
||||||
|
- creates or resumes branch release/X.Y.Z
|
||||||
|
- creates or resumes a dedicated worktree for that branch
|
||||||
|
- pushes the release branch to the remote by default
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Stable publishes freeze a release train. If vX.Y.Z already exists locally,
|
||||||
|
remotely, or on npm, this script refuses to reuse release/X.Y.Z.
|
||||||
|
- Use --no-push only if you intentionally do not want the release branch on
|
||||||
|
GitHub yet.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) dry_run=true ;;
|
||||||
|
--no-push) push_branch=false ;;
|
||||||
|
--worktree-dir)
|
||||||
|
shift
|
||||||
|
[ $# -gt 0 ] || release_fail "--worktree-dir requires a path."
|
||||||
|
worktree_path="$1"
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -n "$bump_type" ]; then
|
||||||
|
release_fail "only one bump type may be provided."
|
||||||
|
fi
|
||||||
|
bump_type="$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! "$bump_type" =~ ^(patch|minor|major)$ ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_remote="$(resolve_release_remote)"
|
||||||
|
fetch_release_remote "$release_remote"
|
||||||
|
|
||||||
|
last_stable_tag="$(get_last_stable_tag)"
|
||||||
|
current_stable_version="$(get_current_stable_version)"
|
||||||
|
target_stable_version="$(compute_bumped_version "$current_stable_version" "$bump_type")"
|
||||||
|
target_canary_version="$(next_canary_version "$target_stable_version")"
|
||||||
|
release_branch="$(release_branch_name "$target_stable_version")"
|
||||||
|
release_tag="v$target_stable_version"
|
||||||
|
|
||||||
|
if [ -z "$worktree_path" ]; then
|
||||||
|
worktree_path="$(default_release_worktree_path "$target_stable_version")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if stable_release_exists_anywhere "$target_stable_version" "$release_remote"; then
|
||||||
|
release_fail "release train $release_branch is frozen because $release_tag already exists locally, remotely, or version $target_stable_version is already on npm."
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch_exists_local=false
|
||||||
|
branch_exists_remote=false
|
||||||
|
branch_worktree_path=""
|
||||||
|
created_worktree=false
|
||||||
|
created_branch=false
|
||||||
|
pushed_branch=false
|
||||||
|
|
||||||
|
if git_local_branch_exists "$release_branch"; then
|
||||||
|
branch_exists_local=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git_remote_branch_exists "$release_branch" "$release_remote"; then
|
||||||
|
branch_exists_remote=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch_worktree_path="$(git_worktree_path_for_branch "$release_branch")"
|
||||||
|
if [ -n "$branch_worktree_path" ]; then
|
||||||
|
worktree_path="$branch_worktree_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -e "$worktree_path" ] && ! path_is_worktree_for_branch "$worktree_path" "$release_branch"; then
|
||||||
|
release_fail "path $worktree_path already exists and is not a worktree for $release_branch."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$branch_worktree_path" ]; then
|
||||||
|
if [ "$dry_run" = true ]; then
|
||||||
|
if [ "$branch_exists_local" = true ] || [ "$branch_exists_remote" = true ]; then
|
||||||
|
release_info "[dry-run] Would add worktree $worktree_path for existing branch $release_branch"
|
||||||
|
else
|
||||||
|
release_info "[dry-run] Would create branch $release_branch from $release_remote/master"
|
||||||
|
release_info "[dry-run] Would add worktree $worktree_path"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$branch_exists_local" = true ]; then
|
||||||
|
git -C "$REPO_ROOT" worktree add "$worktree_path" "$release_branch"
|
||||||
|
elif [ "$branch_exists_remote" = true ]; then
|
||||||
|
git -C "$REPO_ROOT" branch --track "$release_branch" "$release_remote/$release_branch"
|
||||||
|
git -C "$REPO_ROOT" worktree add "$worktree_path" "$release_branch"
|
||||||
|
created_branch=true
|
||||||
|
else
|
||||||
|
git -C "$REPO_ROOT" worktree add -b "$release_branch" "$worktree_path" "$release_remote/master"
|
||||||
|
created_branch=true
|
||||||
|
fi
|
||||||
|
created_worktree=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$dry_run" = false ] && [ "$push_branch" = true ] && [ "$branch_exists_remote" = false ]; then
|
||||||
|
git -C "$worktree_path" push -u "$release_remote" "$release_branch"
|
||||||
|
pushed_branch=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$dry_run" = false ] && [ "$branch_exists_remote" = true ]; then
|
||||||
|
git -C "$worktree_path" branch --set-upstream-to "$release_remote/$release_branch" "$release_branch" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_info ""
|
||||||
|
release_info "==> Release train"
|
||||||
|
release_info " Remote: $release_remote"
|
||||||
|
release_info " Last stable tag: ${last_stable_tag:-<none>}"
|
||||||
|
release_info " Current stable version: $current_stable_version"
|
||||||
|
release_info " Bump: $bump_type"
|
||||||
|
release_info " Target stable version: $target_stable_version"
|
||||||
|
release_info " Next canary version: $target_canary_version"
|
||||||
|
release_info " Branch: $release_branch"
|
||||||
|
release_info " Tag (reserved until stable publish): $release_tag"
|
||||||
|
release_info " Worktree: $worktree_path"
|
||||||
|
release_info " Release notes path: $worktree_path/releases/v${target_stable_version}.md"
|
||||||
|
|
||||||
|
release_info ""
|
||||||
|
release_info "==> Status"
|
||||||
|
if [ -n "$branch_worktree_path" ]; then
|
||||||
|
release_info " ✓ Reusing existing worktree for $release_branch"
|
||||||
|
elif [ "$dry_run" = true ]; then
|
||||||
|
release_info " ✓ Dry run only; no branch or worktree created"
|
||||||
|
else
|
||||||
|
[ "$created_branch" = true ] && release_info " ✓ Created branch $release_branch"
|
||||||
|
[ "$created_worktree" = true ] && release_info " ✓ Created worktree $worktree_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$branch_exists_remote" = true ]; then
|
||||||
|
release_info " ✓ Remote branch already exists on $release_remote"
|
||||||
|
elif [ "$dry_run" = true ] && [ "$push_branch" = true ]; then
|
||||||
|
release_info " [dry-run] Would push $release_branch to $release_remote"
|
||||||
|
elif [ "$push_branch" = true ] && [ "$pushed_branch" = true ]; then
|
||||||
|
release_info " ✓ Pushed $release_branch to $release_remote"
|
||||||
|
elif [ "$push_branch" = false ]; then
|
||||||
|
release_warn "release branch was not pushed. Stable publish will later refuse until the branch exists on $release_remote."
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_info ""
|
||||||
|
release_info "Next steps:"
|
||||||
|
release_info " cd $worktree_path"
|
||||||
|
release_info " Draft or update releases/v${target_stable_version}.md"
|
||||||
|
release_info " ./scripts/release-preflight.sh canary $bump_type"
|
||||||
|
release_info " ./scripts/release.sh $bump_type --canary"
|
||||||
|
release_info ""
|
||||||
|
release_info "Merge rule:"
|
||||||
|
release_info " Merge $release_branch back to master without squash or rebase so tag $release_tag remains reachable from master."
|
||||||
@@ -15,10 +15,11 @@ set -euo pipefail
|
|||||||
# npm dist-tag "canary". Stable releases publish 1.2.3 under "latest".
|
# npm dist-tag "canary". Stable releases publish 1.2.3 under "latest".
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=./release-lib.sh
|
||||||
|
. "$REPO_ROOT/scripts/release-lib.sh"
|
||||||
CLI_DIR="$REPO_ROOT/cli"
|
CLI_DIR="$REPO_ROOT/cli"
|
||||||
TEMP_CHANGESET_FILE="$REPO_ROOT/.changeset/release-bump.md"
|
TEMP_CHANGESET_FILE="$REPO_ROOT/.changeset/release-bump.md"
|
||||||
TEMP_PRE_FILE="$REPO_ROOT/.changeset/pre.json"
|
TEMP_PRE_FILE="$REPO_ROOT/.changeset/pre.json"
|
||||||
PUBLISH_REMOTE="${PUBLISH_REMOTE:-public-gh}"
|
|
||||||
|
|
||||||
dry_run=false
|
dry_run=false
|
||||||
canary=false
|
canary=false
|
||||||
@@ -41,6 +42,7 @@ Notes:
|
|||||||
- Canary publishes prerelease versions like 1.2.3-canary.0 under the npm
|
- Canary publishes prerelease versions like 1.2.3-canary.0 under the npm
|
||||||
dist-tag "canary".
|
dist-tag "canary".
|
||||||
- Stable publishes 1.2.3 under the npm dist-tag "latest".
|
- Stable publishes 1.2.3 under the npm dist-tag "latest".
|
||||||
|
- Run this from branch release/X.Y.Z matching the computed target version.
|
||||||
- Dry runs leave the working tree clean.
|
- Dry runs leave the working tree clean.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -73,15 +75,6 @@ if [[ ! "$bump_type" =~ ^(patch|minor|major)$ ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info() {
|
|
||||||
echo "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "Error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
restore_publish_artifacts() {
|
restore_publish_artifacts() {
|
||||||
if [ -f "$CLI_DIR/package.dev.json" ]; then
|
if [ -f "$CLI_DIR/package.dev.json" ]; then
|
||||||
mv "$CLI_DIR/package.dev.json" "$CLI_DIR/package.json"
|
mv "$CLI_DIR/package.dev.json" "$CLI_DIR/package.json"
|
||||||
@@ -130,28 +123,22 @@ set_cleanup_trap() {
|
|||||||
trap cleanup_release_state EXIT
|
trap cleanup_release_state EXIT
|
||||||
}
|
}
|
||||||
|
|
||||||
require_clean_worktree() {
|
|
||||||
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
|
|
||||||
fail "working tree is not clean. Commit, stash, or remove changes before releasing."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_npm_publish_auth() {
|
require_npm_publish_auth() {
|
||||||
if [ "$dry_run" = true ]; then
|
if [ "$dry_run" = true ]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if npm whoami >/dev/null 2>&1; then
|
if npm whoami >/dev/null 2>&1; then
|
||||||
info " ✓ Logged in to npm as $(npm whoami)"
|
release_info " ✓ Logged in to npm as $(npm whoami)"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
|
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
|
||||||
info " ✓ npm publish auth will be provided by GitHub Actions trusted publishing"
|
release_info " ✓ npm publish auth will be provided by GitHub Actions trusted publishing"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fail "npm publish auth is not available. Use 'npm login' locally or run from the GitHub release workflow."
|
release_fail "npm publish auth is not available. Use 'npm login' locally or run from the GitHub release workflow."
|
||||||
}
|
}
|
||||||
|
|
||||||
list_public_package_info() {
|
list_public_package_info() {
|
||||||
@@ -202,66 +189,6 @@ for (const [dir, name] of rows) {
|
|||||||
NODE
|
NODE
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
replace_version_string() {
|
replace_version_string() {
|
||||||
local from_version="$1"
|
local from_version="$1"
|
||||||
local to_version="$2"
|
local to_version="$2"
|
||||||
@@ -312,25 +239,55 @@ for (const relFile of extraFiles) {
|
|||||||
NODE
|
NODE
|
||||||
}
|
}
|
||||||
|
|
||||||
LAST_STABLE_TAG="$(git -C "$REPO_ROOT" tag --list 'v*' --sort=-version:refname | head -1)"
|
PUBLISH_REMOTE="$(resolve_release_remote)"
|
||||||
CURRENT_STABLE_VERSION="${LAST_STABLE_TAG#v}"
|
fetch_release_remote "$PUBLISH_REMOTE"
|
||||||
if [ -z "$CURRENT_STABLE_VERSION" ]; then
|
|
||||||
CURRENT_STABLE_VERSION="0.0.0"
|
LAST_STABLE_TAG="$(get_last_stable_tag)"
|
||||||
fi
|
CURRENT_STABLE_VERSION="$(get_current_stable_version)"
|
||||||
|
|
||||||
TARGET_STABLE_VERSION="$(compute_bumped_version "$CURRENT_STABLE_VERSION" "$bump_type")"
|
TARGET_STABLE_VERSION="$(compute_bumped_version "$CURRENT_STABLE_VERSION" "$bump_type")"
|
||||||
TARGET_PUBLISH_VERSION="$TARGET_STABLE_VERSION"
|
TARGET_PUBLISH_VERSION="$TARGET_STABLE_VERSION"
|
||||||
|
CURRENT_BRANCH="$(git_current_branch)"
|
||||||
|
EXPECTED_RELEASE_BRANCH="$(release_branch_name "$TARGET_STABLE_VERSION")"
|
||||||
|
NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")"
|
||||||
|
RELEASE_TAG="v$TARGET_STABLE_VERSION"
|
||||||
|
|
||||||
if [ "$canary" = true ]; then
|
if [ "$canary" = true ]; then
|
||||||
TARGET_PUBLISH_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
TARGET_PUBLISH_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$TARGET_STABLE_VERSION" = "$CURRENT_STABLE_VERSION" ]; then
|
if [ "$TARGET_STABLE_VERSION" = "$CURRENT_STABLE_VERSION" ]; then
|
||||||
fail "next stable version matches the current stable version. Refusing to publish."
|
release_fail "next stable version matches the current stable version. Refusing to publish."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$TARGET_PUBLISH_VERSION" == "${CURRENT_STABLE_VERSION}-canary."* ]]; then
|
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."
|
release_fail "canary versions must be derived from the next stable version, never ${CURRENT_STABLE_VERSION}-canary.N."
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_clean_worktree
|
||||||
|
ensure_release_branch_for_version "$TARGET_STABLE_VERSION"
|
||||||
|
|
||||||
|
if git_local_tag_exists "$RELEASE_TAG" || git_remote_tag_exists "$RELEASE_TAG" "$PUBLISH_REMOTE"; then
|
||||||
|
release_fail "release train $EXPECTED_RELEASE_BRANCH is frozen because tag $RELEASE_TAG already exists locally or on $PUBLISH_REMOTE."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if npm_version_exists "$TARGET_STABLE_VERSION"; then
|
||||||
|
release_fail "stable version $TARGET_STABLE_VERSION is already published on npm. Refusing to reuse release train $EXPECTED_RELEASE_BRANCH."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$canary" = false ] && [ ! -f "$NOTES_FILE" ]; then
|
||||||
|
release_fail "stable release notes file is required at $NOTES_FILE before publishing stable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$canary" = true ] && [ ! -f "$NOTES_FILE" ]; then
|
||||||
|
release_warn "stable release notes file is missing at $NOTES_FILE. Draft it before you finalize stable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git_remote_branch_exists "$EXPECTED_RELEASE_BRANCH" "$PUBLISH_REMOTE"; then
|
||||||
|
if [ "$canary" = false ] && [ "$dry_run" = false ]; then
|
||||||
|
release_fail "remote branch $EXPECTED_RELEASE_BRANCH does not exist on $PUBLISH_REMOTE. Run ./scripts/release-start.sh $bump_type first or push the branch before stable publish."
|
||||||
|
fi
|
||||||
|
release_warn "remote branch $EXPECTED_RELEASE_BRANCH does not exist on $PUBLISH_REMOTE yet."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PUBLIC_PACKAGE_INFO="$(list_public_package_info)"
|
PUBLIC_PACKAGE_INFO="$(list_public_package_info)"
|
||||||
@@ -338,33 +295,36 @@ PUBLIC_PACKAGE_NAMES="$(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f2)"
|
|||||||
PUBLIC_PACKAGE_DIRS="$(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f1)"
|
PUBLIC_PACKAGE_DIRS="$(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f1)"
|
||||||
|
|
||||||
if [ -z "$PUBLIC_PACKAGE_INFO" ]; then
|
if [ -z "$PUBLIC_PACKAGE_INFO" ]; then
|
||||||
fail "no public packages were found in the workspace."
|
release_fail "no public packages were found in the workspace."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
info "==> Release plan"
|
release_info "==> Release plan"
|
||||||
info " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
release_info " Remote: $PUBLISH_REMOTE"
|
||||||
info " Current stable version: $CURRENT_STABLE_VERSION"
|
release_info " Current branch: ${CURRENT_BRANCH:-<detached>}"
|
||||||
|
release_info " Expected branch: $EXPECTED_RELEASE_BRANCH"
|
||||||
|
release_info " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
||||||
|
release_info " Current stable version: $CURRENT_STABLE_VERSION"
|
||||||
if [ "$canary" = true ]; then
|
if [ "$canary" = true ]; then
|
||||||
info " Target stable version: $TARGET_STABLE_VERSION"
|
release_info " Target stable version: $TARGET_STABLE_VERSION"
|
||||||
info " Canary version: $TARGET_PUBLISH_VERSION"
|
release_info " Canary version: $TARGET_PUBLISH_VERSION"
|
||||||
info " Guard: canary is derived from next stable version, not ${CURRENT_STABLE_VERSION}-canary.N"
|
release_info " Guard: canary is derived from next stable version, not ${CURRENT_STABLE_VERSION}-canary.N"
|
||||||
else
|
else
|
||||||
info " Stable version: $TARGET_STABLE_VERSION"
|
release_info " Stable version: $TARGET_STABLE_VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
info "==> Step 1/7: Preflight checks..."
|
release_info "==> Step 1/7: Preflight checks..."
|
||||||
require_clean_worktree
|
release_info " ✓ Working tree is clean"
|
||||||
info " ✓ Working tree is clean"
|
release_info " ✓ Branch matches release train"
|
||||||
require_npm_publish_auth
|
require_npm_publish_auth
|
||||||
|
|
||||||
if [ "$dry_run" = true ] || [ "$canary" = true ]; then
|
if [ "$dry_run" = true ] || [ "$canary" = true ]; then
|
||||||
set_cleanup_trap
|
set_cleanup_trap
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
info "==> Step 2/7: Creating release changeset..."
|
release_info "==> Step 2/7: Creating release changeset..."
|
||||||
{
|
{
|
||||||
echo "---"
|
echo "---"
|
||||||
while IFS= read -r pkg_name; do
|
while IFS= read -r pkg_name; do
|
||||||
@@ -379,10 +339,10 @@ info "==> Step 2/7: Creating release changeset..."
|
|||||||
echo "Stable release preparation for $TARGET_STABLE_VERSION"
|
echo "Stable release preparation for $TARGET_STABLE_VERSION"
|
||||||
fi
|
fi
|
||||||
} > "$TEMP_CHANGESET_FILE"
|
} > "$TEMP_CHANGESET_FILE"
|
||||||
info " ✓ Created release changeset for $(printf '%s\n' "$PUBLIC_PACKAGE_NAMES" | sed '/^$/d' | wc -l | xargs) packages"
|
release_info " ✓ Created release changeset for $(printf '%s\n' "$PUBLIC_PACKAGE_NAMES" | sed '/^$/d' | wc -l | xargs) packages"
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
info "==> Step 3/7: Versioning packages..."
|
release_info "==> Step 3/7: Versioning packages..."
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
if [ "$canary" = true ]; then
|
if [ "$canary" = true ]; then
|
||||||
npx changeset pre enter canary
|
npx changeset pre enter canary
|
||||||
@@ -398,12 +358,12 @@ fi
|
|||||||
|
|
||||||
VERSION_IN_CLI_PACKAGE="$(node -e "console.log(require('$CLI_DIR/package.json').version)")"
|
VERSION_IN_CLI_PACKAGE="$(node -e "console.log(require('$CLI_DIR/package.json').version)")"
|
||||||
if [ "$VERSION_IN_CLI_PACKAGE" != "$TARGET_PUBLISH_VERSION" ]; then
|
if [ "$VERSION_IN_CLI_PACKAGE" != "$TARGET_PUBLISH_VERSION" ]; then
|
||||||
fail "versioning drift detected. Expected $TARGET_PUBLISH_VERSION but found $VERSION_IN_CLI_PACKAGE."
|
release_fail "versioning drift detected. Expected $TARGET_PUBLISH_VERSION but found $VERSION_IN_CLI_PACKAGE."
|
||||||
fi
|
fi
|
||||||
info " ✓ Versioned workspace to $TARGET_PUBLISH_VERSION"
|
release_info " ✓ Versioned workspace to $TARGET_PUBLISH_VERSION"
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
info "==> Step 4/7: Building workspace artifacts..."
|
release_info "==> Step 4/7: Building workspace artifacts..."
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
pnpm build
|
pnpm build
|
||||||
bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh"
|
bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh"
|
||||||
@@ -411,49 +371,49 @@ for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-loc
|
|||||||
rm -rf "$REPO_ROOT/$pkg_dir/skills"
|
rm -rf "$REPO_ROOT/$pkg_dir/skills"
|
||||||
cp -r "$REPO_ROOT/skills" "$REPO_ROOT/$pkg_dir/skills"
|
cp -r "$REPO_ROOT/skills" "$REPO_ROOT/$pkg_dir/skills"
|
||||||
done
|
done
|
||||||
info " ✓ Workspace build complete"
|
release_info " ✓ Workspace build complete"
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
info "==> Step 5/7: Building publishable CLI bundle..."
|
release_info "==> Step 5/7: Building publishable CLI bundle..."
|
||||||
"$REPO_ROOT/scripts/build-npm.sh" --skip-checks
|
"$REPO_ROOT/scripts/build-npm.sh" --skip-checks
|
||||||
info " ✓ CLI bundle ready"
|
release_info " ✓ CLI bundle ready"
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
if [ "$dry_run" = true ]; then
|
if [ "$dry_run" = true ]; then
|
||||||
info "==> Step 6/7: Previewing publish payloads (--dry-run)..."
|
release_info "==> Step 6/7: Previewing publish payloads (--dry-run)..."
|
||||||
while IFS= read -r pkg_dir; do
|
while IFS= read -r pkg_dir; do
|
||||||
[ -z "$pkg_dir" ] && continue
|
[ -z "$pkg_dir" ] && continue
|
||||||
info " --- $pkg_dir ---"
|
release_info " --- $pkg_dir ---"
|
||||||
cd "$REPO_ROOT/$pkg_dir"
|
cd "$REPO_ROOT/$pkg_dir"
|
||||||
npm pack --dry-run 2>&1 | tail -3
|
npm pack --dry-run 2>&1 | tail -3
|
||||||
done <<< "$PUBLIC_PACKAGE_DIRS"
|
done <<< "$PUBLIC_PACKAGE_DIRS"
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
if [ "$canary" = true ]; then
|
if [ "$canary" = true ]; then
|
||||||
info " [dry-run] Would publish ${TARGET_PUBLISH_VERSION} under dist-tag canary"
|
release_info " [dry-run] Would publish ${TARGET_PUBLISH_VERSION} under dist-tag canary"
|
||||||
else
|
else
|
||||||
info " [dry-run] Would publish ${TARGET_PUBLISH_VERSION} under dist-tag latest"
|
release_info " [dry-run] Would publish ${TARGET_PUBLISH_VERSION} under dist-tag latest"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [ "$canary" = true ]; then
|
if [ "$canary" = true ]; then
|
||||||
info "==> Step 6/7: Publishing canary to npm..."
|
release_info "==> Step 6/7: Publishing canary to npm..."
|
||||||
npx changeset publish
|
npx changeset publish
|
||||||
info " ✓ Published ${TARGET_PUBLISH_VERSION} under dist-tag canary"
|
release_info " ✓ Published ${TARGET_PUBLISH_VERSION} under dist-tag canary"
|
||||||
else
|
else
|
||||||
info "==> Step 6/7: Publishing stable release to npm..."
|
release_info "==> Step 6/7: Publishing stable release to npm..."
|
||||||
npx changeset publish
|
npx changeset publish
|
||||||
info " ✓ Published ${TARGET_PUBLISH_VERSION} under dist-tag latest"
|
release_info " ✓ Published ${TARGET_PUBLISH_VERSION} under dist-tag latest"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
if [ "$dry_run" = true ]; then
|
if [ "$dry_run" = true ]; then
|
||||||
info "==> Step 7/7: Cleaning up dry-run state..."
|
release_info "==> Step 7/7: Cleaning up dry-run state..."
|
||||||
info " ✓ Dry run leaves the working tree unchanged"
|
release_info " ✓ Dry run leaves the working tree unchanged"
|
||||||
elif [ "$canary" = true ]; then
|
elif [ "$canary" = true ]; then
|
||||||
info "==> Step 7/7: Cleaning up canary state..."
|
release_info "==> Step 7/7: Cleaning up canary state..."
|
||||||
info " ✓ Canary state will be discarded after publish"
|
release_info " ✓ Canary state will be discarded after publish"
|
||||||
else
|
else
|
||||||
info "==> Step 7/7: Finalizing stable release commit..."
|
release_info "==> Step 7/7: Finalizing stable release commit..."
|
||||||
restore_publish_artifacts
|
restore_publish_artifacts
|
||||||
|
|
||||||
git -C "$REPO_ROOT" add -u .changeset packages server cli
|
git -C "$REPO_ROOT" add -u .changeset packages server cli
|
||||||
@@ -463,23 +423,24 @@ else
|
|||||||
|
|
||||||
git -C "$REPO_ROOT" commit -m "chore: release v$TARGET_STABLE_VERSION"
|
git -C "$REPO_ROOT" commit -m "chore: release v$TARGET_STABLE_VERSION"
|
||||||
git -C "$REPO_ROOT" tag "v$TARGET_STABLE_VERSION"
|
git -C "$REPO_ROOT" tag "v$TARGET_STABLE_VERSION"
|
||||||
info " ✓ Created commit and tag v$TARGET_STABLE_VERSION"
|
release_info " ✓ Created commit and tag v$TARGET_STABLE_VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info ""
|
release_info ""
|
||||||
if [ "$dry_run" = true ]; then
|
if [ "$dry_run" = true ]; then
|
||||||
if [ "$canary" = true ]; then
|
if [ "$canary" = true ]; then
|
||||||
info "Dry run complete for canary ${TARGET_PUBLISH_VERSION}."
|
release_info "Dry run complete for canary ${TARGET_PUBLISH_VERSION}."
|
||||||
else
|
else
|
||||||
info "Dry run complete for stable v${TARGET_STABLE_VERSION}."
|
release_info "Dry run complete for stable v${TARGET_STABLE_VERSION}."
|
||||||
fi
|
fi
|
||||||
elif [ "$canary" = true ]; then
|
elif [ "$canary" = true ]; then
|
||||||
info "Published canary ${TARGET_PUBLISH_VERSION}."
|
release_info "Published canary ${TARGET_PUBLISH_VERSION}."
|
||||||
info "Install with: npx paperclipai@canary onboard"
|
release_info "Install with: npx paperclipai@canary onboard"
|
||||||
info "Stable version remains: $CURRENT_STABLE_VERSION"
|
release_info "Stable version remains: $CURRENT_STABLE_VERSION"
|
||||||
else
|
else
|
||||||
info "Published stable v${TARGET_STABLE_VERSION}."
|
release_info "Published stable v${TARGET_STABLE_VERSION}."
|
||||||
info "Next steps:"
|
release_info "Next steps:"
|
||||||
info " git push ${PUBLISH_REMOTE} HEAD:master --follow-tags"
|
release_info " git push ${PUBLISH_REMOTE} HEAD --follow-tags"
|
||||||
info " ./scripts/create-github-release.sh $TARGET_STABLE_VERSION"
|
release_info " ./scripts/create-github-release.sh $TARGET_STABLE_VERSION"
|
||||||
|
release_info " Open a PR from ${EXPECTED_RELEASE_BRANCH} to master and merge without squash or rebase"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -106,6 +106,25 @@ Guidelines:
|
|||||||
- keep highlights short and concrete
|
- keep highlights short and concrete
|
||||||
- spell out upgrade actions for breaking changes
|
- spell out upgrade actions for breaking changes
|
||||||
|
|
||||||
|
### Inline PR and contributor attribution
|
||||||
|
|
||||||
|
When a bullet item clearly maps to a merged pull request, add inline attribution at the
|
||||||
|
end of the entry in this format:
|
||||||
|
|
||||||
|
```
|
||||||
|
- **Feature name** — Description. ([#123](https://github.com/paperclipai/paperclip/pull/123), @contributor1, @contributor2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Only add a PR link when you can confidently trace the bullet to a specific merged PR.
|
||||||
|
Use merge commit messages (`Merge pull request #N from user/branch`) to map PRs.
|
||||||
|
- List the contributor(s) who authored the PR. Use GitHub usernames, not real names or emails.
|
||||||
|
- If multiple PRs contributed to a single bullet, list them all: `([#10](url), [#12](url), @user1, @user2)`.
|
||||||
|
- If you cannot determine the PR number or contributor with confidence, omit the attribution
|
||||||
|
parenthetical — do not guess.
|
||||||
|
- Core maintainer commits that don't have an external PR can omit the parenthetical.
|
||||||
|
|
||||||
## Step 5 — Write the File
|
## Step 5 — Write the File
|
||||||
|
|
||||||
Template:
|
Template:
|
||||||
@@ -124,10 +143,29 @@ Template:
|
|||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
## Upgrade Guide
|
## Upgrade Guide
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thank you to everyone who contributed to this release!
|
||||||
|
|
||||||
|
@username1, @username2, @username3
|
||||||
```
|
```
|
||||||
|
|
||||||
Omit empty sections except `Highlights`, `Improvements`, and `Fixes`, which should usually exist.
|
Omit empty sections except `Highlights`, `Improvements`, and `Fixes`, which should usually exist.
|
||||||
|
|
||||||
|
The `Contributors` section should always be included. List every person who authored
|
||||||
|
commits in the release range, @-mentioning them by their **GitHub username** (not their
|
||||||
|
real name or email). To find GitHub usernames:
|
||||||
|
|
||||||
|
1. Extract usernames from merge commit messages: `git log v{last}..HEAD --oneline --merges` — the branch prefix (e.g. `from username/branch`) gives the GitHub username.
|
||||||
|
2. For noreply emails like `user@users.noreply.github.com`, the username is the part before `@`.
|
||||||
|
3. For contributors whose username is ambiguous, check `gh api users/{guess}` or the PR page.
|
||||||
|
|
||||||
|
**Never expose contributor email addresses.** Use `@username` only.
|
||||||
|
|
||||||
|
Exclude bot accounts (e.g. `lockfile-bot`, `dependabot`) from the list. List contributors
|
||||||
|
in alphabetical order by GitHub username (case-insensitive).
|
||||||
|
|
||||||
## Step 6 — Review Before Release
|
## Step 6 — Review Before Release
|
||||||
|
|
||||||
Before handing it off:
|
Before handing it off:
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ Run the full Paperclip release as a maintainer workflow, not just an npm publish
|
|||||||
This skill coordinates:
|
This skill coordinates:
|
||||||
|
|
||||||
- stable changelog drafting via `release-changelog`
|
- stable changelog drafting via `release-changelog`
|
||||||
|
- release-train setup via `scripts/release-start.sh`
|
||||||
- prerelease canary publishing via `scripts/release.sh --canary`
|
- prerelease canary publishing via `scripts/release.sh --canary`
|
||||||
- Docker smoke testing via `scripts/docker-onboard-smoke.sh`
|
- Docker smoke testing via `scripts/docker-onboard-smoke.sh`
|
||||||
- stable publishing via `scripts/release.sh`
|
- stable publishing via `scripts/release.sh`
|
||||||
- pushing the release commit and tag
|
- pushing the stable branch commit and tag
|
||||||
- GitHub Release creation via `scripts/create-github-release.sh`
|
- GitHub Release creation via `scripts/create-github-release.sh`
|
||||||
- website / announcement follow-up tasks
|
- website / announcement follow-up tasks
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ Before proceeding, verify all of the following:
|
|||||||
2. The repo working tree is clean, including untracked files.
|
2. The repo working tree is clean, including untracked files.
|
||||||
3. There are commits since the last stable tag.
|
3. There are commits since the last stable tag.
|
||||||
4. The release SHA has passed the verification gate or is about to.
|
4. The release SHA has passed the verification gate or is about to.
|
||||||
5. If package manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master`.
|
5. If package manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master` before the release branch is cut.
|
||||||
6. npm publish rights are available locally, or the GitHub release workflow is being used with trusted publishing.
|
6. npm publish rights are available locally, or the GitHub release workflow is being used with trusted publishing.
|
||||||
7. If running through Paperclip, you have issue context for status updates and follow-up task creation.
|
7. If running through Paperclip, you have issue context for status updates and follow-up task creation.
|
||||||
|
|
||||||
@@ -55,13 +56,15 @@ Collect these inputs up front:
|
|||||||
|
|
||||||
Paperclip now uses this release model:
|
Paperclip now uses this release model:
|
||||||
|
|
||||||
1. Draft the **stable** changelog as `releases/vX.Y.Z.md`
|
1. Start or resume `release/X.Y.Z`
|
||||||
2. Publish one or more **prerelease canaries** such as `X.Y.Z-canary.0`
|
2. Draft the **stable** changelog as `releases/vX.Y.Z.md`
|
||||||
3. Smoke test the canary via Docker
|
3. Publish one or more **prerelease canaries** such as `X.Y.Z-canary.0`
|
||||||
4. Publish the stable version `X.Y.Z`
|
4. Smoke test the canary via Docker
|
||||||
5. Push the release commit and tag
|
5. Publish the stable version `X.Y.Z`
|
||||||
6. Create the GitHub Release
|
6. Push the stable branch commit and tag
|
||||||
7. Complete website and announcement surfaces
|
7. Create the GitHub Release
|
||||||
|
8. Merge `release/X.Y.Z` back to `master` without squash or rebase
|
||||||
|
9. Complete website and announcement surfaces
|
||||||
|
|
||||||
Critical consequence:
|
Critical consequence:
|
||||||
|
|
||||||
@@ -70,7 +73,13 @@ Critical consequence:
|
|||||||
|
|
||||||
## Step 1 — Decide the Stable Version
|
## Step 1 — Decide the Stable Version
|
||||||
|
|
||||||
Run release preflight first:
|
Start the release train first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/release-start.sh {patch|minor|major}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run release preflight:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/release-preflight.sh canary {patch|minor|major}
|
./scripts/release-preflight.sh canary {patch|minor|major}
|
||||||
@@ -125,7 +134,7 @@ The GitHub Actions release workflow installs with `pnpm install --frozen-lockfil
|
|||||||
|
|
||||||
## Step 4 — Publish a Canary
|
## Step 4 — Publish a Canary
|
||||||
|
|
||||||
Run:
|
Run from the `release/X.Y.Z` branch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/release.sh {patch|minor|major} --canary --dry-run
|
./scripts/release.sh {patch|minor|major} --canary --dry-run
|
||||||
@@ -203,12 +212,14 @@ Stable publish does **not** push the release for you.
|
|||||||
After stable publish succeeds:
|
After stable publish succeeds:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push public-gh HEAD:master --follow-tags
|
git push public-gh HEAD --follow-tags
|
||||||
./scripts/create-github-release.sh X.Y.Z
|
./scripts/create-github-release.sh X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the stable changelog file as the GitHub Release notes source.
|
Use the stable changelog file as the GitHub Release notes source.
|
||||||
|
|
||||||
|
Then open the PR from `release/X.Y.Z` back to `master` and merge without squash or rebase.
|
||||||
|
|
||||||
## Step 8 — Finish the Other Surfaces
|
## Step 8 — Finish the Other Surfaces
|
||||||
|
|
||||||
Create or verify follow-up work for:
|
Create or verify follow-up work for:
|
||||||
|
|||||||
Reference in New Issue
Block a user