Skip to content

Repository files navigation

Builds some AURs and my libfprint package for my laptop, pushing the diff to xerootg.github.io

Saves me time building AURs every time someone pushes.

xerootg.github.io lists everything in every repo below — versions, sizes, build dates, when each package was first published, and a rolling changelog. It is generated rather than written; see the package index.

Packages are signed. Trust the key once, then add the repo to your pacman.conf:

curl -fsSL https://xerootg.github.io/xerootg.asc | sudo pacman-key --add -
sudo pacman-key --lsign-key "$(curl -fsSL https://xerootg.github.io/xerootg.asc \
  | gpg --show-keys --with-colons | awk -F: '/^fpr:/{print $10; exit}')"
[custom]
Server = https://xerootg.github.io/archlinux
SigLevel = Required DatabaseOptional

--lsign-key is the step people miss: --add imports the key, but pacman still refuses every package until the key is locally signed as trusted. The site carries the same snippet with the fingerprint filled in.

[ghidra] — the oversized-package repo

ghidra-noprompt is ~500 MB, and GitHub refuses any pushed file over 100 MB, so it cannot live in the Pages repo above. It gets its own pacman repo, served directly out of a GitHub Release on this repository:

[ghidra]
Server = https://github.com/xerootg/arch/releases/download/pacman-repo
SigLevel = Required DatabaseOptional

Then sudo pacman -Sy ghidra-noprompt.

That is stock Ghidra from the matching upstream stable release, with one change: opening a program that has never been analyzed no longer prompts "would you like to analyze it now?". The prompt moves to a tool option (Edit → Tool Options → Auto Analysis → Ask To Analyze) that ships disabled, so it can be turned back on without a rebuild. The patch and the build that produced it live in xerootg/ghidra. The package provides/conflicts with ghidra.

.github/workflows/ghidra-noprompt.yml checks that fork daily, repackages any new release, refreshes the ghidra.db in the fixed pacman-repo release, and drops the superseded package asset. The release tag never changes, so the Server URL above is stable.

Its PKGBUILD lives in release-pkgbuilds/, not pkgbuilds/, so the build-pacman-repo pipeline never sees it — that pipeline would otherwise try to commit the package to the Pages repo and break the push for every other package.

The job decides what to build from the release itself, not from the checked-in pkgver, so it stays correct without needing to commit anything. It does try to refresh the PKGBUILD afterwards, but the main ruleset requires a pull request and github-actions[bot] cannot bypass it, so that push is best effort and non-fatal — expect the checked-in recipe to lag a release behind unless the bot is granted a bypass.

[orca] — OrcaSlicer, for the same reason

orca-slicer-git compresses to ~164 MB, so it hits the same 100 MB wall. It gets its own release-hosted repo:

[orca]
Server = https://github.com/xerootg/arch/releases/download/orca-repo
SigLevel = Required DatabaseOptional

Then sudo pacman -Sy orca-slicer-git.

Ships orca-slicer-git and orca-slicer-git-extras (calibration models, daily tips, profile validator). The main package provides/conflicts with orca-slicer.

.github/workflows/orca-slicer.yml computes upstream's pkgver the same way the PKGBUILD does, skips the run if that revision is already published, and otherwise does a full from-source build — around 2h45m — before refreshing orca.db in the fixed orca-repo release and deleting the superseded packages.

Two settings keep that build alive on a 16 GB runner, both in the PKGBUILD: options=('!lto') at pkgbase level, and ninja -j2 for the main build. The pkgbase part matters — an options=() inside a package_*() function only affects packaging, and makepkg resolves lto before build() runs, so a per-package array cannot disable it. Without this the job is OOM-killed (exit 137) while compiling libslic3r's precompiled header, and because that kill takes the whole container down rather than returning a non-zero status, allow-failure does not contain it — it stops every other package in the repo from updating too.

The package index

xerootg.github.io is regenerated by .github/workflows/repo-index.yml and never edited by hand.

Its input is each repo's pacman database — the .db tarball repo-add writes at the end of every build. That file already carries the name, version, description, URL, licence, compressed and installed size, and %BUILDDATE% of every package, and it is the same file pacman downloads, so the page cannot disagree with what a client would actually install. Nothing had to be added to the builders to produce it.

A .db is a snapshot with no history, though, so "when was this added" cannot be recovered from it. That is what the CSVs in the Pages repo are for. They start empty and accumulate:

file contents
data/packages.csv one row per (repo, package): current version, sizes, built_utc, first_seen_utc, last_updated_utc
data/changes.csv append-only log of added / updated / removed, capped at 5000 rows
data/state.json last check time, and a digest of the data

first_seen_utc is written once, the first time a package is observed, and never rewritten. The first run records its packages as seeded rather than added, because their real history predates the tool and claiming otherwise would be a lie the CSV then carries forever. built_utc comes straight from the database and is accurate from day one.

Rendering reads only the CSVs, never the databases. A repo whose .db could not be fetched this run keeps its last known contents on the page instead of silently emptying out, and logs no spurious removals — one failed release download must not rewrite history.

The job runs after each of the three publishers finishes (workflow_run), plus once daily. It commits only when the data actually changed, or when the last commit is over 20 hours old — enough to keep the "last checked" stamp honest without a commit per trigger, in a repo that has already had to be rewritten once for size.

It is a separate workflow rather than a step inside each publisher on purpose: those run on different schedules, take between two minutes and three hours, and two of them never check out the Pages repo. One writer avoids the race.

The site carries a .nojekyll, so Pages serves it statically. There is no build step between publishing a package and it being downloadable, which matters when the site is mostly a few hundred megabytes of .pkg.tar.zst.

Package signing

Every package and database is signed, so clients can run SigLevel = Required DatabaseOptional — the same setting stock /etc/pacman.conf uses — instead of Optional TrustAll. The current fingerprint and a ready-to-paste import command are on xerootg.github.io; the public key is served at /xerootg.asc.

.github/scripts/sign-pacman-repo.sh does all of it, for all three repos. It needs only gpg and coreutils, so the same script runs inside the Arch build containers and directly on the Ubuntu runner for backfills.

Two things drove the design:

build-pacman-repo cannot sign. It shells out to repo-add --quiet --nocolor with no way to pass --sign, and has no GPG support at all. So signing is a separate pass over the repository directory rather than something threaded through the builder.

A pass over the directory is also the only correct approach. Once SigLevel = Required is in play, pacman wants a .sig beside every package, including ones that were not rebuilt this run. Signing only what a build produced would leave the untouched packages permanently unverifiable.

The script is idempotent and rotation-aware: an existing signature is kept only if it still verifies against the key currently loaded, so changing the secret and re-running re-signs whatever no longer checks out.

The stale-signature trap

The database is rewritten on every build. If the signing key were ever missing while signatures were already present, the old .db.sig would no longer match the new .db, and pacman rejects a repo with a bad database signature far more harshly than one with none — every client breaks at once. The script refuses to run in that state rather than publishing it.

Backfilling and rotation

.github/workflows/sign-backfill.yml signs what is already published without rebuilding it. The release-hosted repos only rebuild when upstream moves — ghidra roughly monthly — so without this, turning signing on would leave those packages unverifiable for weeks. It downloads the release assets, signs whatever is missing or stale, uploads the .sig files, and refreshes the release notes. It is also the tool to run after rotating the key.

Release notes now live in .github/release-notes/. They used to be inline heredocs that only ran when a release was first created, which meant the install instructions published on an existing release could never be corrected.

What the site does with it

build-repo-index.py reads the issuer fingerprint out of each <repo>.db.sig with gpg --list-packets. No secret is involved — the issuer is cleartext in the signature packet — and it is the one source that cannot drift, because it is whatever key actually signed the database clients download. A repo is advertised as Required only when its own database signature verifies as present, so the page can never tell someone to enable a setting that would then reject the repo.

Secrets

secret contents
GPG_SIGNING_KEY ASCII-armoured signing subkey, secret half
GPG_PASSPHRASE passphrase for it

Repository-level secrets, so all four workflows can read them. Only the signing subkey is exported — the certifying primary key stays offline, so a compromise of CI costs a subkey that can be revoked without abandoning the identity or asking every user to re-trust a new fingerprint.

About

Private packages for xero

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages