diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 0f1e952..cdb887c 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -123,7 +123,8 @@ jobs: run: | build() { docker buildx build -f release-tooling/package-repos.Dockerfile \ - --secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY "$@" packages + --secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY \ + --build-context packages=packages "$@" . } build --target checks --output type=local,dest=checks build --target site --output type=local,dest=site diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a441a8..0b29134 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,11 @@ jobs: else echo "code=true" >> "$GITHUB_OUTPUT" fi + if git diff --quiet "$base"...HEAD -- release-tooling Dockerfile .github/workflows/packages.yml; then + echo "packaging=false" >> "$GITHUB_OUTPUT" + else + echo "packaging=true" >> "$GITHUB_OUTPUT" + fi echo "base=$base" >> "$GITHUB_OUTPUT" - name: CHANGELOG entry for data changes run: | @@ -50,6 +55,13 @@ jobs: - name: Lowest supported Go if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }} run: docker build --build-arg GO_VERSION=1.22.12 --target portable . + - name: Package repositories, signed with throwaway keys + if: ${{ !cancelled() && steps.changes.outputs.packaging == 'true' }} + run: | + docker build -f release-tooling/package-repos.Dockerfile --target throwaway-keys --output "$RUNNER_TEMP/keys" . + PACKAGES_GPG_KEY="$(cat "$RUNNER_TEMP/keys/gpg.asc")" PACKAGES_RSA_KEY="$(cat "$RUNNER_TEMP/keys/rsa.pem")" \ + docker build -f release-tooling/package-repos.Dockerfile --target checks \ + --secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY . release: name: Release from CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md index b4e8ea7..efad512 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Rules -- Go runs only through `docker compose run --rm `; the merge gate is `docker build .` plus CI's changelog check. +- Go runs only through `docker compose run --rm `; the merge gate is `docker build .` plus CI's changelog check, and, where a change touches the packaging, the package-repository checks the README's Development section shows. - Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit. - A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR, and so does a change to a flag, an exit code, an exported name, a fence, a builtin or the lowest Go; what is major is the README's Versioning table. - One-line commit messages: no ticket prefix, no repo name, no authorship trailers. diff --git a/README.md b/README.md index e23d51c..e62d881 100644 --- a/README.md +++ b/README.md @@ -1206,7 +1206,7 @@ Every pull request runs `docker build .` against both the latest and the lowest supported Go, and must pass before it can be merged — unless it changes none of the files the build and its tests read, nor the workflow itself, in which case it's skipped. That build is the whole gate except the changelog check, which CI -runs against the PR base: vet, complexity, the format check, the Go tests and the +runs against the PR base, and the package-repository checks below: vet, complexity, the format check, the Go tests and the release tooling's tests. Run it locally before pushing. The lowest Go builds the `portable` stage, which skips the format check and the release tooling's tests: @@ -1216,6 +1216,17 @@ docker build --build-arg GO_VERSION=1.22.12 --target portable . # lowest support GO_VERSION=1.22.12 docker compose run --rm test # the same tests, without the image build ``` +A change to `release-tooling/`, the `Dockerfile` or the Packages workflow also builds the +apt, dnf and apk repositories from snapshot packages, signed with throwaway keys, and +installs fejkdata from each: + +```sh +docker build -f release-tooling/package-repos.Dockerfile --target throwaway-keys --output /tmp/fejkdata-keys . +PACKAGES_GPG_KEY="$(cat /tmp/fejkdata-keys/gpg.asc)" PACKAGES_RSA_KEY="$(cat /tmp/fejkdata-keys/rsa.pem)" \ + docker build -f release-tooling/package-repos.Dockerfile --target checks \ + --secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY . +``` + Goal 3 is scored by a panel of simulated readers, and [`AGENTS.md`](AGENTS.md) says what the score asks of a pull request. Goal 5 is checked against simulated template writers from the Audience. @@ -1268,10 +1279,10 @@ the AUR, and deploys the apt, dnf and apk repositories to GitHub Pages. It reads repository secrets `TAP_GITHUB_TOKEN`, a token that can push to `larvit/homebrew-tap` and `larvit/scoop-bucket`; `AUR_KEY`, the AUR account's SSH key; and `PACKAGES_GPG_KEY` and `PACKAGES_RSA_KEY`, the private keys that sign the repositories. The image's GHCR package -must be public, and Pages must deploy from GitHub Actions. To package a release again, run the workflow by hand with its tag; it replaces -what it uploaded before. For a tag older than the latest release it leaves the tap, the -bucket, the AUR, the image's `latest` and the repositories alone, since each holds one -version. +must be public, and Pages must deploy from GitHub Actions. To package a release again, +run the workflow by hand with its tag; it replaces what it uploaded before. For a tag older +than the latest release it leaves the tap, the bucket, the AUR, the image's `latest` and +the repositories alone, since each holds one version. ## License diff --git a/docs/decisions.md b/docs/decisions.md index bee033c..6ff0214 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -973,5 +973,6 @@ dnf) and an RSA key (apk), and any static host can serve the result. GitHub alre code (see "GitHub is canonical"), so Pages adds no new vendor; the image sits on GHCR for the same reason, and as OCI it moves by changing its path. A Pages site holds at most 1 GB and a release's packages take about 35 MB, so the repositories hold the latest release only; every -release keeps its own packages, and the image keeps a tag per version. Valid while GitHub -hosts the code. +release keeps its own packages, and the image keeps a tag per version. Leaving Pages costs +each Linux user an edit: their apt list, `.repo` file or apk line names `larvit.github.io`. +Valid while GitHub hosts the code. diff --git a/release-tooling/package-repos.Dockerfile b/release-tooling/package-repos.Dockerfile index bb20d9e..57b5e02 100644 --- a/release-tooling/package-repos.Dockerfile +++ b/release-tooling/package-repos.Dockerfile @@ -1,19 +1,37 @@ # The apt, dnf and apk repositories GitHub Pages serves, signed, from the .deb, .rpm and -# .apk files in the build context. The checks target installs fejkdata from each: -# -# docker build -f package-repos.Dockerfile --secret id=gpg,env=PACKAGES_GPG_KEY \ -# --secret id=rsa,env=PACKAGES_RSA_KEY --target checks -# docker build ... --target site --output +# .apk files of the `packages` stage: a snapshot built from the source here, or a release's +# when `--build-context packages=` replaces it. The checks target installs fejkdata +# from each repository; the README's Development section shows the commands. ARG BASE_URL=https://larvit.github.io/fejkdata +FROM goreleaser/goreleaser:v2.18.3 AS snapshot +WORKDIR /w/src +COPY . . +RUN git init -q && git add -A && git -c user.name=snapshot -c user.email=snapshot@example.invalid commit -qm snapshot \ + && goreleaser release --snapshot --clean --skip=publish,docker,homebrew,scoop,aur \ + --config release-tooling/goreleaser.yaml + +FROM scratch AS packages +COPY --from=snapshot /w/dist/*.deb /w/dist/*.rpm /w/dist/*.apk / + +FROM debian:trixie-20260918-slim AS throwaway-keys-maker +RUN apt-get update && apt-get install -y --no-install-recommends gnupg openssl && rm -rf /var/lib/apt/lists/* +RUN export GNUPGHOME=/tmp/gnupg && mkdir -m 700 $GNUPGHOME \ + && gpg --batch --quiet --passphrase '' --quick-gen-key 'fejkdata throwaway ' ed25519 sign never \ + && mkdir /keys && gpg --batch --armor --export-secret-keys > /keys/gpg.asc \ + && openssl genrsa -out /keys/rsa.pem 2048 2>/dev/null + +FROM scratch AS throwaway-keys +COPY --from=throwaway-keys-maker /keys / + FROM debian:trixie-20260918-slim AS deb-rpm ARG BASE_URL RUN apt-get update && apt-get install -y --no-install-recommends apt-utils createrepo-c dpkg-dev gnupg \ && rm -rf /var/lib/apt/lists/* -COPY *.deb /site/deb/pool/ -COPY *.rpm /site/rpm/ +COPY --from=packages *.deb /site/deb/pool/ +COPY --from=packages *.rpm /site/rpm/ WORKDIR /site -RUN --mount=type=secret,id=gpg,required=true \ +RUN --mount=type=secret,id=gpg,required=true --mount=type=tmpfs,target=/root/.gnupg \ gpg --batch --quiet --import /run/secrets/gpg \ && gpg --armor --export > fejkdata.asc \ && cd deb \ @@ -37,12 +55,10 @@ RUN --mount=type=secret,id=gpg,required=true \ FROM alpine:3.22.2 AS apk RUN apk add --no-cache abuild openssl -COPY *.apk /packages/ -RUN --mount=type=secret,id=rsa,required=true \ - mkdir -p /keys /site/apk/x86_64 /site/apk/aarch64 \ - && cp /run/secrets/rsa /keys/fejkdata.rsa \ - && openssl rsa -in /keys/fejkdata.rsa -pubout -out /keys/fejkdata.rsa.pub 2>/dev/null \ - && cp /keys/fejkdata.rsa.pub /site/apk/ \ +COPY --from=packages *.apk /packages/ +RUN --mount=type=secret,id=rsa,required=true,target=/keys/fejkdata.rsa \ + mkdir -p /site/apk/x86_64 /site/apk/aarch64 \ + && openssl rsa -in /keys/fejkdata.rsa -pubout -out /site/apk/fejkdata.rsa.pub 2>/dev/null \ && cp /packages/*_linux_amd64.apk /site/apk/x86_64/ \ && cp /packages/*_linux_arm64.apk /site/apk/aarch64/ \ && for arch in x86_64 aarch64; do \