Skip to content

ci: publish Debian packages through frostyard/apt-publisher - #6

Merged
bketelsen merged 1 commit into
stablefrom
claude/publish-through-apt-publisher
Oct 3, 2026
Merged

bketelsen merged 1 commit into
stablefrom
claude/publish-through-apt-publisher

Conversation

@bketelsen

@bketelsen bketelsen commented Oct 3, 2026 •

Copy link
Copy Markdown

Summary

Frostyard's incus builds stop writing the APT repository themselves.

  • A build of stable attaches its Debian 13 packages to a GitHub release.
  • It then asks frostyard/apt-publisher, the single Debian writer (core ADR-0055), to publish them to https://repository.frostyard.org/debian/ trixie.
  • apt-publisher dispatches build to frostyard/snosi once the packages are installable (ADR-0056).

This also stops writes to the frozen legacy stable suite.

  • The release job runs repogen's publish-to-r2@v0.4.1. v0.4.1 is the last repogen release without the deb refusal; 6f85328 pinned it on 2026-09-16 to get around that refusal.
  • So every push to stable still writes dists/stable. Incus 7.4 did at 15:18 UTC on 2026-09-16, after the freeze, and core's stable drift alarm has failed since.
  • Signed stable must stay unchanged through 2027-09-30 (ADR-0055).
  • Merge this before anything else lands on stable, in particular the Incus 7.5.1 sync, Merge upstream/stable (Incus 7.5.1) into frostyard stable #7. That PR contains this commit so it can't land without it.

What changes

Everything is in sync-docker-build.py, which generates builds-docker.yml, so an upstream sync's regeneration keeps it. The workflow is regenerated: before this change the generator reproduced the committed file byte for byte.

  • release job, only for frostyard/incus on refs/heads/stable (push or manual dispatch).
    • Before, it ran on every branch push, so a push to any feature branch published too.
    • Steps:
      1. Check the packages.
        • Requires exactly incus, incus-base, incus-client, incus-extra and incus-ui-canonical, all amd64.
        • All five must share one version matching 1:<x.y[.z]>-debian13-<12-digit UTC time>, and each must be under GitHub's 2 GiB asset limit.
        • apt-publisher publishes every .deb in a release. This keeps any other OS build, architecture or partial set out of it.
        • The artifact's .buildinfo and .changes files are never uploaded.
      2. Attest the .deb files' build provenance (actions/attest-build-provenance v4.2.2).
      3. Create the GitHub release with only the .deb files.
        • The tag is the package version without its epoch, e.g. 7.5.1-debian13-202610031200.
        • Every build gets a new tag, because the build stamps the version with the UTC minute.
        • An existing tag fails the step instead of changing a published release.
        • gh uploads to a draft and publishes it only once every asset is attached, so apt-publisher never sees a partial release.
  • request-publication job (permissions: {}).
    • Sends a publish-deb repository_dispatch to frostyard/apt-publisher with APT_PUBLISH_TOKEN. The payload carries repo, tag and "codenames": ["trixie"].
    • Not continue-on-error.
    • It's a separate job, so "Re-run failed jobs" re-sends the request for the same release without rebuilding.
  • Removed:
    • the repogen step, with R2_*, CLOUDFLARE_* and REPOGEN_GPG_KEY;
    • the "Kickoff snosi" dispatch (ORG_PAT). It could never fire: it was guarded to the default branch, daily, where this workflow doesn't exist;
    • the release job's unused checkout.
  • Build job pins (ADR-0021):
    • actions/checkout and actions/upload-artifact are pinned to the commits their v4 tags point at today, so nothing that runs changes;
    • the checkout sets persist-credentials: false.

Otherwise the build job is unchanged, and other branches and tags still build as CI.

Codename and version

  • The workflow builds only Debian 13 amd64. The version is 1:<incus>-debian13-<UTC minute>.
  • -debian13- isn't a ~debNN marker, so apt-publisher registers incus for trixie only (frostyard/apt-publisher#7). This workflow also requests trixie explicitly.
  • Incus 7.5.1 will be 1:7.5.1-debian13-…, which dpkg --compare-versions sorts above trixie's current 1:7.4-debian13-202609161516.
  • Forky needs a Debian 14 build later, plus one of:
    • appending ~deb13/~deb14 to the version, which needs no apt-publisher change (…-debian13-…~deb13 < …-debian14-…~deb14, checked); or
    • a core ADR-0055 change so -debianNN- counts as a marker.

Attestation

The .deb files are attested, but apt-publisher can't use that yet.

  • Their provenance names refs/heads/stable, while apt-publisher verifies --source-ref refs/tags/<tag>. incus is therefore registered attested=no.
  • To check a file by hand: gh attestation verify incus_<version>_amd64.deb --repo frostyard/incus --source-ref refs/heads/stable.
  • Two ways to get attested=yes:
    • releases triggered by tag pushes; or
    • apt-publisher verifying a registered branch for incus, which changes ADR-0055.

Risk classification

  • Tier 4: Critical (.github/workflows/**, the workflow-and-permissions boundary; it publishes packages that snosi's incus sysext installs)

Threat analysis

  • Trust boundary: incus's stable build → frostyard/apt-publisher, through a cross-repository repository_dispatch.
  • Credentials leave this workflow. It no longer references the production signing key, R2 write credentials, the Cloudflare token or ORG_PAT.
  • APT_PUBLISH_TOKEN has Contents read/write on frostyard/apt-publisher only, which repository_dispatch requires.
    • If it leaks, it can push branches to apt-publisher. apt-publisher's main is protected by the "Protect main" ruleset: pull request required, no force-push or deletion, no bypass.
    • Only main can use the apt-repository environment, which holds the signing key and R2 credentials.
    • It can request publish-deb for any registered producer and tag. apt-publisher accepts only registered producers, and only their registered package names, codenames and architectures. For incus that means the five names, trixie, amd64/arm64/all.
    • The worst case for incus is republishing an existing incus release, which could bring back a withdrawn version.
  • Not attested. apt-publisher trusts incus's release assets as downloaded.
    • Anyone who can create releases in frostyard/incus can get incus-named packages into trixie: repo writers, and any workflow here with contents: write.
    • That's no wider than today: stable has no protection, and a push to it published through repogen.
    • Protecting stable and the attestation follow-up above would narrow it.
  • Injection.
    • Nothing is interpolated with ${{ }} inside run:; values reach scripts through env:.
    • The dispatched tag is validated by the check step's regex (digits, dots, -debian13-, 12 digits) before it's used, and repo is github.repository.
  • Least privilege:
    • workflow default contents: read;
    • build job read-only, with no persisted token;
    • release job: contents, attestations and id-token write;
    • request-publication: no GITHUB_TOKEN permissions.
  • Failure modes.
    • A failed dispatch fails the run visibly; re-run request-publication.
    • A refused or failed publication fails in apt-publisher and is re-run there.
    • apt-publisher's nightly audit, once enabled, reports latest releases that never got published.

Rollback

  • Don't simply revert. That brings back repogen @v0.4.1, which writes the frozen stable suite.
  • To stop publication quickly: remove frostyard/incus from apt-publisher's config/producers.tsv (its publish runs then refuse), or disable this workflow.
  • To unpublish a bad build: run apt-publisher's withdraw workflow (codename trixie, package, version) for each of the five packages.
  • To publish a given release by hand: run apt-publisher's publish workflow with repo=frostyard/incus and the tag.

Merge order and first publication

  1. frostyard/apt-publisher#7 registers incus.
  2. APT_PUBLISH_TOKEN. The org-secrets API already lists it for this repository; confirm it.
  3. This PR, with "Create a merge commit".
    • The head commit carries GitHub's skip-ci marker, so pushing the branch didn't start the 33-minute build.
    • A squash or rebase would carry the marker to stable and skip the build. A merge commit's message is "Merge pull request …" plus the title, so the stable build runs.
  4. Merge upstream/stable (Incus 7.5.1) into frostyard stable #7 (Incus 7.5.1).

After the merge:

  • Watch the Build (Docker) run on stable: about 35 minutes, then release and request-publication.
  • Confirm the release with gh release view <tag> -R frostyard/incus.
  • Watch apt-publisher's publish run.
  • Check through the public URL with apt-publisher's scripts/apt-canary.sh trixie amd64 incus=1:<version>.

snosi still reads the frozen stable suite until Plan 0009 Phase 5. Until snosi moves to /debian/ trixie, its images keep incus 7.4 even after apt-publisher dispatches the rebuild.

Verification

  • The generator reproduced the committed builds-docker.yml byte for byte before the change. Regenerating after it is idempotent.
  • actionlint 1.7.7 with shellcheck 0.11.0 (apt-publisher's pins) shows the same 24 pre-existing findings in upstream's build steps before and after, and none in the new jobs.
  • The package check ran against synthetic .deb files:
    • A complete set passes and ignores .buildinfo/.changes, giving tag 7.5.1-debian13-202610031200.
    • It refuses an arm64 package, a Debian 12 build, an Ubuntu 24.04 build, mixed versions, a missing or an extra package, a version without the epoch, no packages, and a ~deb13 version.
    • The real 7.4 version passes.
  • Not run: the workflow itself. No runs were triggered; the first real run is the merge.

Other findings

  • zabbly-stable and fix/incus-pkgos-trixie-deps still have builds-docker.yml with on: push, workflow_dispatch and repogen publish-to-r2@main (package-type: deb).
  • sync-upstream.yml never runs. Schedules only fire from the default branch, daily, where it doesn't exist, and GitHub doesn't list it as a workflow. Upstream syncs are manual merges.

🤖 Generated with Claude Code

The release job ran repogen's publish-to-r2 pinned to v0.4.1, the last
release that still accepts package-type deb. Every push to stable
therefore kept writing incus into the frozen legacy suite (dists/stable),
as 7.4 did on 2026-09-16. Core ADR-0055 makes frostyard/apt-publisher the
only Debian writer, and signed stable must stay unchanged.

A build of stable now:
- checks that the artifact holds exactly one Debian 13 amd64 build of
  incus, incus-base, incus-client, incus-extra and incus-ui-canonical,
  because apt-publisher publishes every .deb in the release;
- attests the .deb files' build provenance;
- creates a GitHub release holding only those .deb files, tagged with
  the package version minus its epoch (e.g. 7.5.1-debian13-202610031200);
- sends publish-deb to frostyard/apt-publisher with APT_PUBLISH_TOKEN,
  for trixie only and not continue-on-error, from a job with no
  GITHUB_TOKEN permissions.

Builds of any other branch or tag stop after the build job. Before, the
release job ran for every branch push.

Removed: the repogen step with its R2, Cloudflare and signing secrets,
and the "Kickoff snosi" dispatch, which could never fire: it required the
default branch, daily, where this workflow does not exist. apt-publisher
now dispatches build to snosi after the packages are live (ADR-0056).

The build job's checkout and upload-artifact are pinned to the commits
their v4 tags point at (ADR-0021), and the checkout no longer keeps the
token in .git/config.

All of it is generated by sync-docker-build.py, so regenerating the
workflow during an upstream sync keeps it.

[skip ci] keeps the push of this branch from starting the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bketelsen
bketelsen merged commit f3419a8 into stable Oct 3, 2026
@bketelsen
bketelsen deleted the claude/publish-through-apt-publisher branch October 3, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant