docs: rewrite docs/RELEASE.md sentence by sentence against the prose rules
Rewrite docs/RELEASE.md from scratch for #672: 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. The reproducibility claim names the build date that build.js compiles in, so it holds only within one UTC day. The commit carries the findings of the three reviews, the last two in the third review's wording: the self-contained check is described with the browser's directory as its subject, and Node and yarn must be on the PATH, because script/bootstrap leaves an nvm install off it. Model: fable-5-1
This commit was merged in pull request #715.
This commit is contained in:
+213
-75
@@ -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-<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 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=<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.
|
||||
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-<version>.zip
|
||||
release/autistmask-firefox-<version>.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 `<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/`: 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.
|
||||
|
||||
Reference in New Issue
Block a user