chore: switch release calver to mdd patch

This commit is contained in:
dotta
2026-03-18 07:50:33 -05:00
parent f598a556dc
commit 3e0e15394a
11 changed files with 305 additions and 237 deletions

View File

@@ -1,7 +1,7 @@
--- ---
name: release-changelog name: release-changelog
description: > description: >
Generate the stable Paperclip release changelog at releases/v{version}.md by Generate the stable Paperclip release changelog at releases/vYYYY.MDD.P.md by
reading commits, changesets, and merged PR context since the last stable tag. reading commits, changesets, and merged PR context since the last stable tag.
--- ---
@@ -9,20 +9,33 @@ description: >
Generate the user-facing changelog for the **stable** Paperclip release. Generate the user-facing changelog for the **stable** Paperclip release.
## Versioning Model
Paperclip uses **calendar versioning (calver)**:
- Stable releases: `YYYY.MDD.P` (e.g. `2026.318.0`)
- Canary releases: `YYYY.MDD.P-canary.N` (e.g. `2026.318.1-canary.0`)
- Git tags: `vYYYY.MDD.P` for stable, `canary/vYYYY.MDD.P-canary.N` for canary
There are no major/minor/patch bumps. The stable version is derived from the
intended release date (UTC) plus the next same-day stable patch slot.
Output: Output:
- `releases/v{version}.md` - `releases/vYYYY.MDD.P.md`
Important rule: Important rules:
- even if there are canary releases such as `1.2.3-canary.0`, the changelog file stays `releases/v1.2.3.md` - even if there are canary releases such as `2026.318.1-canary.0`, the changelog file stays `releases/v2026.318.1.md`
- do not derive versions from semver bump types
- do not create canary changelog files
## Step 0 — Idempotency Check ## Step 0 — Idempotency Check
Before generating anything, check whether the file already exists: Before generating anything, check whether the file already exists:
```bash ```bash
ls releases/v{version}.md 2>/dev/null ls releases/vYYYY.MDD.P.md 2>/dev/null
``` ```
If it exists: If it exists:
@@ -41,13 +54,14 @@ git tag --list 'v*' --sort=-version:refname | head -1
git log v{last}..HEAD --oneline --no-merges git log v{last}..HEAD --oneline --no-merges
``` ```
The planned stable version comes from one of: The stable version comes from one of:
- an explicit maintainer request - an explicit maintainer request
- the chosen bump type applied to the last stable tag - `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
- the release plan already agreed in `doc/RELEASING.md` - the release plan already agreed in `doc/RELEASING.md`
Do not derive the changelog version from a canary tag or prerelease suffix. Do not derive the changelog version from a canary tag or prerelease suffix.
Do not derive major/minor/patch bumps from API intent — calver uses the date and same-day stable slot.
## Step 2 — Gather the Raw Inputs ## Step 2 — Gather the Raw Inputs
@@ -73,7 +87,6 @@ Look for:
- destructive migrations - destructive migrations
- removed or changed API fields/endpoints - removed or changed API fields/endpoints
- renamed or removed config keys - renamed or removed config keys
- `major` changesets
- `BREAKING:` or `BREAKING CHANGE:` commit signals - `BREAKING:` or `BREAKING CHANGE:` commit signals
Key commands: Key commands:
@@ -85,7 +98,8 @@ git diff v{last}..HEAD -- server/src/routes/ server/src/api/
git log v{last}..HEAD --format="%s" | rg -n 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true git log v{last}..HEAD --format="%s" | rg -n 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true
``` ```
If the requested bump is lower than the minimum required bump, flag that before the release proceeds. If breaking changes are detected, flag them prominently — they must appear in the
Breaking Changes section with an upgrade path.
## Step 4 — Categorize for Users ## Step 4 — Categorize for Users
@@ -130,9 +144,9 @@ Rules:
Template: Template:
```markdown ```markdown
# v{version} # vYYYY.MDD.P
> Released: {YYYY-MM-DD} > Released: YYYY-MM-DD
## Breaking Changes ## Breaking Changes

View File

@@ -2,23 +2,21 @@
name: release name: release
description: > description: >
Coordinate a full Paperclip release across engineering verification, npm, Coordinate a full Paperclip release across engineering verification, npm,
GitHub, website publishing, and announcement follow-up. Use when leadership GitHub, smoke testing, and announcement follow-up. Use when leadership asks
asks to ship a release, not merely to discuss version bumps. to ship a release, not merely to discuss versioning.
--- ---
# Release Coordination Skill # Release Coordination Skill
Run the full Paperclip release as a maintainer workflow, not just an npm publish. Run the full Paperclip maintainer release 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` - canary verification and publish status from `master`
- 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` - manual stable promotion from a chosen source ref
- pushing the stable branch commit and tag - GitHub Release creation
- GitHub Release creation via `scripts/create-github-release.sh`
- website / announcement follow-up tasks - website / announcement follow-up tasks
## Trigger ## Trigger
@@ -26,8 +24,9 @@ This skill coordinates:
Use this skill when leadership asks for: Use this skill when leadership asks for:
- "do a release" - "do a release"
- "ship the next patch/minor/major" - "ship the release"
- "release vX.Y.Z" - "promote this canary to stable"
- "cut the stable release"
## Preconditions ## Preconditions
@@ -35,10 +34,10 @@ Before proceeding, verify all of the following:
1. `.agents/skills/release-changelog/SKILL.md` exists and is usable. 1. `.agents/skills/release-changelog/SKILL.md` exists and is usable.
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 is at least one canary or candidate commit since the last stable tag.
4. The release SHA has passed the verification gate or is about to. 4. The candidate 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` before the release branch is cut. 5. If manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master`.
6. npm publish rights are available locally, or the GitHub release workflow is being used with trusted publishing. 6. npm publish rights are available through GitHub trusted publishing, or through local npm auth for emergency/manual use.
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.
If any precondition fails, stop and report the blocker. If any precondition fails, stop and report the blocker.
@@ -47,78 +46,67 @@ If any precondition fails, stop and report the blocker.
Collect these inputs up front: Collect these inputs up front:
- requested bump: `patch`, `minor`, or `major` - whether the target is a canary check or a stable promotion
- whether this run is a dry run or live release - the candidate `source_ref` for stable
- whether the release is being run locally or from GitHub Actions - whether the stable run is dry-run or live
- release issue / company context for website and announcement follow-up - release issue / company context for website and announcement follow-up
## Step 0 — Release Model ## Step 0 — Release Model
Paperclip now uses this release model: Paperclip now uses a commit-driven release model:
1. Start or resume `release/X.Y.Z` 1. every push to `master` publishes a canary automatically
2. Draft the **stable** changelog as `releases/vX.Y.Z.md` 2. canaries use `YYYY.MDD.P-canary.N`
3. Publish one or more **prerelease canaries** such as `X.Y.Z-canary.0` 3. stable releases use `YYYY.MDD.P`
4. Smoke test the canary via Docker 4. the middle slot is `MDD`, where `M` is the UTC month and `DD` is the zero-padded UTC day
5. Publish the stable version `X.Y.Z` 5. the stable patch slot increments when more than one stable ships on the same UTC date
6. Push the stable branch commit and tag 6. stable releases are manually promoted from a chosen tested commit or canary source commit
7. Create the GitHub Release 7. only stable releases get `releases/vYYYY.MDD.P.md`, git tag `vYYYY.MDD.P`, and a GitHub Release
8. Merge `release/X.Y.Z` back to `master` without squash or rebase
9. Complete website and announcement surfaces
Critical consequence: Critical consequences:
- Canaries do **not** use promote-by-dist-tag anymore. - do not use release branches as the default path
- The changelog remains stable-only. Do not create `releases/vX.Y.Z-canary.N.md`. - do not derive major/minor/patch bumps
- do not create canary changelog files
- do not create canary GitHub Releases
## Step 1 — Decide the Stable Version ## Step 1 — Choose the Candidate
Start the release train first: For canary validation:
- inspect the latest successful canary run on `master`
- record the canary version and source SHA
For stable promotion:
1. choose the tested source ref
2. confirm it is the exact SHA you want to promote
3. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
Useful commands:
```bash ```bash
./scripts/release-start.sh {patch|minor|major} git tag --list 'v*' --sort=-version:refname | head -1
git log --oneline --no-merges
npm view paperclipai@canary version
``` ```
Then run release preflight:
```bash
./scripts/release-preflight.sh canary {patch|minor|major}
# or
./scripts/release-preflight.sh stable {patch|minor|major}
```
Then use the last stable tag as the base:
```bash
LAST_TAG=$(git tag --list 'v*' --sort=-version:refname | head -1)
git log "${LAST_TAG}..HEAD" --oneline --no-merges
git diff --name-only "${LAST_TAG}..HEAD" -- packages/db/src/migrations/
git diff "${LAST_TAG}..HEAD" -- packages/db/src/schema/
git log "${LAST_TAG}..HEAD" --format="%s" | rg -n 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true
```
Bump policy:
- destructive migrations, removed APIs, breaking config changes -> `major`
- additive migrations or clearly user-visible features -> at least `minor`
- fixes only -> `patch`
If the requested bump is too low, escalate it and explain why.
## Step 2 — Draft the Stable Changelog ## Step 2 — Draft the Stable Changelog
Invoke `release-changelog` and generate: Stable changelog files live at:
- `releases/vX.Y.Z.md` - `releases/vYYYY.MDD.P.md`
Invoke `release-changelog` and generate or update the stable notes only.
Rules: Rules:
- review the draft with a human before publish - review the draft with a human before publish
- preserve manual edits if the file already exists - preserve manual edits if the file already exists
- keep the heading and filename stable-only, for example `v1.2.3` - keep the filename stable-only
- do not create a separate canary changelog file - do not create a canary changelog file
## Step 3 — Verify the Release SHA ## Step 3 — Verify the Candidate SHA
Run the standard gate: Run the standard gate:
@@ -128,41 +116,27 @@ pnpm test:run
pnpm build pnpm build
``` ```
If the release will be run through GitHub Actions, the workflow can rerun this gate. Still report whether the local tree currently passes. If the GitHub release workflow will run the publish, it can rerun this gate. Still report local status if you checked it.
The GitHub Actions release workflow installs with `pnpm install --frozen-lockfile`. Treat that as a release invariant, not a nuisance: if manifests changed and the lockfile refresh PR has not landed yet, stop and wait for `master` to contain the committed lockfile before shipping. For PRs that touch release logic, the repo also runs a canary release dry-run in CI. That is a release-specific guard, not a substitute for the standard gate.
## Step 4 — Publish a Canary ## Step 4 — Validate the Canary
Run from the `release/X.Y.Z` branch: The normal canary path is automatic from `master` via:
```bash - `.github/workflows/release.yml`
./scripts/release.sh {patch|minor|major} --canary --dry-run
./scripts/release.sh {patch|minor|major} --canary
```
What this means: Confirm:
- npm receives `X.Y.Z-canary.N` under dist-tag `canary` 1. verification passed
- `latest` remains unchanged 2. npm canary publish succeeded
- no git tag is created 3. git tag `canary/vYYYY.MDD.P-canary.N` exists
- the script cleans the working tree afterward
Guard: Useful checks:
- if the current stable is `0.2.7`, the next patch canary is `0.2.8-canary.0`
- the tooling must never publish `0.2.7-canary.N` after `0.2.7` is already stable
After publish, verify:
```bash ```bash
npm view paperclipai@canary version npm view paperclipai@canary version
``` git tag --list 'canary/v*' --sort=-version:refname | head -5
The user install path is:
```bash
npx paperclipai@canary onboard
``` ```
## Step 5 — Smoke Test the Canary ## Step 5 — Smoke Test the Canary
@@ -173,60 +147,70 @@ Run:
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
``` ```
Useful isolated variant:
```bash
HOST_PORT=3232 DATA_DIR=./data/release-smoke-canary PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
```
Confirm: Confirm:
1. install succeeds 1. install succeeds
2. onboarding completes 2. onboarding completes without crashes
3. server boots 3. the server boots
4. UI loads 4. the UI loads
5. basic company/dashboard flow works 5. basic company creation and dashboard load work
If smoke testing fails: If smoke testing fails:
- stop the stable release - stop the stable release
- fix the issue - fix the issue on `master`
- publish another canary - wait for the next automatic canary
- repeat the smoke test - rerun smoke testing
Each retry should create a higher canary ordinal, while the stable target version can stay the same. ## Step 6 — Preview or Publish Stable
## Step 6 — Publish Stable The normal stable path is manual `workflow_dispatch` on:
Once the SHA is vetted, run: - `.github/workflows/release.yml`
Inputs:
- `source_ref`
- `stable_date`
- `dry_run`
Before live stable:
1. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
2. ensure `releases/vYYYY.MDD.P.md` exists on the source ref
3. run the stable workflow in dry-run mode first when practical
4. then run the real stable publish
The stable workflow:
- re-verifies the exact source ref
- computes the next stable patch slot for the chosen UTC date
- publishes `YYYY.MDD.P` under dist-tag `latest`
- creates git tag `vYYYY.MDD.P`
- creates or updates the GitHub Release from `releases/vYYYY.MDD.P.md`
Local emergency/manual commands:
```bash ```bash
./scripts/release.sh {patch|minor|major} --dry-run ./scripts/release.sh stable --dry-run
./scripts/release.sh {patch|minor|major} ./scripts/release.sh stable
git push public-gh refs/tags/vYYYY.MDD.P
./scripts/create-github-release.sh YYYY.MDD.P
``` ```
Stable publish does this: ## Step 7 — Finish the Other Surfaces
- publishes `X.Y.Z` to npm under `latest`
- creates the local release commit
- creates the local git tag `vX.Y.Z`
Stable publish does **not** push the release for you.
## Step 7 — Push and Create GitHub Release
After stable publish succeeds:
```bash
git push public-gh HEAD --follow-tags
./scripts/create-github-release.sh X.Y.Z
```
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
Create or verify follow-up work for: Create or verify follow-up work for:
- website changelog publishing - website changelog publishing
- launch post / social announcement - launch post / social announcement
- any release summary in Paperclip issue context - release summary in Paperclip issue context
These should reference the stable release, not the canary. These should reference the stable release, not the canary.
@@ -236,9 +220,9 @@ If the canary is bad:
- publish another canary, do not ship stable - publish another canary, do not ship stable
If stable npm publish succeeds but push or GitHub release creation fails: If stable npm publish succeeds but tag push or GitHub release creation fails:
- fix the git/GitHub issue immediately from the same checkout - fix the git/GitHub issue immediately from the same release result
- do not republish the same version - do not republish the same version
If `latest` is bad after stable publish: If `latest` is bad after stable publish:
@@ -247,15 +231,17 @@ If `latest` is bad after stable publish:
./scripts/rollback-latest.sh <last-good-version> ./scripts/rollback-latest.sh <last-good-version>
``` ```
Then fix forward with a new patch release. Then fix forward with a new stable release.
## Output ## Output
When the skill completes, provide: When the skill completes, provide:
- stable version and, if relevant, the final canary version tested - candidate SHA and tested canary version, if relevant
- stable version, if promoted
- verification status - verification status
- npm status - npm status
- smoke-test status
- git tag / GitHub Release status - git tag / GitHub Release status
- website / announcement follow-up status - website / announcement follow-up status
- rollback recommendation if anything is still partially complete - rollback recommendation if anything is still partially complete

View File

@@ -12,7 +12,7 @@ on:
type: string type: string
default: master default: master
stable_date: stable_date:
description: Stable release date in UTC (YYYY-MM-DD). Defaults to today. description: Stable release date in UTC (YYYY-MM-DD). First stable that day is .0, then .1, and so on.
required: false required: false
type: string type: string
dry_run: dry_run:

View File

@@ -69,13 +69,13 @@ Those rewrites are temporary. The working tree is restored after publish or dry-
Paperclip uses calendar versions: Paperclip uses calendar versions:
- stable: `YYYY.M.D` - stable: `YYYY.MDD.P`
- canary: `YYYY.M.D-canary.N` - canary: `YYYY.MDD.P-canary.N`
Examples: Examples:
- stable: `2026.3.17` - stable: `2026.318.0`
- canary: `2026.3.17-canary.2` - canary: `2026.318.1-canary.2`
## Publish model ## Publish model
@@ -85,7 +85,7 @@ Canaries publish under the npm dist-tag `canary`.
Example: Example:
- `paperclipai@2026.3.17-canary.2` - `paperclipai@2026.318.1-canary.2`
This keeps the default install path unchanged while allowing explicit installs with: This keeps the default install path unchanged while allowing explicit installs with:
@@ -99,13 +99,13 @@ Stable publishes use the npm dist-tag `latest`.
Example: Example:
- `paperclipai@2026.3.17` - `paperclipai@2026.318.0`
Stable publishes do not create a release commit. Instead: Stable publishes do not create a release commit. Instead:
- package versions are rewritten temporarily - package versions are rewritten temporarily
- packages are published from the chosen source commit - packages are published from the chosen source commit
- git tag `vYYYY.M.D` points at that original commit - git tag `vYYYY.MDD.P` points at that original commit
## Trusted publishing ## Trusted publishing
@@ -126,7 +126,7 @@ Rollback does not unpublish anything.
It repoints the `latest` dist-tag to a prior stable version: It repoints the `latest` dist-tag to a prior stable version:
```bash ```bash
./scripts/rollback-latest.sh 2026.3.16 ./scripts/rollback-latest.sh 2026.318.0
``` ```
This is the fastest way to restore the default install path if a stable release is bad. This is the fastest way to restore the default install path if a stable release is bad.

View File

@@ -205,7 +205,7 @@ After setup:
3. confirm it passes verification 3. confirm it passes verification
4. confirm publish succeeds under the `npm-canary` environment 4. confirm publish succeeds under the `npm-canary` environment
5. confirm npm now shows a new `canary` release 5. confirm npm now shows a new `canary` release
6. confirm a git tag named `canary/vYYYY.M.D-canary.N` was pushed 6. confirm a git tag named `canary/vYYYY.MDD.P-canary.N` was pushed
Install-path check: Install-path check:
@@ -217,18 +217,19 @@ npx paperclipai@canary onboard
After at least one good canary exists: After at least one good canary exists:
1. prepare `releases/vYYYY.M.D.md` on the source commit you want to promote 1. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
2. open `Actions` -> `Release` 2. prepare `releases/vYYYY.MDD.P.md` on the source commit you want to promote
3. run it with: 3. open `Actions` -> `Release`
4. run it with:
- `source_ref`: the tested commit SHA or canary tag source commit - `source_ref`: the tested commit SHA or canary tag source commit
- `stable_date`: leave blank or set the intended UTC date - `stable_date`: leave blank or set the intended UTC date
- `dry_run`: `true` - `dry_run`: `true`
4. confirm the dry-run succeeds 5. confirm the dry-run succeeds
5. rerun with `dry_run: false` 6. rerun with `dry_run: false`
6. approve the `npm-stable` environment when prompted 7. approve the `npm-stable` environment when prompted
7. confirm npm `latest` points to the new stable version 8. confirm npm `latest` points to the new stable version
8. confirm git tag `vYYYY.M.D` exists 9. confirm git tag `vYYYY.MDD.P` exists
9. confirm the GitHub Release was created 10. confirm the GitHub Release was created
## 13. Suggested Maintainer Policy ## 13. Suggested Maintainer Policy

View File

@@ -6,26 +6,29 @@ The release model is now commit-driven:
1. Every push to `master` publishes a canary automatically. 1. Every push to `master` publishes a canary automatically.
2. Stable releases are manually promoted from a chosen tested commit or canary tag. 2. Stable releases are manually promoted from a chosen tested commit or canary tag.
3. Stable release notes live in `releases/vYYYY.M.D.md`. 3. Stable release notes live in `releases/vYYYY.MDD.P.md`.
4. Only stable releases get GitHub Releases. 4. Only stable releases get GitHub Releases.
## Versioning Model ## Versioning Model
Paperclip uses calendar versions that still fit semver syntax: Paperclip uses calendar versions that still fit semver syntax:
- stable: `YYYY.M.D` - stable: `YYYY.MDD.P`
- canary: `YYYY.M.D-canary.N` - canary: `YYYY.MDD.P-canary.N`
Examples: Examples:
- stable on March 17, 2026: `2026.3.17` - first stable on March 18, 2026: `2026.318.0`
- fourth canary on March 17, 2026: `2026.3.17-canary.3` - second stable on March 18, 2026: `2026.318.1`
- fourth canary for the `2026.318.1` line: `2026.318.1-canary.3`
Important constraints: Important constraints:
- do not use leading zeroes such as `2026.03.17` - the middle numeric slot is `MDD`, where `M` is the UTC month and `DD` is the zero-padded UTC day
- do not use four numeric segments such as `2026.03.17.1` - use `2026.303.0` for March 3, not `2026.33.0`
- the semver-safe canary form is `2026.3.17-canary.1` - do not use leading zeroes such as `2026.0318.0`
- do not use four numeric segments such as `2026.3.18.1`
- the semver-safe canary form is `2026.318.0-canary.1`
## Release Surfaces ## Release Surfaces
@@ -45,7 +48,7 @@ Canaries only cover the first two surfaces plus an internal traceability tag.
- canaries publish from `master` - canaries publish from `master`
- stables publish from an explicitly chosen source ref - stables publish from an explicitly chosen source ref
- tags point at the original source commit, not a generated release commit - tags point at the original source commit, not a generated release commit
- stable notes are always `releases/vYYYY.M.D.md` - stable notes are always `releases/vYYYY.MDD.P.md`
- canaries never create GitHub Releases - canaries never create GitHub Releases
- canaries never require changelog generation - canaries never require changelog generation
@@ -60,7 +63,7 @@ It:
- verifies the pushed commit - verifies the pushed commit
- computes the canary version for the current UTC date - computes the canary version for the current UTC date
- publishes under npm dist-tag `canary` - publishes under npm dist-tag `canary`
- creates a git tag `canary/vYYYY.M.D-canary.N` - creates a git tag `canary/vYYYY.MDD.P-canary.N`
Users install canaries with: Users install canaries with:
@@ -84,15 +87,17 @@ Inputs:
Before running stable: Before running stable:
1. pick the canary commit or tag you trust 1. pick the canary commit or tag you trust
2. create or update `releases/vYYYY.M.D.md` on that source ref 2. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
3. run the stable workflow from that source ref 3. create or update `releases/vYYYY.MDD.P.md` on that source ref
4. run the stable workflow from that source ref
The workflow: The workflow:
- re-verifies the exact source ref - re-verifies the exact source ref
- publishes `YYYY.M.D` under npm dist-tag `latest` - computes the next stable patch slot for the chosen UTC date
- creates git tag `vYYYY.M.D` - publishes `YYYY.MDD.P` under npm dist-tag `latest`
- creates or updates the GitHub Release from `releases/vYYYY.M.D.md` - creates git tag `vYYYY.MDD.P`
- creates or updates the GitHub Release from `releases/vYYYY.MDD.P.md`
## Local Commands ## Local Commands
@@ -114,22 +119,22 @@ This is mainly for emergency/manual use. The normal path is the GitHub workflow.
```bash ```bash
./scripts/release.sh stable ./scripts/release.sh stable
git push public-gh refs/tags/vYYYY.M.D git push public-gh refs/tags/vYYYY.MDD.P
./scripts/create-github-release.sh YYYY.M.D ./scripts/create-github-release.sh YYYY.MDD.P
``` ```
## Stable Changelog Workflow ## Stable Changelog Workflow
Stable changelog files live at: Stable changelog files live at:
- `releases/vYYYY.M.D.md` - `releases/vYYYY.MDD.P.md`
Canaries do not get changelog files. Canaries do not get changelog files.
Recommended local generation flow: Recommended local generation flow:
```bash ```bash
VERSION=2026.3.17 VERSION="$(./scripts/release.sh stable --date 2026-03-18 --print-version)"
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 .agents/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 .agents/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."
``` ```
@@ -175,11 +180,11 @@ Rollback does not unpublish versions.
It only moves the `latest` dist-tag back to a previous stable: It only moves the `latest` dist-tag back to a previous stable:
```bash ```bash
./scripts/rollback-latest.sh 2026.3.16 --dry-run ./scripts/rollback-latest.sh 2026.318.0 --dry-run
./scripts/rollback-latest.sh 2026.3.16 ./scripts/rollback-latest.sh 2026.318.0
``` ```
Then fix forward with a new stable release date. Then fix forward with a new stable patch slot or release date.
## Failure Playbooks ## Failure Playbooks
@@ -201,8 +206,8 @@ This is a partial release. npm is already live.
Do this immediately: Do this immediately:
1. push the missing tag 1. push the missing tag
2. rerun `./scripts/create-github-release.sh YYYY.M.D` 2. rerun `./scripts/create-github-release.sh YYYY.MDD.P`
3. verify the GitHub Release notes point at `releases/vYYYY.M.D.md` 3. verify the GitHub Release notes point at `releases/vYYYY.MDD.P.md`
Do not republish the same version. Do not republish the same version.
@@ -211,7 +216,7 @@ Do not republish the same version.
Roll back the dist-tag: Roll back the dist-tag:
```bash ```bash
./scripts/rollback-latest.sh YYYY.M.D ./scripts/rollback-latest.sh YYYY.MDD.P
``` ```
Then fix forward with a new stable release. Then fix forward with a new stable release.

View File

@@ -49,13 +49,13 @@ The repo and npm tooling still assume semver-shaped version strings in many plac
Recommended format: Recommended format:
- stable: `YYYY.M.D` - stable: `YYYY.MDD.P`
- canary: `YYYY.M.D-canary.N` - canary: `YYYY.MDD.P-canary.N`
Examples: Examples:
- stable on March 17, 2026: `2026.3.17` - first stable on March 17, 2026: `2026.317.0`
- third canary on March 17, 2026: `2026.3.17-canary.2` - third canary on the `2026.317.0` line: `2026.317.0-canary.2`
Why this shape: Why this shape:
@@ -66,11 +66,12 @@ Why this shape:
Important constraints: Important constraints:
- the middle numeric slot should be `MDD`, where `M` is the month and `DD` is the zero-padded day
- `2026.03.17` is not the format to use - `2026.03.17` is not the format to use
- numeric semver identifiers do not allow leading zeroes - numeric semver identifiers do not allow leading zeroes
- `2026.03.16.8` is not the format to use - `2026.3.17.1` is not the format to use
- semver has three numeric components, not four - semver has three numeric components, not four
- the practical semver-safe equivalent of your example is `2026.3.16-canary.8` - the practical semver-safe equivalent is `2026.317.0-canary.8`
This is effectively CalVer on semver rails. This is effectively CalVer on semver rails.
@@ -109,7 +110,7 @@ This is the most important mechanical constraint.
npm can move dist-tags, but it does not let you rename an already-published version. That means: npm can move dist-tags, but it does not let you rename an already-published version. That means:
- you can move `latest` to `paperclipai@1.2.3` - you can move `latest` to `paperclipai@1.2.3`
- you cannot turn `paperclipai@2026.3.16-canary.8` into `paperclipai@2026.3.17` - you cannot turn `paperclipai@2026.317.0-canary.8` into `paperclipai@2026.317.0`
So "promote canary to stable" really means: So "promote canary to stable" really means:
@@ -123,7 +124,7 @@ Recommended stable input:
- `source_ref` - `source_ref`
- commit SHA, or - commit SHA, or
- a canary git tag such as `canary/v2026.3.16-canary.8` - a canary git tag such as `canary/v2026.317.1-canary.8`
### 5. Only stable releases get release notes, tags, and GitHub Releases ### 5. Only stable releases get release notes, tags, and GitHub Releases
@@ -137,9 +138,9 @@ Canaries should stay lightweight:
Stable releases should remain the public narrative surface: Stable releases should remain the public narrative surface:
- git tag `v2026.3.17` - git tag `v2026.317.0`
- GitHub Release `v2026.3.17` - GitHub Release `v2026.317.0`
- stable changelog file `releases/v2026.3.17.md` - stable changelog file `releases/v2026.317.0.md`
## Security Model ## Security Model
@@ -233,14 +234,14 @@ Recommended stable path:
1. pick a canary commit or tag 1. pick a canary commit or tag
2. run changelog generation locally from a trusted machine 2. run changelog generation locally from a trusted machine
3. commit `releases/vYYYY.M.D.md` 3. commit `releases/vYYYY.MDD.P.md`
4. run stable promotion 4. run stable promotion
If the notes are not ready yet, a fallback is acceptable: If the notes are not ready yet, a fallback is acceptable:
- publish stable - publish stable
- create a minimal GitHub Release - create a minimal GitHub Release
- update `releases/vYYYY.M.D.md` immediately afterward - update `releases/vYYYY.MDD.P.md` immediately afterward
But the better steady-state is to have the stable notes committed before stable publish. But the better steady-state is to have the stable notes committed before stable publish.
@@ -268,13 +269,13 @@ Steps:
1. checkout the merged `master` commit 1. checkout the merged `master` commit
2. run verification on that exact commit 2. run verification on that exact commit
3. compute canary version for current UTC date 3. compute canary version for current UTC date
4. version public packages to `YYYY.M.D-canary.N` 4. version public packages to `YYYY.MDD.P-canary.N`
5. publish to npm with dist-tag `canary` 5. publish to npm with dist-tag `canary`
6. create a canary git tag for traceability 6. create a canary git tag for traceability
Recommended canary tag format: Recommended canary tag format:
- `canary/v2026.3.17-canary.4` - `canary/v2026.317.1-canary.4`
Outputs: Outputs:
@@ -299,14 +300,14 @@ Steps:
1. checkout `source_ref` 1. checkout `source_ref`
2. run verification on that exact commit 2. run verification on that exact commit
3. compute stable version from UTC date or provided override 3. compute the next stable patch slot for the UTC date or provided override
4. fail if `vYYYY.M.D` already exists 4. fail if `vYYYY.MDD.P` already exists
5. require `releases/vYYYY.M.D.md` 5. require `releases/vYYYY.MDD.P.md`
6. version public packages to `YYYY.M.D` 6. version public packages to `YYYY.MDD.P`
7. publish to npm under `latest` 7. publish to npm under `latest`
8. create git tag `vYYYY.M.D` 8. create git tag `vYYYY.MDD.P`
9. push tag 9. push tag
10. create GitHub Release from `releases/vYYYY.M.D.md` 10. create GitHub Release from `releases/vYYYY.MDD.P.md`
Outputs: Outputs:
@@ -332,8 +333,8 @@ That logic should be replaced with:
For example: For example:
- `stable_version_for_utc_date(2026-03-17) -> 2026.3.17` - `next_stable_version(2026-03-17) -> 2026.317.0`
- `next_canary_for_utc_date(2026-03-17) -> 2026.3.17-canary.0` - `next_canary_for_utc_date(2026-03-17) -> 2026.317.0-canary.0`
### 2. Stop requiring `release/X.Y.Z` ### 2. Stop requiring `release/X.Y.Z`
@@ -392,19 +393,15 @@ It should continue to:
## Tradeoffs and Risks ## Tradeoffs and Risks
### 1. One stable per UTC day ### 1. The stable patch slot is now part of the version contract
With plain `YYYY.M.D`, you get one stable release per UTC day. With `YYYY.MDD.P`, same-day hotfixes are supported, but the stable patch slot is now part of the visible version format.
That is probably fine, but it is a real product rule. That is the right tradeoff because:
If you need multiple same-day stables later, you have three options: 1. npm still gets semver-valid versions
2. same-day hotfixes stay possible
1. accept a less pretty stable format 3. chronological ordering still works as long as the day is zero-padded inside `MDD`
2. go back to a serial patch component
3. keep daily stable cadence and use canaries for same-day fixes
My recommendation is to accept one stable per UTC day unless reality proves otherwise.
### 2. Public package consumers lose semver intent signaling ### 2. Public package consumers lose semver intent signaling
@@ -469,8 +466,8 @@ That is acceptable if canaries stay clearly separate:
Paperclip should adopt this model: Paperclip should adopt this model:
- stable versions: `YYYY.M.D` - stable versions: `YYYY.MDD.P`
- canary versions: `YYYY.M.D-canary.N` - canary versions: `YYYY.MDD.P-canary.N`
- canaries auto-published on every push to `master` - canaries auto-published on every push to `master`
- stables manually promoted from a chosen tested commit or canary tag - stables manually promoted from a chosen tested commit or canary tag
- no release branches in the default path - no release branches in the default path

View File

@@ -14,8 +14,8 @@ Usage:
./scripts/create-github-release.sh <version> [--dry-run] ./scripts/create-github-release.sh <version> [--dry-run]
Examples: Examples:
./scripts/create-github-release.sh 2026.3.17 ./scripts/create-github-release.sh 2026.318.0
./scripts/create-github-release.sh 2026.3.17 --dry-run ./scripts/create-github-release.sh 2026.318.0 --dry-run
Notes: Notes:
- Run this after pushing the stable tag. - Run this after pushing the stable tag.
@@ -48,7 +48,7 @@ if [ -z "$version" ]; then
fi fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: version must be a stable calendar version like 2026.3.17." >&2 echo "Error: version must be a stable calendar version like 2026.318.0." >&2
exit 1 exit 1
fi fi

View File

@@ -107,7 +107,7 @@ get_current_stable_version() {
fi fi
} }
stable_version_for_date() { stable_version_slot_for_date() {
node - "${1:-}" <<'NODE' node - "${1:-}" <<'NODE'
const input = process.argv[2]; const input = process.argv[2];
@@ -117,7 +117,10 @@ if (Number.isNaN(date.getTime())) {
process.exit(1); process.exit(1);
} }
process.stdout.write(`${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}`); const month = String(date.getUTCMonth() + 1);
const day = String(date.getUTCDate()).padStart(2, '0');
process.stdout.write(`${date.getUTCFullYear()}.${month}${day}`);
NODE NODE
} }
@@ -131,6 +134,53 @@ process.stdout.write(`${y}-${m}-${d}`);
NODE NODE
} }
next_stable_version() {
local release_date="$1"
shift
node - "$release_date" "$@" <<'NODE'
const input = process.argv[2];
const packageNames = process.argv.slice(3);
const { execSync } = require("node:child_process");
const date = input ? new Date(`${input}T00:00:00Z`) : new Date();
if (Number.isNaN(date.getTime())) {
console.error(`invalid date: ${input}`);
process.exit(1);
}
const stableSlot = `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}${String(date.getUTCDate()).padStart(2, "0")}`;
const pattern = new RegExp(`^${stableSlot.replace(/\./g, '\\.')}\.(\\d+)$`);
let max = -1;
for (const packageName of packageNames) {
let versions = [];
try {
const raw = execSync(`npm view ${JSON.stringify(packageName)} versions --json`, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (raw) {
const parsed = JSON.parse(raw);
versions = Array.isArray(parsed) ? parsed : [parsed];
}
} catch {
versions = [];
}
for (const version of versions) {
const match = version.match(pattern);
if (!match) continue;
max = Math.max(max, Number(match[1]));
}
}
process.stdout.write(`${stableSlot}.${max + 1}`);
NODE
}
next_canary_version() { next_canary_version() {
local stable_version="$1" local stable_version="$1"
shift shift
@@ -159,7 +209,7 @@ for (const packageName of packageNames) {
} catch { } catch {
versions = []; versions = [];
} }
for (const version of versions) { for (const version of versions) {
const match = version.match(pattern); const match = version.match(pattern);
if (!match) continue; if (!match) continue;

View File

@@ -10,6 +10,7 @@ channel=""
release_date="" release_date=""
dry_run=false dry_run=false
skip_verify=false skip_verify=false
print_version_only=false
tag_name="" tag_name=""
cleanup_on_exit=false cleanup_on_exit=false
@@ -17,20 +18,23 @@ cleanup_on_exit=false
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Usage:
./scripts/release.sh <canary|stable> [--date YYYY-MM-DD] [--dry-run] [--skip-verify] ./scripts/release.sh <canary|stable> [--date YYYY-MM-DD] [--dry-run] [--skip-verify] [--print-version]
Examples: Examples:
./scripts/release.sh canary ./scripts/release.sh canary
./scripts/release.sh canary --date 2026-03-17 --dry-run ./scripts/release.sh canary --date 2026-03-17 --dry-run
./scripts/release.sh stable ./scripts/release.sh stable
./scripts/release.sh stable --date 2026-03-17 --dry-run ./scripts/release.sh stable --date 2026-03-17 --dry-run
./scripts/release.sh stable --date 2026-03-18 --print-version
Notes: Notes:
- Canary releases publish YYYY.M.D-canary.N under the npm dist-tag "canary" - Stable versions use YYYY.MDD.P, where M is the UTC month, DD is the
and create the git tag canary/vYYYY.M.D-canary.N. zero-padded UTC day, and P is the same-day stable patch slot.
- Stable releases publish YYYY.M.D under the npm dist-tag "latest" and create - Canary releases publish YYYY.MDD.P-canary.N under the npm dist-tag
the git tag vYYYY.M.D. "canary" and create the git tag canary/vYYYY.MDD.P-canary.N.
- Stable release notes must already exist at releases/vYYYY.M.D.md. - Stable releases publish YYYY.MDD.P under the npm dist-tag "latest" and
create the git tag vYYYY.MDD.P.
- Stable release notes must already exist at releases/vYYYY.MDD.P.md.
- The script rewrites versions temporarily and restores the working tree on - The script rewrites versions temporarily and restores the working tree on
exit. Tags always point at the original source commit, not a generated exit. Tags always point at the original source commit, not a generated
release commit. release commit.
@@ -94,6 +98,7 @@ while [ $# -gt 0 ]; do
;; ;;
--dry-run) dry_run=true ;; --dry-run) dry_run=true ;;
--skip-verify) skip_verify=true ;; --skip-verify) skip_verify=true ;;
--print-version) print_version_only=true ;;
-h|--help) -h|--help)
usage usage
exit 0 exit 0
@@ -118,15 +123,20 @@ CURRENT_SHA="$(git -C "$REPO_ROOT" rev-parse HEAD)"
LAST_STABLE_TAG="$(get_last_stable_tag)" LAST_STABLE_TAG="$(get_last_stable_tag)"
CURRENT_STABLE_VERSION="$(get_current_stable_version)" CURRENT_STABLE_VERSION="$(get_current_stable_version)"
RELEASE_DATE="${release_date:-$(utc_date_iso)}" RELEASE_DATE="${release_date:-$(utc_date_iso)}"
TARGET_STABLE_VERSION="$(stable_version_for_date "$RELEASE_DATE")"
TARGET_PUBLISH_VERSION="$TARGET_STABLE_VERSION"
DIST_TAG="latest"
PUBLIC_PACKAGE_INFO="$(list_public_package_info)" PUBLIC_PACKAGE_INFO="$(list_public_package_info)"
mapfile -t PUBLIC_PACKAGE_NAMES < <(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f2) PUBLIC_PACKAGE_NAMES=()
while IFS= read -r package_name; do
[ -n "$package_name" ] || continue
PUBLIC_PACKAGE_NAMES+=("$package_name")
done < <(printf '%s\n' "$PUBLIC_PACKAGE_INFO" | cut -f2)
[ -n "$PUBLIC_PACKAGE_INFO" ] || release_fail "no public packages were found in the workspace." [ -n "$PUBLIC_PACKAGE_INFO" ] || release_fail "no public packages were found in the workspace."
TARGET_STABLE_VERSION="$(next_stable_version "$RELEASE_DATE" "${PUBLIC_PACKAGE_NAMES[@]}")"
TARGET_PUBLISH_VERSION="$TARGET_STABLE_VERSION"
DIST_TAG="latest"
if [ "$channel" = "canary" ]; then if [ "$channel" = "canary" ]; then
require_on_master_branch require_on_master_branch
TARGET_PUBLISH_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION" "${PUBLIC_PACKAGE_NAMES[@]}")" TARGET_PUBLISH_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION" "${PUBLIC_PACKAGE_NAMES[@]}")"
@@ -136,6 +146,11 @@ else
tag_name="$(stable_tag_name "$TARGET_STABLE_VERSION")" tag_name="$(stable_tag_name "$TARGET_STABLE_VERSION")"
fi fi
if [ "$print_version_only" = true ]; then
printf '%s\n' "$TARGET_PUBLISH_VERSION"
exit 0
fi
NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")" NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")"
require_clean_worktree require_clean_worktree

View File

@@ -12,8 +12,8 @@ Usage:
./scripts/rollback-latest.sh <stable-version> [--dry-run] ./scripts/rollback-latest.sh <stable-version> [--dry-run]
Examples: Examples:
./scripts/rollback-latest.sh 2026.3.17 ./scripts/rollback-latest.sh 2026.318.0
./scripts/rollback-latest.sh 2026.3.17 --dry-run ./scripts/rollback-latest.sh 2026.318.0 --dry-run
Notes: Notes:
- This repoints the npm dist-tag "latest" for every public package. - This repoints the npm dist-tag "latest" for every public package.
@@ -45,7 +45,7 @@ if [ -z "$version" ]; then
fi fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: version must be a stable calendar version like 2026.3.17." >&2 echo "Error: version must be a stable calendar version like 2026.318.0." >&2
exit 1 exit 1
fi fi