Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd53737403 | ||
|
|
c45cf628a9 | ||
|
|
69ce8fd6d6 |
+216
-75
@@ -1,87 +1,228 @@
|
||||
# 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. Both come from the system when present. Otherwise
|
||||
`script/bootstrap` installs Node through a pinned (fixed-version) install of
|
||||
nvm, a Node version manager. `script/bootstrap` installs yarn through corepack
|
||||
(Node's package manager installer). When no corepack is found,
|
||||
`script/bootstrap` installs yarn through `npm` (Node's package manager).
|
||||
|
||||
## 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 archive is self-contained: every file that the manifest or the
|
||||
popup's HTML page names is inside the browser's directory (`dist/chrome/` or
|
||||
`dist/firefox/`). The manifest is `manifest.json`, 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