diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 3c160a6..7db1257 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,87 +1,225 @@ # 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 the `main` branch 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. +Only the owner ([@sneak](https://sneak.berlin)): + +- decides whether and when a release is tagged; +- creates, moves or deletes a tag; +- decides where the archives are published. + +Anyone can run steps 1 to 4. No script in this repository uploads or publishes +the archives. None signs them, and none packs a CRX (Chrome's packaged extension +file). + +## Terms + +- The tracker is the repository's Gitea site (Gitea is the software that hosts + the repository): https://git.eeqj.de/sneak/AutistMask. The tracker's Actions + page, https://git.eeqj.de/sneak/AutistMask/actions, lists the workflow runs. +- A workflow is a file under `.gitea/workflows/` that lists jobs. The tracker + runs every workflow on each push. Each job runs one script on the tracker's + runner (the machine that runs jobs). +- The release commit is a commit on `main`. Releases are tagged from `main` only + (`TODO.md`, Workflow). +- A clean checkout is a working tree (the checked-out files) at the release + commit where `git status --porcelain` prints nothing. The build stamps the + commit hash (the commit's id) into the extension and appends `-dirty` when the + tree has local changes. +- A tag is a name that git attaches 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 hexadecimal characters. + +## What you need + +- A clean checkout of the release commit. +- `make`. +- Docker (a tool that runs programs in containers), for `script/cibuild` in + step 1. `make package` needs no Docker. +- Node and yarn, on the `PATH`. `make package` runs both directly. + `script/bootstrap` can install both through nvm (a Node version manager), but + it leaves them off the `PATH`. ## 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 Docker image build (an image is Docker's packaged form of a program). -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. +`script/check` runs: -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. +- `script/test`: the tests, in Docker; +- `script/lint`, which runs three checks in Docker: + - ESLint, a code checker; + - `prettier --check .`, the formatting check; + - `script/check-censored`, which fails when the name that `RULES.md` bars + appears outside its allowed places; +- `script/fmt-check`: `prettier --check .` again, on the host. -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. +The `e2e` workflow (`.gitea/workflows/e2e.yml`) has two jobs. One runs +`script/test-e2e`, the browser tests in Chrome. The other runs +`script/test-e2e-firefox`, the browser tests in Firefox. A red `e2e` job does +not fail the `check` run. - 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`. +Run `script/cibuild` on the clean checkout. `script/cibuild` runs the same steps +as the `check` run, and must exit 0. + +Check: on the Actions page, the `check` run for the release commit succeeded, +and both `e2e` jobs for that commit succeeded. `script/cibuild` exited 0 on the +clean checkout. + +### 2. Confirm the version + +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 the +owner creates in step 5. + +### 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`. `build.js` writes `dist/chrome/`, `dist/firefox/` and a build + receipt. The receipt lists 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 test recovery phrase, which a debug build gives every + new wallet; +- 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 the browser's directory (`dist/chrome/` or `dist/firefox/`) holds + every file named in `manifest.json` or in the popup's HTML page. + `manifest.json` is the file that describes the extension to the browser. The + popup is the window that the toolbar button opens; +- checks that the `version` in each `manifest.json` is the version from step 2; +- writes one archive per browser; +- reads each archive back; +- compares every member (a file inside the archive) with `dist/` byte for byte; +- writes `SHA256SUMS`, with one line per archive: + - the digest; + - two spaces; + - 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 (an add-on is Firefox's word for an +extension). The `.xpi` is not signed. + +`make package` prints a line that starts `Not shipped` and names `styles.css`. +This is expected: each archive holds its own copy of the stylesheet. + +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/`: the same `dist/` always +gives the same bytes. `script/lib/zip.js`: + +- sorts the members; +- sets every member's timestamp (its recorded date) to 1980-01-01; +- compresses each member the same way every time. + +`dist/` itself carries the build date. `build.js` compiles the build date, a UTC +(universal time) day, into the extension. 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. On the same UTC day, a +second `make package` from another clean checkout of the same commit gives the +same `SHA256SUMS`. + +### 5. Tag the release commit + +Owner only. Only the owner decides whether and when a release is tagged. Only +the owner creates, moves or deletes a tag. On the release commit, the owner +runs: + +```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 that carries a message and records who made +the tag and when. `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 +tag is listed at https://git.eeqj.de/sneak/AutistMask/tags. + +### 6. Publish the archives + +Owner only. The owner decides where the archives are published. No script in +this repository uploads or publishes them. + +Whoever receives an archive installs it as follows. + +Chrome loads the extension from a directory: + +- unzip the archive into a new, empty directory, for example + `unzip autistmask-chrome-0.1.0.zip -d autistmask-chrome`; +- open `chrome://extensions/` and turn on "Developer mode"; +- click "Load unpacked" and pick that directory. + +Chrome then reports the extension id `gipbhkogfopeahplcjhipkgpcimdpkip`. The id +is the fixed string that identifies the extension to Chrome; the `key` in +`manifest/chrome.json` sets the id. + +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 the +`about:debugging` page. Firefox unloads a temporary add-on when Firefox exits. +The section "Installing on Firefox" in `README.md` has the details. + +Check: each archive installs on a fresh browser profile (a new, empty set of +browser data) by the route above.