Salta ai contenuti

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)

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 main already follow Conventional Commits, enforced by commitlint in quality. main requires quality and smoke, also for administrators.
  • A pull request opened with GITHUB_TOKEN triggers 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/attest v4 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, with push-to-registry, as OCI referrers in GHCR. Artifact storage records exist only for organization-owned repositories. actions/attest-build-provenance v4 is a wrapper around actions/attest.
  • GitHub-hosted ubuntu-24.04-arm runners are free for public repositories, so arm64 images can be built natively instead of under QEMU.
  • The Dockerfile has three targets. worker and migrate are the same build stage with a different command. A local build measured web at 374 MB and worker at 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.

Versions.

  • One product version, 0.MINOR.PATCH before 1.0. A feat commit or a breaking change bumps the minor version (bump-minor-pre-major), and a fix or perf commit bumps the patch.
  • The root, apps/web and packages/sdk manifests and the image tags in docker-compose.yml carry the version. The internal packages stay at 0.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-action pinned by SHA) runs on every push to main. It keeps one release pull request with the bumped versions and the new CHANGELOG.md section.
  • It authenticates as a GitHub App owned by the maintainer, through actions/create-github-app-token, so the release pull request runs quality and smoke like any other.
  • The workflow does nothing until the App’s client ID variable exists.

Releases.

  • Merging the release pull request tags vX.Y.Z and creates a draft pre-release (draft and force-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 (target web), ghcr.io/spinny03/guestcraft-worker (target worker) and ghcr.io/spinny03/guestcraft-migrate (target migrate). 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 computed import(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.Z and X.Y.
  • There is no latest or 0 tag 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.json like the other CI binaries. The SPDX and CycloneDX SBOMs are attached to the release.
  • Verification uses gh attestation verify with --signer-workflow pinned to release.yml, --source-ref refs/heads/main and --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.

  • 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 image workflow builds both targets on both architectures on every push to main and 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.
  • The first changelog lists every feat and fix commit since the repository started, because there is no earlier release to start from.
  • 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. Their unknown/unknown platform 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 install and next 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: write and id-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 main publishes.
  • A latest tag. Before 1.0 it would move installations across minor versions, and so across forward-only migrations, on a plain docker compose pull.