chore: switch release calver to mdd patch
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: release-changelog
|
||||
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.
|
||||
---
|
||||
|
||||
@@ -9,20 +9,33 @@ description: >
|
||||
|
||||
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:
|
||||
|
||||
- `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
|
||||
|
||||
Before generating anything, check whether the file already exists:
|
||||
|
||||
```bash
|
||||
ls releases/v{version}.md 2>/dev/null
|
||||
ls releases/vYYYY.MDD.P.md 2>/dev/null
|
||||
```
|
||||
|
||||
If it exists:
|
||||
@@ -41,13 +54,14 @@ git tag --list 'v*' --sort=-version:refname | head -1
|
||||
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
|
||||
- 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -73,7 +87,6 @@ Look for:
|
||||
- destructive migrations
|
||||
- removed or changed API fields/endpoints
|
||||
- renamed or removed config keys
|
||||
- `major` changesets
|
||||
- `BREAKING:` or `BREAKING CHANGE:` commit signals
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -130,9 +144,9 @@ Rules:
|
||||
Template:
|
||||
|
||||
```markdown
|
||||
# v{version}
|
||||
# vYYYY.MDD.P
|
||||
|
||||
> Released: {YYYY-MM-DD}
|
||||
> Released: YYYY-MM-DD
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
||||
@@ -2,23 +2,21 @@
|
||||
name: release
|
||||
description: >
|
||||
Coordinate a full Paperclip release across engineering verification, npm,
|
||||
GitHub, website publishing, and announcement follow-up. Use when leadership
|
||||
asks to ship a release, not merely to discuss version bumps.
|
||||
GitHub, smoke testing, and announcement follow-up. Use when leadership asks
|
||||
to ship a release, not merely to discuss versioning.
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
- stable changelog drafting via `release-changelog`
|
||||
- release-train setup via `scripts/release-start.sh`
|
||||
- prerelease canary publishing via `scripts/release.sh --canary`
|
||||
- canary verification and publish status from `master`
|
||||
- Docker smoke testing via `scripts/docker-onboard-smoke.sh`
|
||||
- stable publishing via `scripts/release.sh`
|
||||
- pushing the stable branch commit and tag
|
||||
- GitHub Release creation via `scripts/create-github-release.sh`
|
||||
- manual stable promotion from a chosen source ref
|
||||
- GitHub Release creation
|
||||
- website / announcement follow-up tasks
|
||||
|
||||
## Trigger
|
||||
@@ -26,8 +24,9 @@ This skill coordinates:
|
||||
Use this skill when leadership asks for:
|
||||
|
||||
- "do a release"
|
||||
- "ship the next patch/minor/major"
|
||||
- "release vX.Y.Z"
|
||||
- "ship the release"
|
||||
- "promote this canary to stable"
|
||||
- "cut the stable release"
|
||||
|
||||
## Preconditions
|
||||
|
||||
@@ -35,10 +34,10 @@ Before proceeding, verify all of the following:
|
||||
|
||||
1. `.agents/skills/release-changelog/SKILL.md` exists and is usable.
|
||||
2. The repo working tree is clean, including untracked files.
|
||||
3. There are commits since the last stable tag.
|
||||
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` before the release branch is cut.
|
||||
6. npm publish rights are available locally, or the GitHub release workflow is being used with trusted publishing.
|
||||
3. There is at least one canary or candidate commit since the last stable tag.
|
||||
4. The candidate SHA has passed the verification gate or is about to.
|
||||
5. If manifests changed, the CI-owned `pnpm-lock.yaml` refresh is already merged on `master`.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- requested bump: `patch`, `minor`, or `major`
|
||||
- whether this run is a dry run or live release
|
||||
- whether the release is being run locally or from GitHub Actions
|
||||
- whether the target is a canary check or a stable promotion
|
||||
- the candidate `source_ref` for stable
|
||||
- whether the stable run is dry-run or live
|
||||
- release issue / company context for website and announcement follow-up
|
||||
|
||||
## 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`
|
||||
2. Draft the **stable** changelog as `releases/vX.Y.Z.md`
|
||||
3. Publish one or more **prerelease canaries** such as `X.Y.Z-canary.0`
|
||||
4. Smoke test the canary via Docker
|
||||
5. Publish the stable version `X.Y.Z`
|
||||
6. Push the stable branch commit and tag
|
||||
7. Create the GitHub Release
|
||||
8. Merge `release/X.Y.Z` back to `master` without squash or rebase
|
||||
9. Complete website and announcement surfaces
|
||||
1. every push to `master` publishes a canary automatically
|
||||
2. canaries use `YYYY.MDD.P-canary.N`
|
||||
3. stable releases use `YYYY.MDD.P`
|
||||
4. the middle slot is `MDD`, where `M` is the UTC month and `DD` is the zero-padded UTC day
|
||||
5. the stable patch slot increments when more than one stable ships on the same UTC date
|
||||
6. stable releases are manually promoted from a chosen tested commit or canary source commit
|
||||
7. only stable releases get `releases/vYYYY.MDD.P.md`, git tag `vYYYY.MDD.P`, and a GitHub Release
|
||||
|
||||
Critical consequence:
|
||||
Critical consequences:
|
||||
|
||||
- Canaries do **not** use promote-by-dist-tag anymore.
|
||||
- The changelog remains stable-only. Do not create `releases/vX.Y.Z-canary.N.md`.
|
||||
- do not use release branches as the default path
|
||||
- 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
|
||||
./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
|
||||
|
||||
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:
|
||||
|
||||
- review the draft with a human before publish
|
||||
- preserve manual edits if the file already exists
|
||||
- keep the heading and filename stable-only, for example `v1.2.3`
|
||||
- do not create a separate canary changelog file
|
||||
- keep the filename stable-only
|
||||
- do not create a canary changelog file
|
||||
|
||||
## Step 3 — Verify the Release SHA
|
||||
## Step 3 — Verify the Candidate SHA
|
||||
|
||||
Run the standard gate:
|
||||
|
||||
@@ -128,41 +116,27 @@ pnpm test:run
|
||||
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
|
||||
./scripts/release.sh {patch|minor|major} --canary --dry-run
|
||||
./scripts/release.sh {patch|minor|major} --canary
|
||||
```
|
||||
- `.github/workflows/release.yml`
|
||||
|
||||
What this means:
|
||||
Confirm:
|
||||
|
||||
- npm receives `X.Y.Z-canary.N` under dist-tag `canary`
|
||||
- `latest` remains unchanged
|
||||
- no git tag is created
|
||||
- the script cleans the working tree afterward
|
||||
1. verification passed
|
||||
2. npm canary publish succeeded
|
||||
3. git tag `canary/vYYYY.MDD.P-canary.N` exists
|
||||
|
||||
Guard:
|
||||
|
||||
- if the current stable is `0.2.7`, the next patch canary is `0.2.8-canary.0`
|
||||
- the tooling must never publish `0.2.7-canary.N` after `0.2.7` is already stable
|
||||
|
||||
After publish, verify:
|
||||
Useful checks:
|
||||
|
||||
```bash
|
||||
npm view paperclipai@canary version
|
||||
```
|
||||
|
||||
The user install path is:
|
||||
|
||||
```bash
|
||||
npx paperclipai@canary onboard
|
||||
git tag --list 'canary/v*' --sort=-version:refname | head -5
|
||||
```
|
||||
|
||||
## Step 5 — Smoke Test the Canary
|
||||
@@ -173,60 +147,70 @@ Run:
|
||||
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:
|
||||
|
||||
1. install succeeds
|
||||
2. onboarding completes
|
||||
3. server boots
|
||||
4. UI loads
|
||||
5. basic company/dashboard flow works
|
||||
2. onboarding completes without crashes
|
||||
3. the server boots
|
||||
4. the UI loads
|
||||
5. basic company creation and dashboard load work
|
||||
|
||||
If smoke testing fails:
|
||||
|
||||
- stop the stable release
|
||||
- fix the issue
|
||||
- publish another canary
|
||||
- repeat the smoke test
|
||||
- fix the issue on `master`
|
||||
- wait for the next automatic canary
|
||||
- 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
|
||||
./scripts/release.sh {patch|minor|major} --dry-run
|
||||
./scripts/release.sh {patch|minor|major}
|
||||
./scripts/release.sh stable --dry-run
|
||||
./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:
|
||||
|
||||
- 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
|
||||
## Step 7 — Finish the Other Surfaces
|
||||
|
||||
Create or verify follow-up work for:
|
||||
|
||||
- website changelog publishing
|
||||
- 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.
|
||||
|
||||
@@ -236,9 +220,9 @@ If the canary is bad:
|
||||
|
||||
- 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
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
Then fix forward with a new patch release.
|
||||
Then fix forward with a new stable release.
|
||||
|
||||
## Output
|
||||
|
||||
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
|
||||
- npm status
|
||||
- smoke-test status
|
||||
- git tag / GitHub Release status
|
||||
- website / announcement follow-up status
|
||||
- rollback recommendation if anything is still partially complete
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
type: string
|
||||
default: master
|
||||
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
|
||||
type: string
|
||||
dry_run:
|
||||
|
||||
@@ -69,13 +69,13 @@ Those rewrites are temporary. The working tree is restored after publish or dry-
|
||||
|
||||
Paperclip uses calendar versions:
|
||||
|
||||
- stable: `YYYY.M.D`
|
||||
- canary: `YYYY.M.D-canary.N`
|
||||
- stable: `YYYY.MDD.P`
|
||||
- canary: `YYYY.MDD.P-canary.N`
|
||||
|
||||
Examples:
|
||||
|
||||
- stable: `2026.3.17`
|
||||
- canary: `2026.3.17-canary.2`
|
||||
- stable: `2026.318.0`
|
||||
- canary: `2026.318.1-canary.2`
|
||||
|
||||
## Publish model
|
||||
|
||||
@@ -85,7 +85,7 @@ Canaries publish under the npm dist-tag `canary`.
|
||||
|
||||
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:
|
||||
|
||||
@@ -99,13 +99,13 @@ Stable publishes use the npm dist-tag `latest`.
|
||||
|
||||
Example:
|
||||
|
||||
- `paperclipai@2026.3.17`
|
||||
- `paperclipai@2026.318.0`
|
||||
|
||||
Stable publishes do not create a release commit. Instead:
|
||||
|
||||
- package versions are rewritten temporarily
|
||||
- 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
|
||||
|
||||
@@ -126,7 +126,7 @@ Rollback does not unpublish anything.
|
||||
It repoints the `latest` dist-tag to a prior stable version:
|
||||
|
||||
```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.
|
||||
|
||||
@@ -205,7 +205,7 @@ After setup:
|
||||
3. confirm it passes verification
|
||||
4. confirm publish succeeds under the `npm-canary` environment
|
||||
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:
|
||||
|
||||
@@ -217,18 +217,19 @@ npx paperclipai@canary onboard
|
||||
|
||||
After at least one good canary exists:
|
||||
|
||||
1. prepare `releases/vYYYY.M.D.md` on the source commit you want to promote
|
||||
2. open `Actions` -> `Release`
|
||||
3. run it with:
|
||||
1. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
|
||||
2. prepare `releases/vYYYY.MDD.P.md` on the source commit you want to promote
|
||||
3. open `Actions` -> `Release`
|
||||
4. run it with:
|
||||
- `source_ref`: the tested commit SHA or canary tag source commit
|
||||
- `stable_date`: leave blank or set the intended UTC date
|
||||
- `dry_run`: `true`
|
||||
4. confirm the dry-run succeeds
|
||||
5. rerun with `dry_run: false`
|
||||
6. approve the `npm-stable` environment when prompted
|
||||
7. confirm npm `latest` points to the new stable version
|
||||
8. confirm git tag `vYYYY.M.D` exists
|
||||
9. confirm the GitHub Release was created
|
||||
5. confirm the dry-run succeeds
|
||||
6. rerun with `dry_run: false`
|
||||
7. approve the `npm-stable` environment when prompted
|
||||
8. confirm npm `latest` points to the new stable version
|
||||
9. confirm git tag `vYYYY.MDD.P` exists
|
||||
10. confirm the GitHub Release was created
|
||||
|
||||
## 13. Suggested Maintainer Policy
|
||||
|
||||
|
||||
@@ -6,26 +6,29 @@ The release model is now commit-driven:
|
||||
|
||||
1. Every push to `master` publishes a canary automatically.
|
||||
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.
|
||||
|
||||
## Versioning Model
|
||||
|
||||
Paperclip uses calendar versions that still fit semver syntax:
|
||||
|
||||
- stable: `YYYY.M.D`
|
||||
- canary: `YYYY.M.D-canary.N`
|
||||
- stable: `YYYY.MDD.P`
|
||||
- canary: `YYYY.MDD.P-canary.N`
|
||||
|
||||
Examples:
|
||||
|
||||
- stable on March 17, 2026: `2026.3.17`
|
||||
- fourth canary on March 17, 2026: `2026.3.17-canary.3`
|
||||
- first stable on March 18, 2026: `2026.318.0`
|
||||
- second stable on March 18, 2026: `2026.318.1`
|
||||
- fourth canary for the `2026.318.1` line: `2026.318.1-canary.3`
|
||||
|
||||
Important constraints:
|
||||
|
||||
- do not use leading zeroes such as `2026.03.17`
|
||||
- do not use four numeric segments such as `2026.03.17.1`
|
||||
- the semver-safe canary form is `2026.3.17-canary.1`
|
||||
- the middle numeric slot is `MDD`, where `M` is the UTC month and `DD` is the zero-padded UTC day
|
||||
- use `2026.303.0` for March 3, not `2026.33.0`
|
||||
- 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
|
||||
|
||||
@@ -45,7 +48,7 @@ Canaries only cover the first two surfaces plus an internal traceability tag.
|
||||
- canaries publish from `master`
|
||||
- stables publish from an explicitly chosen source ref
|
||||
- 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 require changelog generation
|
||||
|
||||
@@ -60,7 +63,7 @@ It:
|
||||
- verifies the pushed commit
|
||||
- computes the canary version for the current UTC date
|
||||
- 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:
|
||||
|
||||
@@ -84,15 +87,17 @@ Inputs:
|
||||
Before running stable:
|
||||
|
||||
1. pick the canary commit or tag you trust
|
||||
2. create or update `releases/vYYYY.M.D.md` on that source ref
|
||||
3. run the stable workflow from that source ref
|
||||
2. resolve the target stable version with `./scripts/release.sh stable --date YYYY-MM-DD --print-version`
|
||||
3. create or update `releases/vYYYY.MDD.P.md` on that source ref
|
||||
4. run the stable workflow from that source ref
|
||||
|
||||
The workflow:
|
||||
|
||||
- re-verifies the exact source ref
|
||||
- publishes `YYYY.M.D` under npm dist-tag `latest`
|
||||
- creates git tag `vYYYY.M.D`
|
||||
- creates or updates the GitHub Release from `releases/vYYYY.M.D.md`
|
||||
- computes the next stable patch slot for the chosen UTC date
|
||||
- publishes `YYYY.MDD.P` under npm dist-tag `latest`
|
||||
- creates git tag `vYYYY.MDD.P`
|
||||
- creates or updates the GitHub Release from `releases/vYYYY.MDD.P.md`
|
||||
|
||||
## Local Commands
|
||||
|
||||
@@ -114,22 +119,22 @@ This is mainly for emergency/manual use. The normal path is the GitHub workflow.
|
||||
|
||||
```bash
|
||||
./scripts/release.sh stable
|
||||
git push public-gh refs/tags/vYYYY.M.D
|
||||
./scripts/create-github-release.sh YYYY.M.D
|
||||
git push public-gh refs/tags/vYYYY.MDD.P
|
||||
./scripts/create-github-release.sh YYYY.MDD.P
|
||||
```
|
||||
|
||||
## Stable Changelog Workflow
|
||||
|
||||
Stable changelog files live at:
|
||||
|
||||
- `releases/vYYYY.M.D.md`
|
||||
- `releases/vYYYY.MDD.P.md`
|
||||
|
||||
Canaries do not get changelog files.
|
||||
|
||||
Recommended local generation flow:
|
||||
|
||||
```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."
|
||||
```
|
||||
|
||||
@@ -175,11 +180,11 @@ Rollback does not unpublish versions.
|
||||
It only moves the `latest` dist-tag back to a previous stable:
|
||||
|
||||
```bash
|
||||
./scripts/rollback-latest.sh 2026.3.16 --dry-run
|
||||
./scripts/rollback-latest.sh 2026.3.16
|
||||
./scripts/rollback-latest.sh 2026.318.0 --dry-run
|
||||
./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
|
||||
|
||||
@@ -201,8 +206,8 @@ This is a partial release. npm is already live.
|
||||
Do this immediately:
|
||||
|
||||
1. push the missing tag
|
||||
2. rerun `./scripts/create-github-release.sh YYYY.M.D`
|
||||
3. verify the GitHub Release notes point at `releases/vYYYY.M.D.md`
|
||||
2. rerun `./scripts/create-github-release.sh YYYY.MDD.P`
|
||||
3. verify the GitHub Release notes point at `releases/vYYYY.MDD.P.md`
|
||||
|
||||
Do not republish the same version.
|
||||
|
||||
@@ -211,7 +216,7 @@ Do not republish the same version.
|
||||
Roll back the dist-tag:
|
||||
|
||||
```bash
|
||||
./scripts/rollback-latest.sh YYYY.M.D
|
||||
./scripts/rollback-latest.sh YYYY.MDD.P
|
||||
```
|
||||
|
||||
Then fix forward with a new stable release.
|
||||
|
||||
@@ -49,13 +49,13 @@ The repo and npm tooling still assume semver-shaped version strings in many plac
|
||||
|
||||
Recommended format:
|
||||
|
||||
- stable: `YYYY.M.D`
|
||||
- canary: `YYYY.M.D-canary.N`
|
||||
- stable: `YYYY.MDD.P`
|
||||
- canary: `YYYY.MDD.P-canary.N`
|
||||
|
||||
Examples:
|
||||
|
||||
- stable on March 17, 2026: `2026.3.17`
|
||||
- third canary on March 17, 2026: `2026.3.17-canary.2`
|
||||
- first stable on March 17, 2026: `2026.317.0`
|
||||
- third canary on the `2026.317.0` line: `2026.317.0-canary.2`
|
||||
|
||||
Why this shape:
|
||||
|
||||
@@ -66,11 +66,12 @@ Why this shape:
|
||||
|
||||
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
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
- 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:
|
||||
|
||||
@@ -123,7 +124,7 @@ Recommended stable input:
|
||||
|
||||
- `source_ref`
|
||||
- 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
|
||||
|
||||
@@ -137,9 +138,9 @@ Canaries should stay lightweight:
|
||||
|
||||
Stable releases should remain the public narrative surface:
|
||||
|
||||
- git tag `v2026.3.17`
|
||||
- GitHub Release `v2026.3.17`
|
||||
- stable changelog file `releases/v2026.3.17.md`
|
||||
- git tag `v2026.317.0`
|
||||
- GitHub Release `v2026.317.0`
|
||||
- stable changelog file `releases/v2026.317.0.md`
|
||||
|
||||
## Security Model
|
||||
|
||||
@@ -233,14 +234,14 @@ Recommended stable path:
|
||||
|
||||
1. pick a canary commit or tag
|
||||
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
|
||||
|
||||
If the notes are not ready yet, a fallback is acceptable:
|
||||
|
||||
- publish stable
|
||||
- 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.
|
||||
|
||||
@@ -268,13 +269,13 @@ Steps:
|
||||
1. checkout the merged `master` commit
|
||||
2. run verification on that exact commit
|
||||
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`
|
||||
6. create a canary git tag for traceability
|
||||
|
||||
Recommended canary tag format:
|
||||
|
||||
- `canary/v2026.3.17-canary.4`
|
||||
- `canary/v2026.317.1-canary.4`
|
||||
|
||||
Outputs:
|
||||
|
||||
@@ -299,14 +300,14 @@ Steps:
|
||||
|
||||
1. checkout `source_ref`
|
||||
2. run verification on that exact commit
|
||||
3. compute stable version from UTC date or provided override
|
||||
4. fail if `vYYYY.M.D` already exists
|
||||
5. require `releases/vYYYY.M.D.md`
|
||||
6. version public packages to `YYYY.M.D`
|
||||
3. compute the next stable patch slot for the UTC date or provided override
|
||||
4. fail if `vYYYY.MDD.P` already exists
|
||||
5. require `releases/vYYYY.MDD.P.md`
|
||||
6. version public packages to `YYYY.MDD.P`
|
||||
7. publish to npm under `latest`
|
||||
8. create git tag `vYYYY.M.D`
|
||||
8. create git tag `vYYYY.MDD.P`
|
||||
9. push tag
|
||||
10. create GitHub Release from `releases/vYYYY.M.D.md`
|
||||
10. create GitHub Release from `releases/vYYYY.MDD.P.md`
|
||||
|
||||
Outputs:
|
||||
|
||||
@@ -332,8 +333,8 @@ That logic should be replaced with:
|
||||
|
||||
For example:
|
||||
|
||||
- `stable_version_for_utc_date(2026-03-17) -> 2026.3.17`
|
||||
- `next_canary_for_utc_date(2026-03-17) -> 2026.3.17-canary.0`
|
||||
- `next_stable_version(2026-03-17) -> 2026.317.0`
|
||||
- `next_canary_for_utc_date(2026-03-17) -> 2026.317.0-canary.0`
|
||||
|
||||
### 2. Stop requiring `release/X.Y.Z`
|
||||
|
||||
@@ -392,19 +393,15 @@ It should continue to:
|
||||
|
||||
## 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. accept a less pretty stable format
|
||||
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.
|
||||
1. npm still gets semver-valid versions
|
||||
2. same-day hotfixes stay possible
|
||||
3. chronological ordering still works as long as the day is zero-padded inside `MDD`
|
||||
|
||||
### 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:
|
||||
|
||||
- stable versions: `YYYY.M.D`
|
||||
- canary versions: `YYYY.M.D-canary.N`
|
||||
- stable versions: `YYYY.MDD.P`
|
||||
- canary versions: `YYYY.MDD.P-canary.N`
|
||||
- canaries auto-published on every push to `master`
|
||||
- stables manually promoted from a chosen tested commit or canary tag
|
||||
- no release branches in the default path
|
||||
|
||||
@@ -14,8 +14,8 @@ Usage:
|
||||
./scripts/create-github-release.sh <version> [--dry-run]
|
||||
|
||||
Examples:
|
||||
./scripts/create-github-release.sh 2026.3.17
|
||||
./scripts/create-github-release.sh 2026.3.17 --dry-run
|
||||
./scripts/create-github-release.sh 2026.318.0
|
||||
./scripts/create-github-release.sh 2026.318.0 --dry-run
|
||||
|
||||
Notes:
|
||||
- Run this after pushing the stable tag.
|
||||
@@ -48,7 +48,7 @@ if [ -z "$version" ]; then
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ get_current_stable_version() {
|
||||
fi
|
||||
}
|
||||
|
||||
stable_version_for_date() {
|
||||
stable_version_slot_for_date() {
|
||||
node - "${1:-}" <<'NODE'
|
||||
const input = process.argv[2];
|
||||
|
||||
@@ -117,7 +117,10 @@ if (Number.isNaN(date.getTime())) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -131,6 +134,53 @@ process.stdout.write(`${y}-${m}-${d}`);
|
||||
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() {
|
||||
local stable_version="$1"
|
||||
shift
|
||||
@@ -159,7 +209,7 @@ for (const packageName of packageNames) {
|
||||
} catch {
|
||||
versions = [];
|
||||
}
|
||||
|
||||
|
||||
for (const version of versions) {
|
||||
const match = version.match(pattern);
|
||||
if (!match) continue;
|
||||
|
||||
@@ -10,6 +10,7 @@ channel=""
|
||||
release_date=""
|
||||
dry_run=false
|
||||
skip_verify=false
|
||||
print_version_only=false
|
||||
tag_name=""
|
||||
|
||||
cleanup_on_exit=false
|
||||
@@ -17,20 +18,23 @@ cleanup_on_exit=false
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
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:
|
||||
./scripts/release.sh canary
|
||||
./scripts/release.sh canary --date 2026-03-17 --dry-run
|
||||
./scripts/release.sh stable
|
||||
./scripts/release.sh stable --date 2026-03-17 --dry-run
|
||||
./scripts/release.sh stable --date 2026-03-18 --print-version
|
||||
|
||||
Notes:
|
||||
- Canary releases publish YYYY.M.D-canary.N under the npm dist-tag "canary"
|
||||
and create the git tag canary/vYYYY.M.D-canary.N.
|
||||
- Stable releases publish YYYY.M.D under the npm dist-tag "latest" and create
|
||||
the git tag vYYYY.M.D.
|
||||
- Stable release notes must already exist at releases/vYYYY.M.D.md.
|
||||
- Stable versions use YYYY.MDD.P, where M is the UTC month, DD is the
|
||||
zero-padded UTC day, and P is the same-day stable patch slot.
|
||||
- Canary releases publish YYYY.MDD.P-canary.N under the npm dist-tag
|
||||
"canary" and create the git tag canary/vYYYY.MDD.P-canary.N.
|
||||
- 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
|
||||
exit. Tags always point at the original source commit, not a generated
|
||||
release commit.
|
||||
@@ -94,6 +98,7 @@ while [ $# -gt 0 ]; do
|
||||
;;
|
||||
--dry-run) dry_run=true ;;
|
||||
--skip-verify) skip_verify=true ;;
|
||||
--print-version) print_version_only=true ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -118,15 +123,20 @@ CURRENT_SHA="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||||
LAST_STABLE_TAG="$(get_last_stable_tag)"
|
||||
CURRENT_STABLE_VERSION="$(get_current_stable_version)"
|
||||
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)"
|
||||
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."
|
||||
|
||||
TARGET_STABLE_VERSION="$(next_stable_version "$RELEASE_DATE" "${PUBLIC_PACKAGE_NAMES[@]}")"
|
||||
TARGET_PUBLISH_VERSION="$TARGET_STABLE_VERSION"
|
||||
DIST_TAG="latest"
|
||||
|
||||
if [ "$channel" = "canary" ]; then
|
||||
require_on_master_branch
|
||||
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")"
|
||||
fi
|
||||
|
||||
if [ "$print_version_only" = true ]; then
|
||||
printf '%s\n' "$TARGET_PUBLISH_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")"
|
||||
|
||||
require_clean_worktree
|
||||
|
||||
@@ -12,8 +12,8 @@ Usage:
|
||||
./scripts/rollback-latest.sh <stable-version> [--dry-run]
|
||||
|
||||
Examples:
|
||||
./scripts/rollback-latest.sh 2026.3.17
|
||||
./scripts/rollback-latest.sh 2026.3.17 --dry-run
|
||||
./scripts/rollback-latest.sh 2026.318.0
|
||||
./scripts/rollback-latest.sh 2026.318.0 --dry-run
|
||||
|
||||
Notes:
|
||||
- This repoints the npm dist-tag "latest" for every public package.
|
||||
@@ -45,7 +45,7 @@ if [ -z "$version" ]; then
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user