0039. Release with release-please, GHCR images and GitHub artifact attestations
Questi contenuti non sono ancora disponibili nella tua lingua.
- Status: proposed
- Date: 2026-10-28
- Deciders: maintainer (owner approval of the first release pending)
Context
Section titled “Context”GuestCraft has no release yet. Installers build the checkout or deploy a branch to Vercel, so nobody can say which code they run or check where an image came from. Row 24 of the quality program deferred SBOMs, provenance and signed releases until the repository became public. Two reasons: GitHub artifact attestations and npm provenance need a public repository on GitHub’s free plans, and the project must not pay for anything. The repository became public on 28 October 2026.
The technical analysis (section 18.8, “Releases and supply chain”) proposes:
- Changesets for versions and changelogs, with the version pull request passing the same gates as any other;
- images identified by digest, with a CycloneDX SBOM, BuildKit provenance and keyless cosign signatures;
- published verification instructions;
- SLSA Build L2 at first, because L3 needs an isolated builder.
ADR 0003 left the release management of the npm packages open until their first publication.
Facts checked on 28 October 2026:
- Commits on
mainalready follow Conventional Commits, enforced by commitlint inquality.mainrequiresqualityandsmoke, also for administrators. - A pull request opened with
GITHUB_TOKENtriggers no workflow, and this repository does not allow Actions to open pull requests at all. So an automated release pull request needs another identity to pass the required checks. actions/attestv4 signs in-toto statements with a short-lived Sigstore certificate, from the public-good instance for public repositories. It stores them in GitHub’s attestation API and, withpush-to-registry, as OCI referrers in GHCR. Artifact storage records exist only for organization-owned repositories.actions/attest-build-provenancev4 is a wrapper aroundactions/attest.- GitHub-hosted
ubuntu-24.04-armrunners are free for public repositories, so arm64 images can be built natively instead of under QEMU. - The
Dockerfilehas three targets.workerandmigrateare the samebuildstage with a different command. A local build measuredwebat 374 MB andworkerat 3.16 GB. Syft 1.55 writes SPDX SBOMs of 2.9 MB and 5.7 MB for them, under the 16 MB limit of an attestation.
Decision
Section titled “Decision”Versions.
- One product version,
0.MINOR.PATCHbefore 1.0. Afeatcommit or a breaking change bumps the minor version (bump-minor-pre-major), and afixorperfcommit bumps the patch. - The root,
apps/webandpackages/sdkmanifests and the image tags indocker-compose.ymlcarry the version. The internal packages stay at0.0.0. - The SDK uses the product version while it is unpublished.
- The HTTP API keeps its own contract revision (ADR 0016), which each release note states.
- 1.0 is the owner’s decision.
Release pull request.
- release-please (Apache-2.0,
googleapis/release-please-actionpinned by SHA) runs on every push tomain. It keeps one release pull request with the bumped versions and the newCHANGELOG.mdsection. - It authenticates as a GitHub App owned by the maintainer, through
actions/create-github-app-token, so the release pull request runsqualityandsmokelike any other. - The workflow does nothing until the App’s client ID variable exists.
Releases.
- Merging the release pull request tags
vX.Y.Zand creates a draft pre-release (draftandforce-tag-creation). - The same workflow builds the images from the tagged commit, pushes and attests them, and only then publishes the release, with notes and SBOMs.
- A failure leaves a draft. The flow is compatible with GitHub’s immutable releases.
Images.
ghcr.io/spinny03/guestcraft(targetweb),ghcr.io/spinny03/guestcraft-worker(targetworker) andghcr.io/spinny03/guestcraft-migrate(targetmigrate). The one-shot migrator ships separately from the resident worker. Each runtime image keeps only what its entry points import (tooling/scripts/src/prune-runtime.ts): the worker (src/worker/main.ts) and the migrator (src/migrate/main.ts,src/migrate/reencrypt-main.ts) each carry their reached sources, workspace packages and third-party closure, without package scripts, first-party tests or the HTTP runtime. Kept third-party packages retain their test files because Node may load them. Conditional exports resolve as Node.js does, by the order of their keys; a computedimport(name)or unlisted indirect first-party load stops the build. A local build measured 349 MB for the worker and 342 MB for the migrator, from 876 MB each; the Docker workflow checks a file inventory per image, and the image workflow runs on every pull request.- Each is built natively on amd64 and arm64 runners, pushed by digest, without build cache, and joined into an index tagged
X.Y.ZandX.Y. - There is no
latestor0tag before 1.0, so a pull never crosses a minor version by surprise. - The OCI labels (title, description, source, revision, version, licence
AGPL-3.0-only) are on each image, and the annotations are on each index. - One composite action,
.github/actions/build-image, holds the build for the release and a pull-request dry run, so the dry run proves the release build.
Supply chain.
- For every platform image, SLSA build provenance and an SPDX 2.3 SBOM attestation, both from
actions/attest. Provenance is also attested for each index. All are pushed to GHCR as referrers. - SBOMs come from Syft, pinned by SHA-256 in
tooling/requirements/tools.jsonlike the other CI binaries. The SPDX and CycloneDX SBOMs are attached to the release. - Verification uses
gh attestation verifywith--signer-workflowpinned torelease.yml,--source-ref refs/heads/mainand--deny-self-hosted-runners. - The claim is SLSA Build L2: a hosted build platform and signed provenance.
Compose. docker-compose.yml runs the published images of its own version. docker-compose.build.yml builds the checkout with the same two targets under local tags. CI’s smoke uses the build file.
Rollback. ADR 0038 permits a previous compatible runtime to cold-start after expand-only migrations; contract migrations require roll-forward. Runtimes built before this change still enforce exact journal equality. A version without new migrations can be undone by changing the tag. Each release note lists migration kinds and the recovery boundary.
Consequences
Section titled “Consequences”- Each release has a changelog, a tag, two signed and attested multi-platform images, and SBOMs in two formats. Its notes show the digests, the verification command, the API revision and the new migrations.
- The owner must create the GitHub App once and store its client ID and private key. After the first release they must also make the two GHCR packages public. GitHub has no API for that step. The release guide lists both steps.
- The worker image is the full build stage, about 3 GB. Shrinking it means a bundled worker and migrator (ADR 0003’s tsdown artifacts), a separate change.
- Before 1.0, only the latest minor line receives fixes (SECURITY.md).
- Enforcement:
- actionlint, pinned by checksum, lints every workflow in
quality; - the
imageworkflow builds both targets on both architectures on every push tomainand on pull requests that change the build inputs; - an integration test covers the release-notes script.
- Whether the App token, the GHCR push and the attestations work is first proven by the first release; the checklist verifies them.
- actionlint, pinned by checksum, lints every workflow in
- The first changelog lists every
featandfixcommit since the repository started, because there is no earlier release to start from.
Alternatives considered
Section titled “Alternatives considered”- Changesets, which the analysis proposed. It lost because a hand-written intent file in every pull request duplicates the Conventional Commits that commitlint already enforces. Its strength, independent versions for several npm packages, does not apply while one product is released and the SDK is unpublished. This is a deviation from chapter 17, and it can be reopened when the SDK goes to npm.
- pnpm’s native
pnpm change/pnpm version -r(ADR 0003). Same per-change files. It is newer, and no maintained GitHub Action opens release pull requests with it. - BuildKit provenance and SBOM attestations (
provenance: mode=max,sbom: true). They lost because BuildKit’s attestations are unsigned manifests inside the image. Theirunknown/unknownplatform entries also confuse some clients. GitHub’s attestations are signed, logged in Sigstore’s transparency log and verifiable with one command, so the build disables BuildKit’s. - A separate keyless
cosign sign. It would sign the same digest with the same GitHub OIDC identity through the same Sigstore instance, so it adds no trust. It would add another tool and another signature to maintain. The attestations are standard Sigstore bundles. A cosign verification recipe can be documented once tested against the first release. - QEMU emulation for arm64. It is slower by an order of magnitude for
pnpm installandnext build. Native runners cost nothing on a public repository. - A reusable workflow as the signer, which GitHub suggests for SLSA Build L3. A reusable workflow inherits its caller’s token permissions or fails, so the pull-request dry run would need
packages: writeandid-token: write. It also costs a top-level permissions block, which Scorecard checks. It can be revisited for L3. - A GitHub release triggered by a tag push. Anyone with write access could start a publication by pushing a tag. Here only a merged release pull request on
mainpublishes. - A
latesttag. Before 1.0 it would move installations across minor versions, and so across forward-only migrations, on a plaindocker compose pull.