docs: rewrite the release procedure sentence by sentence against the prose rules
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

Rewrite docs/RELEASE.md from scratch: a terms list, what is needed, and six
numbered steps, each with its check. Every statement is checked against the
Makefile, script/, build.js, the two workflows and the tests. The step that
described distribution routes not built is cut; installing from the archives
replaces it. The reproducibility claim now names the build date that build.js
compiles in, so it holds only within one UTC day.

Model: fable-5-1
This commit is contained in:
2026-11-02 02:49:45 +00:00
parent 3191577f73
commit 69ce8fd6d6
+171 -75
View File
@@ -1,87 +1,183 @@
# Releasing AutistMask
This is the procedure that turns a green `main` into a tagged, packaged release.
It gathers into one place what is otherwise spread across the `Makefile` and
three `README.md` sections, so the person cutting a release does not have to
reconstruct the order from them.
A release is a tagged commit on `main` and the two archives that `make package`
writes from that commit. This file is the procedure, in order.
There is one version, declared in three files (`package.json`,
`manifest/chrome.json`, `manifest/firefox.json`), and `make package` builds and
packages but publishes nothing. `make build` and `make package` can be run by
anyone; tagging, signing, packing a CRX and any upload need credentials only the
owner ([@sneak](https://sneak.berlin)) holds and are marked **owner-only**
below. Releases are tagged from `main` (see the Workflow section of `TODO.md`),
so the "release commit" throughout is the `main` commit the milestone PR merged.
Nothing in this repository publishes. There is no store upload, no signing and
no CRX (a CRX is Chrome's packaged extension file). Anyone can run the build and
the packaging. Tagging is the owner's job ([@sneak](https://sneak.berlin)).
## Terms
- The release commit is the commit on `main` to be released. `main` receives one
pull request per milestone (the tracker's set of issues for one version), from
`next`, and releases are tagged from `main` (`TODO.md`, Workflow).
- A clean checkout is a working tree at the release commit where
`git status --porcelain` prints nothing. The build stamps the commit hash into
the extension and appends `-dirty` when the tree has local changes.
- A tag is a name that git fixes to one commit. Release tags read `vX.Y.Z`, such
as `v0.1.0`.
- A digest is the sha256 hash of a file (a fixed-length fingerprint of its
bytes), written as 64 hex characters.
- A workflow is a list of jobs that the tracker runs on every push, from a file
under `.gitea/workflows/`. The tracker lists the runs on the repository's
Actions page.
## What you need
- A clean checkout of the release commit.
- `make`.
- Docker, for `make check`. `make package` needs no Docker.
- Node and yarn. Both come from the system when present. Otherwise `make setup`
installs Node through a pinned install of nvm (a Node version manager) and
yarn through corepack (Node's package manager installer).
## Procedure
1. **Confirm `main` is green in CI.** The `check` workflow
(`.gitea/workflows/check.yml`) runs `script/cibuild`, which runs
`script/check` and then builds the image uncached, so a green `check` run is
a green `make check`. Find the run for the exact release commit on the
tracker's Actions view. _Check:_ that commit's `check` run succeeded; running
`make check` on a clean checkout of the commit reproduces it and exits 0.
### 1. Confirm the release commit is green
2. **Confirm the version matches the intended tag.** `package.json`,
`manifest/chrome.json` and `manifest/firefox.json` must all declare the same
`X.Y.Z`. `make build` fails when they disagree, but nothing checks that they
equal the tag you mean to create — that is this manual step. _Check:_ all
three files read the same `X.Y.Z`, and it is the version you intend to tag
`vX.Y.Z`.
The `check` workflow (`.gitea/workflows/check.yml`) runs `script/cibuild` on
every push. `script/cibuild` runs:
3. **Build and package from a clean checkout of that commit.** From a fresh
clone, or a working tree with no local modifications (`git status` clean),
checked out at the release commit: run `make setup`, then `make package`.
`make package` runs `make build` first, so the archives can only be made from
a `dist/` verified against that build's own receipt as a release (not debug)
build. It writes three files into `release/`:
`autistmask-chrome-<version>.zip`, `autistmask-firefox-<version>.xpi`, and
`SHA256SUMS`. _Check:_ those three files exist and `<version>` in the archive
names is the version confirmed in step 2. The Firefox `.xpi` is **unsigned**
(see step 6 and "Installing on Firefox" in `README.md`).
- `script/bootstrap`, which installs the dependencies;
- `script/check`, which is what `make check` runs: the tests, the lint checks
and the formatting check. The lint checks are ESLint (a code checker), the
prettier check (a formatting check) and `script/check-censored`;
- the Docker image build.
4. **Verify `SHA256SUMS`.** The archives are deterministic — sorted entries,
fixed timestamps, fixed compression — so a second `make package` from another
clean checkout of the same commit produces byte-identical files. Verify the
recorded digests against the files with `sha256sum -c SHA256SUMS`, run from
`release/`. To confirm reproducibility, run `make package` again on a
separate clean checkout and compare the digests. _Check:_ `sha256sum -c`
reports `OK` for every file, and an independent build's digests match.
The `e2e` workflow (`.gitea/workflows/e2e.yml`) runs the two browser suites on
every push, as two jobs. A red `e2e` job does not fail the `check` run.
5. **Tag the release commit.** _(owner-only)_ Create an annotated tag `vX.Y.Z`
on the release commit and push it: `git tag -a vX.Y.Z` (with a message), then
`git push origin vX.Y.Z`. _Check:_ `git tag` lists `vX.Y.Z`, and
`git rev-parse vX.Y.Z^{commit}` resolves to the release commit.
Check: on the Actions page, the `check` run for the release commit succeeded,
and both `e2e` jobs for that commit succeeded. `make check` on the clean
checkout gives the same answer as the `check` run.
6. **Distribute per browser.** _(owner-only; pending the owner's choice on
https://git.eeqj.de/sneak/AutistMask/issues/386)_ How 1.0.0 is distributed on
each browser is not yet decided; it is the open question on that issue, and
the concrete steps cannot be written until the owner records a choice there.
These steps need credentials only the owner holds. The options under
consideration are:
- **Firefox** — the packaged `.xpi` is unsigned, and release Firefox and ESR
refuse an unsigned add-on:
- (a) AMO self-distribution signing (unlisted): submit the `.xpi` to AMO
with the owner's credentials; AMO returns a signed `.xpi` installable
on every Firefox, with nothing listed publicly.
- (b) AMO listed: as (a), plus a public AMO listing and review.
- (c) Ship the unsigned `.xpi` and state that Firefox support means
Developer Edition, Nightly, or an Unbranded build with
`xpinstall.signatures.required` set to `false`.
- **Chrome** — the repo packs no CRX and publishes nothing; the extension id
is fixed by the `key` in `manifest/chrome.json`:
- (a) Chrome Web Store (unlisted): upload the `.zip` with the owner's
developer account; the store delivers installs and updates.
- (b) Self-hosted CRX signed with the private key the owner holds
(`chrome --pack-extension=dist/chrome --pack-extension-key=<path to the .pem>`),
installable only via enterprise policy on Windows and macOS, so
realistically Linux-only.
- (c) "Load unpacked" from `dist/chrome/` only, as today.
### 2. Confirm the version
Once the owner decides, the chosen steps — including which credentials they
need and who holds them — are written into this section and `README.md`'s
installation sections are updated to match, which is part of the definition
of done of https://git.eeqj.de/sneak/AutistMask/issues/386. _Check:_ for a
store or AMO route, the artifact installs from the store or AMO on a clean
browser profile; for the CRX or unpacked route, the documented load succeeds
and Chrome reports the extension id `gipbhkogfopeahplcjhipkgpcimdpkip`.
Three files declare the version:
- `package.json`;
- `manifest/chrome.json`;
- `manifest/firefox.json`.
`script/lib/version.js` fails the build when the three disagree.
`tests/version.test.js` fails when the version is not three numbers separated by
dots. Nothing compares the version with a tag. That comparison is this step.
Check: all three files read the same `X.Y.Z`, and `vX.Y.Z` is the tag that step
5 creates.
### 3. Build and package
On the clean checkout, run:
```bash
make setup
make package
```
`make setup` installs two things:
- the dependencies, from the lockfile `yarn.lock`, which pins each one's
version;
- the git pre-commit hook, a script that git runs before each commit. Here the
hook runs `script/check`.
`make package` runs `make build`, then `script/lib/package.js`.
`make build`:
- runs `build.js`, which writes `dist/chrome/` and `dist/firefox/`, and a build
receipt: the list of every file that `build.js` wrote, with each file's
digest. The receipt lives outside the repository and is deleted when the build
ends;
- runs `script/verify-build --expect release` against the receipt. The check
fails when:
- `dist/` differs from the receipt;
- a bundle (one JavaScript file built from many source files) was compiled
with the build-time constant `DEBUG` on;
- any file carries the fixed recovery phrase that a debug build hands out;
- runs `script/check-censored --require-dist` against the built files;
- removes `dist/` when any of those steps fails.
`script/lib/package.js`:
- empties `release/`;
- refuses a symbolic link (a file that points at another file) under `dist/`;
- checks that every file named by the manifest or the popup markup is inside the
browser's directory;
- checks that the `version` in each `manifest.json` is the tree's version;
- writes one archive per browser, reads it back and compares every member (a
file inside the archive) with `dist/` byte for byte;
- writes `SHA256SUMS`: one line per archive with the digest, two spaces and the
file name.
`make package` writes three files:
```
release/autistmask-chrome-<version>.zip
release/autistmask-firefox-<version>.xpi
release/SHA256SUMS
```
The `.xpi` is Firefox's add-on package file. It is not signed. The output line
`Not shipped` names `dist/styles.css`. `build.js` writes the compiled stylesheet
there and copies it into each browser directory. The copy at the root of `dist/`
is in neither archive. `make clean` removes `dist/` and `release/`.
Check: `release/` holds those three files, and `<version>` in their names is the
version from step 2.
### 4. Verify the digests
From `release/`, run `sha256sum -c SHA256SUMS`.
The archives are deterministic for a given `dist/`: `script/lib/zip.js` sorts
the members, sets every timestamp to 1980-01-01 and fixes the compression level.
`dist/` itself carries the build date: `build.js` compiles the day of the build,
in UTC (universal time), into the extension, with the commit hash and the
version. So two builds of the same commit on the same UTC day give
byte-identical archives. Builds on different days differ.
Check: `sha256sum -c` prints `OK` for both archives. A second `make package`
from another clean checkout of the same commit, on the same UTC day, gives the
same `SHA256SUMS`.
### 5. Tag the release commit
Owner only. On the release commit, run:
```bash
git tag -a vX.Y.Z -m "AutistMask X.Y.Z"
git push origin vX.Y.Z
```
`-a` makes an annotated tag: a tag object with its own message, author and date.
`v0.1.0` is one.
Once the tag exists, `git describe --tags` on the release commit prints
`vX.Y.Z`. `script/cibuild` and `script/docker` pass that value to the Docker
image build as `VERSION`.
Check: `git rev-parse vX.Y.Z^{commit}` prints the release commit's hash, and the
tracker lists the tag under the repository's tags.
### 6. Hand out the archives
The archives in `release/` are the release. This repository has no further step.
Chrome loads the extension from a directory:
- unzip `autistmask-chrome-<version>.zip`;
- open `chrome://extensions/` and turn on "Developer mode";
- click "Load unpacked" and pick the unzipped directory.
Chrome then reports the extension id `gipbhkogfopeahplcjhipkgpcimdpkip`, which
the `key` in `manifest/chrome.json` fixes.
Release builds of Firefox refuse to install an unsigned add-on. Desktop Firefox
115 or newer, in any edition, loads the `.xpi` as a temporary add-on from
`about:debugging` and unloads it when Firefox exits. The section "Installing on
Firefox" in `README.md` has the details.
Check: each archive installs on a fresh browser profile by the route above.