birdbrain-beta20-gate.md
Pre-ship gate run — birdbrain 1.0.1-beta.20
Scratch copy for ticking. Template: docs/plans/2026-08-15-pre-ship-validation-gate.md (do not tick the boxes in the template). Keep this file with the release.
Prefilled from the state on 2026-08-19:
- Candidate tag: v1.0.1-beta.20 @ b6505e7 (pushed 2026-08-19)
- Previous shipped build: v1.0.1-beta.19 — same public feed, so section 5 runs the IN-APP path (Settings -> Updates -> Check for updates -> Download -> Restart to update) on both Windows and the Ubuntu AppImage. Manual install is the fallback only.
- Releases: https://github.com/thebristolsound/birdbrain-releases/releases
- Section 3 macOS: not tested, unsupported this round.
- Section 4b expects tsa-intermediates.pem, and tsa-root.pem only when an anchor is bundled for the configured TSA (DigiCert default: yes). Record which case applies.
Preconditions
- The candidate is a pushed
v*tag, not a branch build. release.ymlfinished green for that tag and the GitHub Release carries the Windows.exe, Linux.AppImageand.deb, thelatest*.ymlupdate manifests, andbirdbrain-extension.zip.- The previous beta's installers are still downloadable (section 5 needs them) — that is the tag immediately below the candidate in https://github.com/thebristolsound/birdbrain-releases/releases, not a version named here. Naming one dates this document at every release, which is how the previous version of this line came to point at a build two releases old.
1. CI and Security green — must pass
The CI workflow (.github/workflows/ci.yml) must be green on the tagged commit, all
five jobs below. A sixth job, changes, decides whether those five run at all; on a push
to main or a dispatched run it always lets them through, and its own path-filter step
skips. Read it as plumbing, not as a gate — but a run where the five report skipped
rather than success is not a pass:
lint—pnpm linttypecheck—pnpm typechecktest—pnpm test:coveragewithBIRDBRAIN_REQUIRE_OPENSSL=1build—pnpm buildandpnpm build:extensione2e— Playwright against the built app
The Security workflow (.github/workflows/security.yml) runs on the same triggers and
must be green on the same commit, all three jobs:
Secret scan (full history)— gitleaks over the whole historyDependency audit— dependency advisoriesRegistry publish guard—package.jsonstill marked private
Notes worth knowing before you read a green tick as meaning more than it does:
Both workflows run on
pushtomain, on pull requests, and — since #445 — onworkflow_dispatch. Prefer cutting the tag at a commit that already has a greenmainrun: if the commit is not onmainyet, push it tomainfirst, wait for that run, then tag it. That is no longer the only path. When the tagged commit has no green run of its own, dispatch one at the tag directly:sh1 gh workflow run ci.yml --ref <tag> 2 gh workflow run security.yml --ref <tag> The tag has to have been cut after #445 merged. Dispatch reads the trigger from the workflow file at the ref you name, not from
main, so a tag whose tree predates #445 has noworkflow_dispatchto fire and the command is refused before any run starts:1 $ gh workflow run ci.yml --ref v1.0.1-beta.18 2 could not create workflow dispatch event: HTTP 422: Workflow does not have 'workflow_dispatch' trigger Every tag up to and including
v1.0.1-beta.18is in that state, so for those the push-to-main-first path above is the only one. Either way, a green run on a different commit is not evidence about this build. Note thatgh run rerunis still not a substitute — it only replays the commit its original run used.A dispatched run is not identical to the
pushrun it stands in for.github.event_nameisworkflow_dispatch, andgithub.base_refis empty; two steps in either workflow read those — the diff-coverage step (the next note) and thechangesjob's path filter — and both skip. The difference that bears hardest on what a green tick means is the same rule as above: a dispatched run executes the job definitions at the target ref, not today's. These workflow files change often and materially, so dispatching at an older tag can green-tick a weaker gate thanmaincurrently runs. Read the result as evidence about that commit under the job set that commit carried.The
testjob's diff-coverage step is gatedif: github.event_name == 'pull_request'(.github/workflows/ci.yml:187), soscripts/diff-coverage.mjsdoes not run on the push-to-mainrun this section reads, nor on a dispatched one. Diff coverage is a per-PR gate; a greentesthere means the suite and the project-wide coverage thresholds passed, nothing more.BIRDBRAIN_REQUIRE_OPENSSL=1is what stops the OpenSSL-dependent timestamp tests from turning green-by-skipping (tests/helpers/openssl.ts). Locally, runBIRDBRAIN_REQUIRE_OPENSSL=1 pnpm testfor the same reason.
2. CI-built artifacts only — must pass
- Every installer used from here on was downloaded from the GitHub Release for this tag.
- No locally built installer is used anywhere in sections 3–5, including "just to check something".
A local build is not the artifact the tester runs: it comes from a different toolchain, a
different node_modules, and a working tree that may not match the tag. Testing one tells
you nothing checkable about the other.
3. Smoke checklist — Windows NSIS and Ubuntu AppImage must pass
.deb is best-effort: run it if there is time, record failures as ordinary issues, do
not block the release on them. macOS ships as a byproduct and is unsupported — not tested
here.
Run the same eight steps per platform.
Windows (NSIS .exe) — must pass
- Install (expect the SmartScreen "More info → Run anyway" click-through; builds are unsigned) → launch
- Create a case
- Sideload the extension from
birdbrain-extension.zip(chrome://extensions→ Developer mode → Load unpacked); the dashboard banner flips to Browser Extension Connected and the top-bar indicator reads Connected - Capture a page as HTML
- Capture a page as MHTML
- View both captures in the app
- Export the case
- Quit, relaunch, confirm the case and both captures are still there
Ubuntu (AppImage) — must pass
chmod +x→ launch- Create a case
- Sideload the extension; the dashboard banner flips to Browser Extension Connected and the top-bar indicator reads Connected
- Capture a page as HTML
- Capture a page as MHTML
- View both captures in the app
- Export the case
- Quit, relaunch, confirm the case and both captures are still there
Ubuntu (.deb) — best-effort
sudo apt install ./birdbrain_<version>_amd64.deb, then the same eight steps
4. Verification — must pass
Two halves. The first is a known-answer check that the shipped verification path still produces the known-good result; the second is an end-to-end pass on evidence this build actually produced.
4a. Known-answer timestamp fixture
tests/fixtures/timestamp/ holds a real DigiCert RFC 3161 token over a fixed content hash,
with the trust anchor alongside it, so this check needs no network and no system trust
store.
This decomposes #291's single "run the verifier against tests/fixtures/timestamp/" item
into the three boxes below, because that item was not implementable as written: the
standalone verifier has no mode that reads the fixture. --self-check routes to
runSelfCheck() (src/verifier/cli.ts:117), which compares a frozen canonical-JSON golden
vector, and the only other mode takes a package directory. So the fixture is exercised by
the test suite and by hand with openssl, and the verifier's own known-answer check is
--self-check — the same coverage, split across the tools that can actually deliver it.
BIRDBRAIN_REQUIRE_OPENSSL=1 pnpm testpassed on the tagged commit (section 1 covers this) — that is what exercises the fixture throughparseTimestampTokenand the canonicalopenssl ts -verifypath.By hand, from
tests/fixtures/timestamp/:1 ```sh 2 openssl ts -verify \ 3 -in digicert-response.tsr \ 4 -queryfile request.tsq \ 5 -CAfile digicert-trusted-root-g4.pem 6 # => Verification: OK 7 ``` The standalone verifier's self-check passes on a binary built from the tagged commit (
pnpm build:verifier, thendist/verifier/birdbrain-verify --self-check; on Windows the binary isdist\verifier\birdbrain-verify.exe). This asserts the bundled canonical-JSON core is byte-identical to the in-app core; it says nothing about any particular package.
The verifier binary is not a release artifact — release.yml publishes installers and
the extension zip only, and Node SEA builds for the OS it runs on. Build it locally from
the tagged commit, and note that the verifier is section 2's one exception: both this step
and the re-verify step in 4b run a locally built binary rather than a CI-built one. That
exception covers the verifier and nothing else — every installer stays CI-built.
4b. Live capture → verify → export → re-verify
Do this on one of the section 3 platforms, using that platform's captures.
- Capture — take a fresh capture in the smoke case.
- Verify in-app — open the capture and run Verify on its provenance badge; the result is not tampered.
- Export — export the case as an evidence package.
- Re-verify outside the app — unzip the package and run
birdbrain-verify <package-dir>against it. It reports PASS. - The unzipped package contains
manifest.jsonl,evidence.json,report.html,certification.html,signing-public-key.pem,VERIFY.mdandtsa-intermediates.pem. It also containstsa-root.pemwhen a trust anchor is bundled for the configured authority — the default DigiCert TSA has one; another authority does not, and then the absence is correct. Record which case this build is in rather than ticking past it: without a bundled root, the tokens can be checked for internal consistency but not for authenticity until the runner supplies an anchor obtained independently. (#579 split the singletsa-ca-chain.pemthis step used to name; a package has not carried that file since.)
What a PASS here does and does not mean, so the tick is not read as more than it is: it is
an integrity and internal-consistency result. The manifest chain shows nobody edited the
manifest without this installation's signing key — it does not constrain the Operator,
who holds that key. The verifier's timestamp checks are structural (imprint and
byte-binding); canonical TSA authenticity is the openssl ts -verify path in the package's
VERIFY.md, which is why 4a exists separately.
5. Upgrade path — must pass
The gate item that exists because the durability contract from #286 is only worth what a real upgrade demonstrates.
- Install the previous beta on a clean machine.
- Create a case and take at least one capture in it.
- Install the candidate over the top. Use the in-app path when the previous beta is on the same public update feed — Settings → Updates → Check for updates, then Download, then Restart to update. Manual installation (Windows: run the new installer; AppImage: replace the file; do not remove user data) is the fallback, and is the only option when the previous beta predates the feed.
- Launch. The case and its capture are still there, and the capture still opens.
- A pre-migration snapshot exists in the
db-snapshotsfolder next to the database in the user data directory, and it is listed under Settings → Database → Utilities → Pre-Migration Snapshots.
Why the in-app path is now first. Until 1.0.1-beta.18 every shipped build had the
private source repository baked into its app-update.yml, so electron-updater's
unauthenticated GitHub provider could never reach a feed and no gate run could exercise the
update mechanism (#567). Builds from 1.0.1-beta.18 onward point at the public
thebristolsound/birdbrain-releases feed, so an upgrade between two of them tests what a
tester will actually do. A manual install-over-the-top proves the data survives; it proves
nothing about the mechanism that is meant to deliver the upgrade.
Record which path was used. On the .deb, the download is automatic but the install runs
dpkg behind a system password prompt, so it happens only on Restart to update — that is
expected, not a failure.
If the candidate carries no schema change, no snapshot is taken and none is expected —
record that rather than ticking the last box. Check LATEST_SCHEMA_VERSION in
src/main/services/db/core.ts against the previous beta if you need to know which case you
are in.
Run this section on at least one must-pass platform. Running it on both is better and is where the 45-minute target usually goes; one is the floor.
Go / no-go record
| Field | Value |
|---|---|
| Candidate tag | |
| CI run (link) | |
| Security run (link) | |
| Section 1 CI + Security green | pass / fail |
| Section 2 CI artifacts only | pass / fail |
| Section 3 Windows NSIS | pass / fail |
| Section 3 Ubuntu AppImage | pass / fail |
Section 3 Ubuntu .deb (best-effort) | pass / fail / not run |
| Section 4a known-answer fixture | pass / fail |
| Section 4b live capture → re-verify | pass / fail |
| Section 5 upgrade path | pass / fail |
| Elapsed | |
| Verdict | go / no-go |
| Run by / date |
Anything that failed and was accepted anyway belongs in the release notes, not in a memory.