Send the store the zip that the release attests¶
- Status: accepted
- Date: 2026-11-01
Context¶
The releases carried SBOMs, notices and a provenance, but not the extension. That had two costs:
- Whoever installs it without the Chrome Web Store, such as a tester or the admin of a club, needed Node.js and a build.
- The job package of
publish-store.ymlbuilt its own zip from the tag, outside the isolated build of the release. The store got a package that no provenance described, and that nothing tied to the signed tag beyond the version in its manifest.
A release is immutable once published, so a file has to join its draft inside the build. Since v1.3.0, the blueprint offers two ways: a hook of the project, scripts/release-assets.sh, whose files the release attests and attaches (its decision 0084), and a project kind chrome-extension (its decision 0085).
Options¶
- Attach a zip, and keep the build of the store. Two builds of one version, which can differ, and the store still gets the one without a provenance.
- The hook, and the store takes its zip. One file for everyone, in a change of this project alone, with
PUBLISH_TOstillnone. - The kind
chrome-extension. The same result, with the workflow of the store in the blueprint. It also moves the checks and the workflow that this project keeps for itself, which is a change of its own.
Decision¶
Option 2.
scripts/release-assets.shcallsscripts/package-extension.sh, which writesplayer-switch-for-autodarts-<version>.zip. It stops when the manifest lacks the version of the release, and a beta gets the numbers of its version in the manifest and the whole asversion_name.- From a clean checkout, the same commit makes the same zip, byte for byte, in Python on every system, so that anyone can build it again and compare. So the build starts from an empty
extension/lib/, and the files are stored rather than compressed: what zlib makes of a file depends on its build, Python on Windows and on Linux gave different bytes, and a zip of about 330 KB instead of 110 KB costs nobody anything. That also takes the fallback to PowerShell, whose archives have backslashes, away. - The job package of
publish-store.ymltakes that file from the release by its exact name, and runsgh attestation verifyon it forbuild-release.yml,refs/heads/mainand a runner of GitHub, then compares the version in its manifest with the release. It runs no code of the repository and no npm package, and reaches GitHub and Sigstore alone. - The zip holds only the kinds of file that
scripts/package-extension.shlists and each file that the manifest names; any other kind stops the build, so nothing new reaches the users unnoticed.scripts/check-project.shbuilds the zip twice, the second time as a release does and in another time zone, and compares the bytes. - The job package hands on the sha256 of the zip. The job store compares its artifact with that sum before it signs in, and names the sum in the summary of the run, so that anyone can compare what went to the store with a download or a build.
- A release without the zip, or a zip that fails a check, stays off the store and fails the run. There is no fallback to a build: that fallback is what an attacker of a job would ask for.
- The README and the website offer the zip as the way to install without the store. The screenshot script renders the picture of
chrome://extensions, without an extension in it, so that a release doesn't change its pixels.
Consequences¶
A player needs no Node.js, and the package of a player and that of the store are one file, which the SBOMs of the release now describe in place of the sources. The job package needs no npm registry, and the store gets nothing that the release doesn't attest.
A release from before the zip, up to 1.1.0, can't go to the store any more, not even by hand with its tag: the run says so, and the fix is a new release. A copy loaded from the zip gets no update by itself, which the README and the website say.
A move to the kind chrome-extension takes scripts/release-assets.sh away, because that kind builds the zip itself and refuses a *.zip of the hook; test/release-zip.test.ts names this next to PUBLISH_TO.