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
+50 -17
View File
@@ -11,7 +11,9 @@ on:
required: true required: true
AUR_KEY: AUR_KEY:
required: true required: true
CLOUDSMITH_API_KEY: PACKAGES_GPG_KEY:
required: true
PACKAGES_RSA_KEY:
required: true required: true
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -34,6 +36,8 @@ jobs:
permissions: permissions:
contents: write contents: write
packages: write packages: write
outputs:
latest: ${{ steps.latest.outputs.latest }}
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with: with:
@@ -54,8 +58,8 @@ jobs:
with: with:
go-version: ${{ steps.go.outputs.version }} go-version: ${{ steps.go.outputs.version }}
cache: false cache: false
# The tap, the bucket, the AUR and the image's latest tag each hold one version, so # The tap, the bucket, the AUR, the image's latest tag and the Linux repositories each
# only the latest release updates them. # hold one version, so only the latest release updates them.
- name: Channels holding one version - name: Channels holding one version
id: latest id: latest
env: env:
@@ -63,9 +67,10 @@ jobs:
TAG: ${{ inputs.tag }} TAG: ${{ inputs.tag }}
run: | run: |
if [ "$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)" = "$TAG" ]; then if [ "$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)" = "$TAG" ]; then
echo "skip=" >> "$GITHUB_OUTPUT" echo "latest=true" >> "$GITHUB_OUTPUT"
else 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 fi
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4 - uses: docker/login-action@v4
@@ -82,20 +87,48 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }} 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 - name: The built binary and the published image print the tag
env: env:
TAG: ${{ inputs.tag }} TAG: ${{ inputs.tag }}
IMAGE: ${{ steps.latest.outputs.skip == '' }}
run: | run: |
test "$(dist/fejkdata_linux_amd64_v1/fejkdata --version)" = "fejkdata $TAG" 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"
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: permissions:
contents: write contents: write
packages: write packages: write
pages: write
id-token: write
uses: ./.github/workflows/packages.yml uses: ./.github/workflows/packages.yml
with: with:
tag: ${{ needs.release.outputs.tag }} tag: ${{ needs.release.outputs.tag }}
secrets: secrets:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }} 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 `}}` - 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 - 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 - 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 # 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 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, Debian, Ubuntu, Fedora, RHEL and Alpine install from fejkdata's signed repositories, which
after one setup script adds it and its signing key: hold the latest release:
```sh ```sh
curl -1sLf https://dl.cloudsmith.io/public/larvit/fejkdata/cfg/setup/bash.deb.sh | sudo -E bash && sudo apt install fejkdata # Debian, Ubuntu
curl -1sLf https://dl.cloudsmith.io/public/larvit/fejkdata/cfg/setup/bash.rpm.sh | sudo -E bash && sudo dnf install fejkdata sudo curl -fsSLo /etc/apt/keyrings/fejkdata.asc https://larvit.github.io/fejkdata/fejkdata.asc
curl -1sLf https://dl.cloudsmith.io/public/larvit/fejkdata/cfg/setup/bash.alpine.sh | sudo -E bash && sudo apk add fejkdata 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 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/ 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 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 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) 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 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]` 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 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 binaries and packages, pushes the image, updates the Homebrew tap, the Scoop bucket and
tap, the Scoop bucket and the AUR. It reads the repository secrets `TAP_GITHUB_TOKEN`, the AUR, and deploys the apt, dnf and apk repositories to GitHub Pages. It reads the
a token that can push to `larvit/homebrew-tap` and `larvit/scoop-bucket`, `AUR_KEY`, repository secrets `TAP_GITHUB_TOKEN`, a token that can push to `larvit/homebrew-tap` and
the AUR account's SSH key, and `CLOUDSMITH_API_KEY`; the image's GHCR package must be `larvit/scoop-bucket`; `AUR_KEY`, the AUR account's SSH key; and `PACKAGES_GPG_KEY` and
public. To package a release again, run the workflow by hand with its tag; it replaces `PACKAGES_RSA_KEY`, the private keys that sign the repositories. The image's GHCR package
what it uploaded before. For a tag older than the latest release it updates only that 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
release's assets and Cloudsmith, since the tap, the bucket, the AUR and the image's what it uploaded before. For a tag older than the latest release it leaves the tap, the
`latest` each hold one version. bucket, the AUR, the image's `latest` and the repositories alone, since each holds one
version.
## License ## 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 `todo.md` item 218 gives those countries streets. Valid until the maintainer accepts ODbL
data, or the country opens a register. 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 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`). vendor-neutral option (`~/.claude/principles/technical-principles.md`). A hosted package
A repository apt, dnf or apk can upgrade from needs signed indexes for three formats. service would sign and index the packages for us, but only saves tooling, so the repositories
Self-hosting them on GitHub Pages would put two signing keys and the tooling that rebuilds are built here: `release-tooling/package-repos.Dockerfile` signs them with a GPG key (apt,
every index into this repository. Cloudsmith signs and indexes all three, for free to open dnf) and an RSA key (apk), and any static host can serve the result. GitHub already hosts the
source, so the tie buys that tooling, not a capability only Cloudsmith has. Leaving it costs code (see "GitHub is canonical"), so Pages adds no new vendor; the image sits on GHCR for the
each Linux user one new setup line. Every release's assets carry the same packages, so same reason, and as OCI it moves by changing its path. A Pages site holds at most 1 GB and a
nothing is lost in a move. GHCR sits beside the code and takes CI's own token; the image is release's packages take about 35 MB, so the repositories hold the latest release only; every
OCI, so leaving it changes only the image path. Valid while Cloudsmith's open-source plan release keeps its own packages, and the image keeps a tag per version. Valid while GitHub
stays free. hosts the code.
+1 -1
View File
@@ -58,7 +58,7 @@ dockers_v2:
- ghcr.io/larvit/fejkdata - ghcr.io/larvit/fejkdata
tags: tags:
- "{{ .Version }}" - "{{ .Version }}"
- latest - '{{ if not (isEnvSet "OLDER_TAG") }}latest{{ end }}'
dockerfile: ../tooling/release-tooling/image.Dockerfile dockerfile: ../tooling/release-tooling/image.Dockerfile
labels: labels:
org.opencontainers.image.source: *homepage 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": [ "matchStrings": [
"goreleaser/goreleaser-action@\\S+\\s+with:\\s+version: (?<currentValue>\\S+)" "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": [ "extends": [