doc/plans/2026-03-17-release-automation-and-versioning.md
Paperclip's current release flow is documented in doc/RELEASING.md and implemented through:
.github/workflows/release.ymlscripts/release-lib.shscripts/release-start.shscripts/release-preflight.shscripts/release.shscripts/create-github-release.shToday the model is:
patch, minor, or majorrelease/X.Y.Zreleases/vX.Y.Z.mdmasterThat is workable, but it creates friction in exactly the places that should be cheap:
patch vs minor vs majorThe target state from this discussion is simpler:
master publishes a canary automaticallyMove Paperclip to semver-compatible calendar versioning, auto-publish canaries from master, promote stable from a chosen tested commit, and use npm trusted publishing plus GitHub environments so no long-lived npm or LLM token needs to live in Actions.
The repo and npm tooling still assume semver-shaped version strings in many places. That does not mean Paperclip must keep semver as a product policy. It does mean the version format should remain semver-valid.
Recommended format:
YYYY.MDD.PYYYY.MDD.P-canary.NExamples:
2026.317.02026.317.0 line: 2026.317.0-canary.2Why this shape:
patch/minor/major decisionsImportant constraints:
MDD, where M is the month and DD is the zero-padded day2026.03.17 is not the format to use
2026.3.17.1 is not the format to use
2026.317.0-canary.8This is effectively CalVer on semver rails.
This is not semver in spirit anymore. It is semver in syntax only.
That tradeoff is probably acceptable for Paperclip, but it should be explicit:
major/minor/patchThis is especially relevant for public library packages like @paperclipai/shared, @paperclipai/db, and the adapter packages.
If every merge to master publishes a canary, the current release/X.Y.Z train model becomes more ceremony than value.
Recommended replacement:
master is the only canary trainmaster can publish a canarymasterThis matches the workflow you actually want:
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:
latest to [email protected][email protected] into [email protected]So "promote canary to stable" really means:
Because of that, the stable workflow should take a source ref, not just a bump type.
Recommended stable input:
source_ref
canary/v2026.317.1-canary.8Canaries should stay lightweight:
canaryreleases/v*.mdStable releases should remain the public narrative surface:
v2026.317.0v2026.317.0releases/v2026.317.0.mdUse npm trusted publishing with GitHub Actions OIDC, then disable token-based publishing access for the packages.
Why:
NPM_TOKEN in repo or org secretsThis is the cleanest answer to the open-repo security concern.
Use one workflow filename for both canary and stable publishing:
.github/workflows/release.ymlWhy:
Recommended environments:
npm-canarynpm-stableRecommended policy:
npm-canary
masternpm-stable
masterStable should require an explicit second human gate even if the workflow is manually dispatched.
Add or tighten CODEOWNERS coverage for:
.github/workflows/*scripts/release*doc/RELEASING.mdThis matters because trusted publishing authorizes a workflow file. The biggest remaining risk is not secret exfiltration from forks. It is a maintainer-approved change to the release workflow itself.
After trusted publishing is verified:
That eliminates the "someone stole the npm token" class of failure.
pull_request_targetGenerate stable changelogs only, and keep LLM-assisted changelog generation out of CI for now.
Reasoning:
Recommended stable path:
releases/vYYYY.MDD.P.mdIf the notes are not ready yet, a fallback is acceptable:
releases/vYYYY.MDD.P.md immediately afterwardBut the better steady-state is to have the stable notes committed before stable publish.
If you later want CI-assisted changelog drafting, do it with:
That is phase-two hardening work, not a phase-one requirement.
Trigger:
push on masterSteps:
master commitYYYY.MDD.P-canary.NcanaryRecommended canary tag format:
canary/v2026.317.1-canary.4Outputs:
Trigger:
workflow_dispatchInputs:
source_refstable_datedry_runSteps:
source_refvYYYY.MDD.P already existsreleases/vYYYY.MDD.P.mdYYYY.MDD.PlatestvYYYY.MDD.Preleases/vYYYY.MDD.P.mdOutputs:
The current release scripts depend on:
patchminormajorThat logic should be replaced with:
compute_canary_version_for_datecompute_stable_version_for_dateFor example:
next_stable_version(2026-03-17) -> 2026.317.0next_canary_for_utc_date(2026-03-17) -> 2026.317.0-canary.0release/X.Y.ZThese current invariants should be removed from the happy path:
release/X.Y.Z"X.Y.Z come from the same release branch"release-start.shReplace them with:
mastersource_refThe current system uses Changesets to:
CHANGELOG.md filesWith CalVer, Changesets may still be useful for publish orchestration, but it should no longer own version selection.
Recommended implementation order:
changeset publish if it works with explicitly-set versionsPaperclip's release problem is now "publish the whole fixed package set at one explicit version", not "derive the next semantic bump from human intent".
Recommended new script:
scripts/set-release-version.mjsResponsibilities:
This is safer than keeping a bump-oriented changeset flow and then forcing it into a date-based scheme.
rollback-latest.sh should stay, but it should stop assuming a semver meaning beyond syntax.
It should continue to:
latest to a prior stable versionWith YYYY.MDD.P, same-day hotfixes are supported, but the stable patch slot is now part of the visible version format.
That is the right tradeoff because:
MDDThis is the main downside of CalVer.
If that becomes a problem, one alternative is:
That is more complex operationally, so I would not start there unless package consumers actually need it.
Publishing on every master merge means:
That is acceptable if canaries stay clearly separate:
canaryrelease.ymlnpm-canary and npm-stable environmentsCODEOWNERS protection for release filespush to masterdoc/RELEASING.mdsource_refrelease-start.sh from the primary pathpatch/minor/major from maintainer docsPaperclip should adopt this model:
YYYY.MDD.PYYYY.MDD.P-canary.NmasterThat gets rid of the annoying part of semver without fighting npm, makes canaries cheap, keeps stables deliberate, and materially improves the security posture of the public repository.