Answer architecture round 2: 1 🔴, 0 🟡; self-host signed apt, dnf and apk repositories on GitHub Pages in place of Cloudsmith, and keep an older tag's image while leaving latest alone

This commit is contained in:
2026-10-13 19:56:37 +02:00
committed by Lillem4n
parent cc7d4fb972
commit f08182fb94
8 changed files with 173 additions and 56 deletions
+49 -16
View File
@@ -11,7 +11,9 @@ on:
required: true
AUR_KEY:
required: true
CLOUDSMITH_API_KEY:
PACKAGES_GPG_KEY:
required: true
PACKAGES_RSA_KEY:
required: true
workflow_dispatch:
inputs:
@@ -34,6 +36,8 @@ jobs:
permissions:
contents: write
packages: write
outputs:
latest: ${{ steps.latest.outputs.latest }}
steps:
- uses: actions/checkout@v7
with:
@@ -54,8 +58,8 @@ jobs:
with:
go-version: ${{ steps.go.outputs.version }}
cache: false
# The tap, the bucket, the AUR and the image's latest tag each hold one version, so
# only the latest release updates them.
# The tap, the bucket, the AUR, the image's latest tag and the Linux repositories each
# hold one version, so only the latest release updates them.
- name: Channels holding one version
id: latest
env:
@@ -63,9 +67,10 @@ jobs:
TAG: ${{ inputs.tag }}
run: |
if [ "$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)" = "$TAG" ]; then
echo "skip=" >> "$GITHUB_OUTPUT"
echo "latest=true" >> "$GITHUB_OUTPUT"
else
echo "skip=--skip=homebrew,scoop,aur,docker" >> "$GITHUB_OUTPUT"
echo "skip=--skip=homebrew,scoop,aur" >> "$GITHUB_OUTPUT"
echo "OLDER_TAG=1" >> "$GITHUB_ENV"
fi
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
@@ -82,20 +87,48 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
- name: Upload to the Cloudsmith repositories
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
pip install --quiet cloudsmith-cli==1.27.0
for f in dist/*.deb; do cloudsmith push deb --republish larvit/fejkdata/any-distro/any-version "$f"; done
for f in dist/*.rpm; do cloudsmith push rpm --republish larvit/fejkdata/any-distro/any-version "$f"; done
for f in dist/*.apk; do cloudsmith push alpine --republish larvit/fejkdata/alpine/any-version "$f"; done
- name: The built binary and the published image print the tag
env:
TAG: ${{ inputs.tag }}
IMAGE: ${{ steps.latest.outputs.skip == '' }}
run: |
test "$(dist/fejkdata_linux_amd64_v1/fejkdata --version)" = "fejkdata $TAG"
if [ "$IMAGE" = true ]; then
test "$(docker run --rm "ghcr.io/larvit/fejkdata:${TAG#v}" --version)" = "fejkdata $TAG"
fi
repos:
name: apt, dnf and apk repositories for ${{ inputs.tag }}
needs: packages
if: needs.packages.outputs.latest == 'true'
runs-on: ubuntu-26.04
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: The release's Linux packages
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
run: gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir packages --pattern '*.deb' --pattern '*.rpm' --pattern '*.apk'
- uses: docker/setup-buildx-action@v4
- name: Install fejkdata from each repository, then export them
env:
PACKAGES_GPG_KEY: ${{ secrets.PACKAGES_GPG_KEY }}
PACKAGES_RSA_KEY: ${{ secrets.PACKAGES_RSA_KEY }}
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
}
build --target checks --output type=local,dest=checks
build --target site --output type=local,dest=site
- uses: actions/upload-pages-artifact@v5
with:
path: site
- id: deploy
uses: actions/deploy-pages@v5
+4 -1
View File
@@ -78,10 +78,13 @@ jobs:
permissions:
contents: write
packages: write
pages: write
id-token: write
uses: ./.github/workflows/packages.yml
with:
tag: ${{ needs.release.outputs.tag }}
secrets:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
PACKAGES_GPG_KEY: ${{ secrets.PACKAGES_GPG_KEY }}
PACKAGES_RSA_KEY: ${{ secrets.PACKAGES_RSA_KEY }}
+1 -1
View File
@@ -122,7 +122,7 @@ In [docs/decisions.md](docs/decisions.md):
- Goal 7.2 holds while a first template can be written with no escape; a literal brace still takes `{{` or `}}`
- The locales are Ethnologue 2026's ten most-spoken languages, each in the country with the most speakers of it, the Nordic countries and Germany
- A country with no open address register covering it whole ships its places and postal codes without streets
- The deb, rpm and apk repositories live on Cloudsmith, and the image on GHCR
- The apt, dnf and apk repositories are static files on GitHub Pages, holding the latest release only
# Scoring run
+25 -14
View File
@@ -43,13 +43,23 @@ docker run --rm -i ghcr.io/larvit/fejkdata < template.json # any OS with
go install github.com/larvit/fejkdata/cmd/fejkdata@latest # any OS with Go
```
Debian, Ubuntu, Fedora, RHEL and Alpine install from fejkdata's Cloudsmith repository,
after one setup script adds it and its signing key:
Debian, Ubuntu, Fedora, RHEL and Alpine install from fejkdata's signed repositories, which
hold the latest release:
```sh
curl -1sLf https://dl.cloudsmith.io/public/larvit/fejkdata/cfg/setup/bash.deb.sh | sudo -E bash && sudo apt install fejkdata
curl -1sLf https://dl.cloudsmith.io/public/larvit/fejkdata/cfg/setup/bash.rpm.sh | sudo -E bash && sudo dnf install fejkdata
curl -1sLf https://dl.cloudsmith.io/public/larvit/fejkdata/cfg/setup/bash.alpine.sh | sudo -E bash && sudo apk add fejkdata
# Debian, Ubuntu
sudo curl -fsSLo /etc/apt/keyrings/fejkdata.asc https://larvit.github.io/fejkdata/fejkdata.asc
echo 'deb [signed-by=/etc/apt/keyrings/fejkdata.asc] https://larvit.github.io/fejkdata/deb stable main' | sudo tee /etc/apt/sources.list.d/fejkdata.list
sudo apt update && sudo apt install fejkdata
# Fedora, RHEL
sudo curl -fsSLo /etc/yum.repos.d/fejkdata.repo https://larvit.github.io/fejkdata/rpm/fejkdata.repo
sudo dnf install fejkdata
# Alpine, as root
wget -qO /etc/apk/keys/fejkdata.rsa.pub https://larvit.github.io/fejkdata/apk/fejkdata.rsa.pub
echo https://larvit.github.io/fejkdata/apk >> /etc/apk/repositories
apk add fejkdata
```
Each [release](https://github.com/larvit/fejkdata/releases) also carries the binaries for
@@ -1156,7 +1166,7 @@ internal/rows/ a table's rows: the TSV, the options proved over it, the links b
data/ the shipped modules, each a Go package embedding its categories (JSON, and a TSV per table) and its manifest; and Modules, which lists them
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md), and the modules they share
docs/ the decision log, the register research behind the shipped data, the survey of other libraries, panels' scores and rulings on how readable the code is, and what each chunk cost an agent to read
release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages and image built for it
release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages, image and package repositories built for it
testdata/ the pinned shipped shape (see Versioning)
```
@@ -1253,14 +1263,15 @@ To release, head `CHANGELOG.md` with the version's section in place of `Unreleas
and merge: once `main` passes the gate, CI tags that commit `vX.Y.Z` and publishes
the GitHub release with the section as its body. A top heading of `[Unreleased]`
publishes nothing. The new release then runs the Packages workflow, which attaches the
binaries and packages, pushes the image, uploads to Cloudsmith and updates the Homebrew
tap, the Scoop bucket and the AUR. It reads the 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 `CLOUDSMITH_API_KEY`; the image's GHCR package must be
public. 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 updates only that
release's assets and Cloudsmith, since the tap, the bucket, the AUR and the image's
`latest` each hold one version.
binaries and packages, pushes the image, updates the Homebrew tap, the Scoop bucket and
the AUR, and deploys the apt, dnf and apk repositories to GitHub Pages. It reads the
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.
## License
+10 -10
View File
@@ -963,15 +963,15 @@ section says why beside each such country. This works against goal 2.1 until
`todo.md` item 218 gives those countries streets. Valid until the maintainer accepts ODbL
data, or the country opens a register.
## The deb, rpm and apk repositories live on Cloudsmith, and the image on GHCR
## The apt, dnf and apk repositories are static files on GitHub Pages, holding the latest release only
2026-10-13, decided by the maintainer. Serves goal 1.1; technical principle: prefer the
vendor-neutral option, sidestepped deliberately (`~/.claude/principles/technical-principles.md`).
A repository apt, dnf or apk can upgrade from needs signed indexes for three formats.
Self-hosting them on GitHub Pages would put two signing keys and the tooling that rebuilds
every index into this repository. Cloudsmith signs and indexes all three, for free to open
source, so the tie buys that tooling, not a capability only Cloudsmith has. Leaving it costs
each Linux user one new setup line. Every release's assets carry the same packages, so
nothing is lost in a move. GHCR sits beside the code and takes CI's own token; the image is
OCI, so leaving it changes only the image path. Valid while Cloudsmith's open-source plan
stays free.
vendor-neutral option (`~/.claude/principles/technical-principles.md`). A hosted package
service would sign and index the packages for us, but only saves tooling, so the repositories
are built here: `release-tooling/package-repos.Dockerfile` signs them with a GPG key (apt,
dnf) and an RSA key (apk), and any static host can serve the result. GitHub already hosts 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
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.
+1 -1
View File
@@ -58,7 +58,7 @@ dockers_v2:
- ghcr.io/larvit/fejkdata
tags:
- "{{ .Version }}"
- latest
- '{{ if not (isEnvSet "OLDER_TAG") }}latest{{ end }}'
dockerfile: ../tooling/release-tooling/image.Dockerfile
labels:
org.opencontainers.image.source: *homepage
+82
View File
@@ -0,0 +1,82 @@
# 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 <dir>
# docker build ... --target site --output <site dir> <dir>
ARG BASE_URL=https://larvit.github.io/fejkdata
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/
WORKDIR /site
RUN --mount=type=secret,id=gpg,required=true \
gpg --batch --quiet --import /run/secrets/gpg \
&& gpg --armor --export > fejkdata.asc \
&& cd deb \
&& for arch in amd64 arm64; do \
mkdir -p dists/stable/main/binary-$arch \
&& dpkg-scanpackages --arch $arch pool > dists/stable/main/binary-$arch/Packages \
&& gzip -9k dists/stable/main/binary-$arch/Packages; \
done \
&& apt-ftparchive -o APT::FTPArchive::Release::Origin=fejkdata \
-o APT::FTPArchive::Release::Suite=stable -o APT::FTPArchive::Release::Codename=stable \
-o APT::FTPArchive::Release::Components=main \
-o "APT::FTPArchive::Release::Architectures=amd64 arm64" \
release dists/stable > dists/stable/Release \
&& gpg --batch --yes --clearsign -o dists/stable/InRelease dists/stable/Release \
&& gpg --batch --yes --armor --detach-sign -o dists/stable/Release.gpg dists/stable/Release \
&& cd ../rpm \
&& createrepo_c --quiet . \
&& gpg --batch --yes --armor --detach-sign repodata/repomd.xml \
&& printf '[fejkdata]\nname=fejkdata\nbaseurl=%s/rpm\nrepo_gpgcheck=1\ngpgcheck=0\ngpgkey=%s/fejkdata.asc\n' \
"$BASE_URL" "$BASE_URL" > fejkdata.repo
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/ \
&& cp /packages/*_linux_amd64.apk /site/apk/x86_64/ \
&& cp /packages/*_linux_arm64.apk /site/apk/aarch64/ \
&& for arch in x86_64 aarch64; do \
cd /site/apk/$arch \
&& apk index --quiet --allow-untrusted -o APKINDEX.tar.gz *.apk \
&& mv *.apk "fejkdata-$(tar -xzOf APKINDEX.tar.gz APKINDEX | sed -n 's/^V://p').apk" \
&& abuild-sign -q -k /keys/fejkdata.rsa APKINDEX.tar.gz; \
done
FROM scratch AS site
COPY --from=deb-rpm /site /
COPY --from=apk /site /
FROM debian:trixie-20260918-slim AS check-deb
COPY --from=site / /site
RUN echo 'deb [signed-by=/site/fejkdata.asc] file:/site/deb stable main' > /etc/apt/sources.list.d/fejkdata.list \
&& apt-get update -o Dir::Etc::SourceList=/etc/apt/sources.list.d/fejkdata.list -o Dir::Etc::SourceParts=- \
&& apt-get install -y fejkdata \
&& fejkdata --version > /checked
FROM fedora:43@sha256:a651ddf48ea28a06ed4e1e6519f51c9f47e7a5a138722ade87369b8fbb7e5b42 AS check-rpm
COPY --from=site / /site
RUN sed -e 's#^baseurl=.*#baseurl=file:///site/rpm#' -e 's#^gpgkey=.*#gpgkey=file:///site/fejkdata.asc#' \
/site/rpm/fejkdata.repo > /etc/yum.repos.d/fejkdata.repo \
&& dnf install -y --repo fejkdata fejkdata \
&& fejkdata --version > /checked
FROM alpine:3.22.2 AS check-apk
COPY --from=site / /site
RUN cp /site/apk/fejkdata.rsa.pub /etc/apk/keys/ \
&& apk add --no-cache --repositories-file /dev/null -X /site/apk fejkdata \
&& fejkdata --version > /checked
FROM scratch AS checks
COPY --from=check-deb /checked /deb
COPY --from=check-rpm /checked /rpm
COPY --from=check-apk /checked /apk
-12
View File
@@ -23,18 +23,6 @@
"matchStrings": [
"goreleaser/goreleaser-action@\\S+\\s+with:\\s+version: (?<currentValue>\\S+)"
]
},
{
"customType": "regex",
"datasourceTemplate": "pypi",
"depNameTemplate": "cloudsmith-cli",
"description": "the Cloudsmith CLI the Packages workflow installs",
"managerFilePatterns": [
"/^\\.github/workflows/packages\\.yml$/"
],
"matchStrings": [
"cloudsmith-cli==(?<currentValue>\\S+)"
]
}
],
"extends": [