A project builds its own release assets in a job that may write nothing¶
- Status: accepted
- Date: 2026-10-25
Context¶
The build of a release, build-release.yml, packages an asset only where PUBLISH_TO delivers it: the zip of HACS, the wheel, the npm package and the signed Nextcloud app. Every other project ships only its SBOMs, its notices and the provenance. player-switch-for-autodarts is a browser extension that the Chrome Web Store delivers (PUBLISH_TO = "none"): its releases had no zip of the extension, so the package that its users install had no signed provenance, and nobody who installs it by hand could download it (#470). A release is immutable once it is published, so such an asset has to reach the draft inside the build.
Options¶
- A new
PUBLISH_TOfor each kind of package, such aschrome-web-store, with its build in the blueprint. - A hook of the project,
scripts/release-assets.sh, that runs in the job that attests and writes the release, as the build of an npm package does. - The same hook in a job of its own in
build-release.yml, with a token that may only read, whose files the job of the release takes as an artifact and attests with the rest.
Decision¶
Option 3. A store or a format that one repository needs would make the blueprint grow a build for each, and the hook of option 2 would run the project's code and its npm packages in a job that holds an OIDC token and may write the release and its attestations.
- The contract. When a project has
scripts/release-assets.sh, the job assets of the project runsbash scripts/release-assets.sh FOLDER VERSIONat the commit of the release, after the Node.js of.nvmrcandnpm ci --ignore-scriptswhere apackage.jsonis, without a cache. It writes each asset as a file at the top ofFOLDER, by a name of letters, digits,.,_,+and-; the job fails for no file, a folder, a link or another name. - The job.
permissions: contents: read, no environment and no secret; Harden-Runner blocks every host but GitHub, Node.js and npm, withoutsudoand Docker. It checks out the tag of the release and fails unless the tag stands at the commit of the release, which the job of the release checks the signed tag against before it attests anything, and hands the files on as the artifactproject-assets, kept for seven days. - The release. The job release assets takes the artifact, refuses a name that one of its own assets has, as well as
provenance.*,*.intoto.jsonl,*.spdx.json,*.cdx.json,*.openvex.jsonandTHIRD_PARTY_NOTICES.md, which verify-release reads as files of the release itself, and the names that the registry jobs ofrelease.ymldownload by pattern (*.whland*.tar.gzforpypi,*.tgzfornpm,*.tar.gz*fornextcloud-appstore,*.zipforchrome-web-store), attests every asset with the sameattest-build-provenancestep and attaches it to the draft. Where no registry delivers the project, the zips and tarballs of the hook are what the release ships, and its SBOMs describe them as a package.verify-release.ymlchecks the signed provenance of every asset, these too. - Projects without the hook release as before: the new job only checks out and ends.
scripts/release-assets.shis a file of the project, in_skip_if_exists.
Consequences¶
A project can attach what no kind of the blueprint builds, attested, to each release. A Chrome extension doesn't need the hook: its kind builds and attests the zip (decision 0085). Each release runs one more short job. A hook that needs another host fails with the blocked call in the summary of Harden-Runner; that host goes into the job's list, in the blueprint.