Answer architecture round 3: 0 🔴, 4 🟡; keep signing keys out of image layers, and build and install from the package repositories in the gate with throwaway keys

This commit is contained in:
2026-10-13 20:01:37 +02:00
committed by Lillem4n
parent f08182fb94
commit e6e06d007b
6 changed files with 64 additions and 23 deletions
+2 -1
View File
@@ -123,7 +123,8 @@ jobs:
run: | run: |
build() { build() {
docker buildx build -f release-tooling/package-repos.Dockerfile \ 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 checks --output type=local,dest=checks
build --target site --output type=local,dest=site build --target site --output type=local,dest=site
+12
View File
@@ -32,6 +32,11 @@ jobs:
else else
echo "code=true" >> "$GITHUB_OUTPUT" echo "code=true" >> "$GITHUB_OUTPUT"
fi 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" echo "base=$base" >> "$GITHUB_OUTPUT"
- name: CHANGELOG entry for data changes - name: CHANGELOG entry for data changes
run: | run: |
@@ -50,6 +55,13 @@ jobs:
- name: Lowest supported Go - name: Lowest supported Go
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }} if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
run: docker build --build-arg GO_VERSION=1.22.12 --target portable . 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: release:
name: Release from CHANGELOG.md name: Release from CHANGELOG.md
+1 -1
View File
@@ -1,6 +1,6 @@
# Rules # Rules
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo|generate>`; the merge gate is `docker build .` plus CI's changelog check. - Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo|generate>`; 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. - 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. - 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. - One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
+16 -5
View File
@@ -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 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 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 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 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: `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 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 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 what the score asks of a pull request. Goal 5 is checked against simulated template
writers from the Audience. 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 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 `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 `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 must be public, and Pages must deploy from GitHub Actions. To package a release again,
what it uploaded before. For a tag older than the latest release it leaves the tap, the run the workflow by hand with its tag; it replaces what it uploaded before. For a tag older
bucket, the AUR, the image's `latest` and the repositories alone, since each holds one than the latest release it leaves the tap, the bucket, the AUR, the image's `latest` and
version. the repositories alone, since each holds one version.
## License ## License
+3 -2
View File
@@ -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 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 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'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 release keeps its own packages, and the image keeps a tag per version. Leaving Pages costs
hosts the code. each Linux user an edit: their apt list, `.repo` file or apk line names `larvit.github.io`.
Valid while GitHub hosts the code.
+30 -14
View File
@@ -1,19 +1,37 @@
# The apt, dnf and apk repositories GitHub Pages serves, signed, from the .deb, .rpm and # 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: # .apk files of the `packages` stage: a snapshot built from the source here, or a release's
# # when `--build-context packages=<dir>` replaces it. The checks target installs fejkdata
# docker build -f package-repos.Dockerfile --secret id=gpg,env=PACKAGES_GPG_KEY \ # from each repository; the README's Development section shows the commands.
# --secret id=rsa,env=PACKAGES_RSA_KEY --target checks <dir>
# docker build ... --target site --output <site dir> <dir>
ARG BASE_URL=https://larvit.github.io/fejkdata 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 <throwaway@example.invalid>' 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 FROM debian:trixie-20260918-slim AS deb-rpm
ARG BASE_URL ARG BASE_URL
RUN apt-get update && apt-get install -y --no-install-recommends apt-utils createrepo-c dpkg-dev gnupg \ RUN apt-get update && apt-get install -y --no-install-recommends apt-utils createrepo-c dpkg-dev gnupg \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY *.deb /site/deb/pool/ COPY --from=packages *.deb /site/deb/pool/
COPY *.rpm /site/rpm/ COPY --from=packages *.rpm /site/rpm/
WORKDIR /site 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 --batch --quiet --import /run/secrets/gpg \
&& gpg --armor --export > fejkdata.asc \ && gpg --armor --export > fejkdata.asc \
&& cd deb \ && cd deb \
@@ -37,12 +55,10 @@ RUN --mount=type=secret,id=gpg,required=true \
FROM alpine:3.22.2 AS apk FROM alpine:3.22.2 AS apk
RUN apk add --no-cache abuild openssl RUN apk add --no-cache abuild openssl
COPY *.apk /packages/ COPY --from=packages *.apk /packages/
RUN --mount=type=secret,id=rsa,required=true \ RUN --mount=type=secret,id=rsa,required=true,target=/keys/fejkdata.rsa \
mkdir -p /keys /site/apk/x86_64 /site/apk/aarch64 \ mkdir -p /site/apk/x86_64 /site/apk/aarch64 \
&& cp /run/secrets/rsa /keys/fejkdata.rsa \ && openssl rsa -in /keys/fejkdata.rsa -pubout -out /site/apk/fejkdata.rsa.pub 2>/dev/null \
&& openssl rsa -in /keys/fejkdata.rsa -pubout -out /keys/fejkdata.rsa.pub 2>/dev/null \
&& cp /keys/fejkdata.rsa.pub /site/apk/ \
&& cp /packages/*_linux_amd64.apk /site/apk/x86_64/ \ && cp /packages/*_linux_amd64.apk /site/apk/x86_64/ \
&& cp /packages/*_linux_arm64.apk /site/apk/aarch64/ \ && cp /packages/*_linux_arm64.apk /site/apk/aarch64/ \
&& for arch in x86_64 aarch64; do \ && for arch in x86_64 aarch64; do \