From 69ce8fd6d63bf6605a77a3a83619e24624c316fe Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 2 Nov 2026 02:49:45 +0000 Subject: [PATCH] docs: rewrite the release procedure sentence by sentence against the prose rules 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 --- docs/RELEASE.md | 246 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 171 insertions(+), 75 deletions(-) diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 3c160a6..d911a37 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -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-.zip`, `autistmask-firefox-.xpi`, and - `SHA256SUMS`. _Check:_ those three files exist and `` 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=`), - 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-.zip +release/autistmask-firefox-.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 `` 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-.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.