release candidates: every push to main runs the full CI, the acceptance matrix and the Linux / Windows installer builds in parallel (ci.yml) and records the green result per commit as the draft rc-<sha> (green.json last); a version-only bump commit (proven file by file with the version writers) reuses its parent's CI and matrix and rebuilds only the installers; a v* tag only promotes the candidate (release.yml), fails fast on a red or missing commit; acceptance runs carry a CLI key; tools/release.ts does the whole release on the Mac (bump, CHANGELOG cut, fast gate, push, tag, Mac build + notarize in parallel with Forgejo, cached macOS acceptance, add, verify, publish with one upload of the Mac files, /Applications update, npm commands) and --rehearsal; DMG notarization runs while the app and updater bundle are checked; rehearsal channel on the VDS; docs

This commit is contained in:
CRTkafa 2026-10-15 00:42:39 +08:00
parent 89d94d6993
commit c3b1e8c370
14 changed files with 1438 additions and 526 deletions

View file

@ -31,55 +31,116 @@ in `~/Desktop/storebound-dist/desktop-0.1.0` was never published, so no installe
`tauri.conf.json`, `Cargo.toml` and the app's entry in `Cargo.lock`. CI runs this check; `src-tauri/tests/d5.rs`
checks the app's copies from the Rust side.
- `bun tools/desktop-version.ts --set 0.8.0` writes a new version into package.json and all of them.
- **Release order** (one commit, one version): see "The release on git.crt.fyi" below. The app goes first
- **Release order** (one commit, one version): see "The release in one command" below. The app goes first
(`publish` refuses a folder whose version is not package.json's), then `npm publish` of the same version
(CONTRIBUTING.md), so `npx storebound desktop` of the new CLI finds its own version.
## The release on git.crt.fyi
## The release in one command
One version tag, one Forgejo release: git.crt.fyi/CRT/storebound/releases keeps only the newest entry (older entries
are deleted after a successful release; their git tags stay). Notes are short: the CHANGELOG section's summary
paragraph (at most 4 lines), the install line and links, never the whole section. So the section's first paragraph
is the release summary: write it for that.
```sh
bun tools/release.ts 0.8.5 # checks and the plan
bun tools/release.ts 0.8.5 --yes # the release; then the owner's npm publish
```
1. **Version and changelog** on main: `bun tools/desktop-version.ts --set 0.8.0`, a `## 0.8.0 · <date>` section
that starts with a summary paragraph, `bun run ci`, push.
2. **Tag push:** `git tag v0.8.0 && git push origin v0.8.0`. `.forgejo/workflows/release.yml` then:
- checks that the tag is `v` + package.json's version, every version copy matches (tools/desktop-version.ts) and
the CHANGELOG has the section;
- runs the full CI (`bun run ci`, the same steps as ci.yml: `.forgejo/actions/linux-setup` and `ci-run`);
- builds the AppImage + deb (Linux runner) and the NSIS installer (Windows runner), with the checks described
under "Windows and Linux" below;
- scans every artifact with the private terms (secret `FORBIDDEN_TERMS`), writes SHA256SUMS;
- creates or updates the release "storebound 0.8.0" (`tools/forgejo-release.ts upsert`: found by tag, assets of
the same name replaced, SHA256SUMS merged, read back), then deletes the older release entries
(`forgejo-release.ts prune --keep v0.8.0`).
3. **macOS on the Mac** (the DMG cannot be built or notarized on these runners): `bun tools/desktop-release.ts mac`,
then `add` of the runners' Linux / Windows files (signs them for the updater, see below), then
`bun tools/desktop-release.ts upload-forgejo --out ~/.storebound-releases/desktop/0.8.0 --yes`: the signed DMG and
the updater bundle with their `.sig` join the same release, SHA256SUMS is merged. It refuses an unsigned DMG and
a release the workflow has not made yet. Token: `FORGEJO_TOKEN`, else the git credential helper of git.crt.fyi.
4. **storebound.dev:** `bun tools/desktop-release.ts publish --out ... --yes` (below).
5. **npm:** `npm publish` of the same version: the owner's step (CONTRIBUTING.md).
Option, not set up: an npm automation token as the Actions secret `NPM_TOKEN` and a manual `npm` job in
release.yml (`needs: release`, workflow_dispatch only, `npm publish` after the Mac's upload) would let the owner
publish with one click in Forgejo instead of a terminal. The token can publish any version of the package, so it
stays off until the owner wants it.
Target: tag push to everything published (the Forgejo release with every installer, storebound.dev's latest.json,
the app in /Applications) in under 10 minutes, with every gate still in place. It works because nothing is built or
tested twice: every push to main is already a release candidate.
**Dry run** (no version, nothing public): Actions > release > Run workflow with `dry` ticked (the default), or
`POST /api/v1/repos/CRT/storebound/actions/workflows/release.yml/dispatches` with
`{"ref":"main","inputs":{"dry":"on","windows":"on"}}` (over the API a boolean input is "on"; "true" counts as off).
The same jobs run on that commit; the release job makes a DRAFT `test-<sha>` with the real files, reads it back,
prints what `prune` would delete, and deletes the draft at the end (also when a step failed). `dry` off needs a `tag`
input and remakes that tag's release.
### Release candidates (every push to main)
`.forgejo/workflows/ci.yml` runs on every push to main, in parallel:
- `ci`: the full `bun run ci` (Rust tests and clippy included);
- `acceptance`: the release acceptance matrix, Linux and Windows x Node 20 / Node 22 / Bun (docs/acceptance.md);
- `linux`: the AppImage and the deb, self-tests, both started under Xvfb, privacy scan (the release target folder is
cached, so only changed crates compile);
- `windows`: the NSIS installer, silent per-user install, self-test, uninstall, the Node prompt smoke, privacy scan.
When all are green, the `record` job scans the installers with the private terms (secret `FORBIDDEN_TERMS`), builds
the 6-run acceptance matrix, and stores everything as the **draft release `rc-<full sha>`** on git.crt.fyi:
installers, `SHA256SUMS`, the acceptance run files, `storebound_<version>_acceptance.md` and `green.json` (sha,
version, CLI key, run link, every file's SHA-256). `green.json` is uploaded last: a candidate without it is not
complete. The newest 12 candidates are kept (`tools/release-candidate.ts prune`); release pruning never touches them.
`bun tools/release-candidate.ts status --sha <sha>` says green (with the record), red (with the failed run's link),
pending or none.
**Version-only commits.** The release's bump commit changes only the version. ci.yml's first job, `gate`, proves it
file by file: every changed file must be a version copy of `tools/desktop-version.ts` whose new text is exactly the
parent's text with the new version written in by that same writer, or `CHANGELOG.md` / `docs/acceptance*`. Then the
parent's full CI and acceptance matrix count for the bump commit (the parent's run is waited for if it is still
running; a red parent fails the gate), and only the installers are built again, because their version is compiled in
(Cargo, tauri.conf.json, the bundled CLI, the file names). That is simpler and safer than making the installers
version-independent. The acceptance runs carry a **CLI key** (`tools/cli-key.ts`: a hash of everything the packed
CLI is made of, with the version copies normalized), and `acceptance.ts report --strict` accepts a run of another
version only when its key is the checkout's.
### The tag only promotes
`.forgejo/workflows/release.yml` on a `v*` tag checks the tag against package.json, every copy and the CHANGELOG
section, then `release-candidate.ts promote`: it waits for the tagged commit's candidate (heartbeat; fails at once
when the commit is red or has no ci run, with the run's link; 40 min timeout), checks its version, and makes the
release "storebound <version>" from the candidate's files (no rebuild, no retest). Stable versions then delete the
older release entries (tags stay). Versions with a "-" are prereleases: never pruning, never the stable release.
Run it by hand from Actions > release with a `tag` (dry by default: only the plan).
### What `tools/release.ts` does on the Mac
1. **Preflight:** a clean checkout equal to origin/main whose candidate is green or still running, the updater key,
the publish host (`STOREBOUND_PUBLISH_HOST` or `~/.storebound-keys/publish-host`). The macOS acceptance matrix
(Bun, Node 20, Node 22 from fnm) starts in the background unless its results for this CLI key are cached in
`~/.storebound-releases/acceptance-cache/<key>/` (a version bump keeps the key; `bun tools/release.ts
mac-acceptance` fills the cache ahead of time).
2. **Bump:** the version in package.json and every copy, the CHANGELOG's `## Unreleased` cut into
`## <version> · <date>` (its first paragraph is the release summary).
3. **Fast gate, commit, push:** `bun run ci:quick`, the commit as CRT, push to main, push the tag.
4. **In parallel:** `desktop-release.ts mac` (universal build, sign; Tauri notarizes the app while the boot check runs;
the DMG is notarized while the app and the updater bundle are checked, scanned and signed), Forgejo's ci run of the
bump commit (installers only) and release.yml's promotion. Meanwhile the candidate's Linux / Windows installers
and acceptance runs are downloaded.
5. **add, matrix, verify:** updater signatures of the Linux / Windows files, the 9-run acceptance matrix (strict) to
the release, `verify`.
6. **publish** with `--forgejo-tag`: the VDS fetches the Linux / Windows files from the Forgejo release over its own
network, rsync sends only the Mac's DMG and updater bundle, the VDS attaches those to the Forgejo release from its
copy, then the publish script swaps latest.json. The owner's uplink (about 0.5 to 1 MB/s) carries each Mac file
once instead of three times.
7. **/Applications/storebound.app:** the running app is asked to quit (an AppleEvent), then its exact PID gets
SIGTERM / SIGKILL if needed; the new app is copied from the DMG, verified (`codesign --verify --deep --strict`)
and reopened in the background (`open -g`).
8. **Prints** `npm publish` and `npm deprecate storebound@"<version>" ...` (the owner's step) and the time of every
phase (also in `~/.storebound-releases/timings/`). Logs: `~/.storebound-releases/logs/<version>/`.
Every wait has a timeout and a heartbeat; child processes are killed by PID when it exits.
**Notarization in parallel, where possible.** Apple notarizes the app (inside `tauri bundle`) and then the DMG. The
DMG cannot go first or at the same time: it must contain the app with its stapled ticket (the gate checks
`stapler validate` on the app inside the mounted DMG). So each wait for Apple runs next to other work instead: the
boot check during the app's notarization; every app and updater-bundle check, their privacy scan and the updater
signature during the DMG's.
**Rehearsal:** `bun tools/release.ts --rehearsal [<n>] --yes` runs all of it for version `0.0.0-rehearsal.<n>` on the
branch `rehearsal/<n>` (ci.yml runs there too): a prerelease on git.crt.fyi, the `rehearsal` channel on
storebound.dev (`/desktop/rehearsal/latest.json`, which no app reads; the publish script refuses rehearsal versions
on stable / beta and other versions on rehearsal), the app copied to a scratch folder (nothing quit or opened), never
npm. At the end it deletes the release and tag, the candidate, the branch, the version's files on storebound.dev and
the local folders, and prints the times.
### By hand (what release.ts runs)
- `bun tools/desktop-version.ts --set 0.8.5` and the CHANGELOG section, `bun run ci:quick`, commit, push, tag, push
the tag (release.yml promotes the candidate).
- `bun tools/desktop-release.ts mac --out ~/.storebound-releases/desktop/0.8.5`.
- `bun tools/release-candidate.ts fetch --sha <sha> --dir <dir>`, then `bun tools/desktop-release.ts add --out ...
--commit <sha> --run <run> <AppImage> <deb> <setup.exe>`.
- `bun tools/desktop-release.ts publish --out ... --forgejo-tag v0.8.5 --yes` (or `upload-forgejo`, then `publish`).
- `npm publish` of the same version: the owner's step (CONTRIBUTING.md).
**Secrets** (repo Settings > Actions > Secrets): `RELEASE_TOKEN`, a token of CRT with scope `write:repository`
(name `storebound-release-workflow`; releases, assets, the dry run's tag) and `FORBIDDEN_TERMS`, the contents of
`forbidden-terms.txt` (never printed: hits show `x***`), and `FORGEJO_ORIGIN`, the server's IP: git.crt.fyi is
behind Cloudflare, which refuses uploads over 100 MB (the AppImage, the DMG) with a 502, so `forgejo-release.ts`
uploads assets to the origin directly (`curl --resolve`, TLS still checked for git.crt.fyi). On the Mac the same IP
is in `~/.storebound-keys/forgejo-origin`. Rotate the token: create a new one under CRT's
Settings > Applications with that scope, put it into the secret, delete the old one.
(name `storebound-release-workflow`; releases, assets, candidates: ci.yml's gate and record jobs and release.yml use
it), `FORBIDDEN_TERMS`, the contents of `forbidden-terms.txt` (never printed: hits show `x***`), and
`FORGEJO_ORIGIN`, the server's IP: git.crt.fyi is behind Cloudflare, which refuses uploads over 100 MB (the AppImage,
the DMG) with a 502, so `forgejo-release.ts` uploads assets to the origin directly (`curl --resolve`, TLS still
checked for git.crt.fyi). On the Mac the same IP is in `~/.storebound-keys/forgejo-origin`. Rotate the token: create
a new one under CRT's Settings > Applications with that scope, put it into the secret, delete the old one.
## Keys and private files (outside every repository)
@ -197,8 +258,7 @@ and choose Open Anyway (once).
## Windows and Linux (Forgejo Actions)
The `linux` and `windows` jobs of `.forgejo/workflows/release.yml` (a `v*` tag push, or a dry run by hand; untick
"windows" in a dry run to build Linux only).
The `linux` and `windows` jobs of `.forgejo/workflows/ci.yml`, on every push to main (the release candidate).
- **Linux** (`runs-on: ubuntu-22.04`, a container): AppImage and .deb built with the path-free environment, then:
- self-tests of the extracted AppImage and the unpacked .deb;
@ -233,9 +293,9 @@ installer compare with `Get-FileHash` of `C:\storebound-target\release\bundle\ns
### Runner setup
Forgejo runs in Docker on the VDS. `.forgejo/workflows/ci.yml` (`bun run ci`, Rust tests and clippy included) runs on
every push and pull request to main; `release.yml` as above makes the Forgejo release (Jenkins still publishes
site/, and `publish` storebound.dev/desktop). Workflow artifacts of this public repository can be downloaded by anyone for 14 days.
Forgejo runs in Docker on the VDS. `.forgejo/workflows/ci.yml` (the release candidate: `bun run ci`, the acceptance
matrix, the installers) runs on every push to main and `ci` + `acceptance` on pull requests; `release.yml` promotes a
candidate on a tag (Jenkins still publishes site/, and `publish` storebound.dev/desktop). Workflow artifacts of this public repository can be downloaded by anyone for 14 days.
1. **Actions in Forgejo (done).** `FORGEJO__actions__ENABLED=true` and
`FORGEJO__actions__DEFAULT_ACTIONS_URL=https://data.forgejo.org` in the compose file; the Actions unit is on for
@ -292,7 +352,6 @@ site/, and `publish` storebound.dev/desktop). Workflow artifacts of this public
- **Shared runner:** any workflow of any CRT repository with `runs-on: windows` runs here, as `runner`, with
internet access. Keep workflows of fork pull requests off it (as ci.yml does for Linux).
The workflow's Windows job runs only when "windows" is ticked (from the API, `"inputs": {"windows": "on"}`).
4. **Windows code signing (later).** Buy a code-signing certificate, then sign the installer before `add` and pass
`--code-signed`.
@ -301,7 +360,7 @@ site/, and `publish` storebound.dev/desktop). Workflow artifacts of this public
| URL | served from | how it gets there |
|---|---|---|
| /#download (the downloads, a section of the home page; /desktop/ redirects there) | /var/www/storebound.dev/index.html, /var/www/storebound.dev/desktop/index.html | `site/index.html`, `site/desktop/index.html`, with the site (Jenkins + /usr/local/sbin/storebound-publish) |
| /desktop/latest.json, /desktop/beta/latest.json | /var/www/storebound-desktop/ | storebound-desktop-publish |
| /desktop/latest.json, /desktop/beta/latest.json, /desktop/rehearsal/latest.json | /var/www/storebound-desktop/ | storebound-desktop-publish |
| /desktop/dl/<version>/<files> | /var/www/storebound-desktop/dl/ | storebound-desktop-publish |
**Why the binaries live outside the site's document root.** The site's publish script rsyncs `site/` with
@ -313,7 +372,7 @@ Apache maps the three paths with `Include /etc/apache2/storebound-desktop.conf`
- version folders are sent `immutable`;
- no directory listings.
**`/usr/local/sbin/storebound-desktop-publish <version> [stable|beta]`** (`tools/vds/storebound-desktop-publish`,
**`/usr/local/sbin/storebound-desktop-publish <version> [stable|beta|rehearsal]`** (`tools/vds/storebound-desktop-publish`,
root:root 755) takes `/var/lib/storebound-desktop/incoming/<version>/` (root only, mode 700) and checks it:
- only plain files with plain names;
@ -358,7 +417,9 @@ bun tools/desktop-release.ts publish --out ~/.storebound-releases/desktop/0.1.0
3. **Publishes:** runs the publish script.
4. **Confirms:** reads the live latest.json back.
`--channel beta` publishes to /desktop/beta/latest.json, which apps on the Beta channel read.
`--channel beta` publishes to /desktop/beta/latest.json, which apps on the Beta channel read. `--channel rehearsal`
(release rehearsals only) to /desktop/rehearsal/latest.json, which no app reads. `--forgejo-tag v<version>`: the
server fetches the files the Forgejo release already has and attaches the Mac's files to it (see above).
**Rollback.** Tauri updates only to a higher version, so a bad release is replaced by a fixed higher version. Its
files stay where they are, because links and checksums must not change.