RPM Auto Update
Nightly reconciler that keeps the rpm/* package repos and the rpm org's
Gitea package registry in sync with their upstream releases.
It mirrors aur/.autoupdate: a new
upstream tag, a spec edited by hand and pushed to origin, and an upload that
failed last night all take the same path. On every pass the version-release in
the spec is compared with what the registry holds, and the registry is brought
in step.
Layout
| file | role |
|---|---|
autoupdate.sh |
entry point: bump, build, upload, push, one package per process |
latest-version.sh |
resolves the latest upstream version (verbatim from aur/.autoupdate) |
common.sh |
logging, guards, version compare, tarball fetch, registry API |
dockerfile |
the Fedora toolchain image the nightly job runs in |
.gitea/workflows/update.yaml |
nightly matrix job |
.gitea/workflows/build-image.yaml |
monthly image rebuild |
Nothing here is package-specific. Everything that differs between packages lives in the package repo itself.
Package repo requirements
rpm/<pkg> carries:
<pkg>.specwith literalVersion:andRelease: N%{?dist}lines and a%changelogsection.Source0should be the Gitea archive of the tag,https://git.bjphoster.com/source/<srcname>/archive/%{version}.tar.gz, unpacked with%autosetup -n <srcname>(the archive's top-level directory is<srcname>/)..autoupdate, the same manifest the AUR packages use:
srchost="gitea" # gitea | github
srcmntr="source" # upstream owner/org
srcname="skeptical-updater" # upstream repo
verstrip="" # tag prefix to strip (tags are bare semver)
gui="false" # ignored here; AUR AppImage extraction only
Start a new package at a placeholder such as 0.0.1: the first nightly run
bumps it to the newest tag.
What a run does
-
Clone
https://git.bjphoster.com/rpm/<pkg>and read.autoupdate. -
Resolve the newest upstream tag (
latest-version.sh: Gitea tags API,sort -V). A failed lookup exits 1; "no new version" is not a failure. -
If the tag is newer than
Version:, rewrite the spec:Version:set to the tag,Release:back to1%{?dist}, and a%changelogentry prepended:* Sat Oct 10 2026 <GIT_NAME> <<GIT_EMAIL>> - X-1 - Updated to XThe rest of the file stays byte-identical, and
rpmspecmust read back exactlyX-1before anything is committed. The commit isUpdated <pkg> to X-1. -
Ask the registry whether it already holds that version-release.
-
If it does not, download
https://git.bjphoster.com/source/<srcname>/archive/<X>.tar.gz, build withrpmbuild -bb --define 'dist %{nil}'as the unprivilegedbuilderuser, runrpmlint(advisory, never fatal) and upload every built.rpmto the registry. -
Push to origin, last. The registry is the distribution channel and goes first, as the AUR does in
aur/.autoupdate: a failed build or upload leaves origin untouched, and the next night recomputes everything.
Building with dist undefined makes the noarch package <pkg>-X-1.noarch.rpm
rather than .fc43.noarch.rpm, so the one file serves every RPM distribution
that points at the registry.
Versions
X-N: everything before the dash is upstream, -N is the packaging revision
(Release:).
- newer upstream tag:
X-1 force_rebuildwith no new tag:X-(N+1), changelog- Rebuilt X- an older tag (a tag deleted upstream) never downgrades the package
- a hand edit that bumps
Release:and is pushed to origin is published on the next run, because the registry lacks that version-release
Environment and inputs
| workflow input | variable | effect |
|---|---|---|
package |
run one matrix entry only (blank = all) | |
force_rebuild |
FORCE_REBUILD=1 |
bump Release even with no new upstream version; replaces a registry copy of that exact version-release if one exists |
force_refresh |
FORCE_REFRESH=1 |
re-download the source and rebuild even when nothing changed (a check; uploads only what is missing) |
no_git_push |
NO_GIT_PUSH=1 |
dry run: build, but neither upload nor push to origin |
NO_UPLOAD=1 / --no-upload |
never contact the registry: no lookup, no upload; builds only when this run bumped the version | |
GIT_HOST |
git host and registry (default git.bjphoster.com) |
|
GIT_NAME, GIT_EMAIL |
%changelog and commit identity (default: git config) |
|
GIT_TOKEN |
registry token (default: the git credential helper, as in CI) | |
WORKDIR |
where to clone and build (default: a temp dir) | |
AUTOUPDATE_TEST_VERSION |
local test mode: use this as the upstream version | |
AUTOUPDATE_TEST_TARBALL |
local test mode: build from this local tarball | |
PKG_REPO_BASE |
clone package repos from here (default https://$GIT_HOST/rpm) |
Either AUTOUPDATE_TEST_* variable forces NO_GIT_PUSH=1 and NO_UPLOAD=1,
so a test run can never publish anything.
Setup
Required on the rpm org (Settings -> Actions -> Secrets / Variables), or on
the .autoupdate repo itself:
| kind | name | value |
|---|---|---|
| secret | GIT_TOKEN |
access token with write:repository (clone and push rpm/*) and write:package (registry upload, image push, image pull) |
| variable | GIT_NAME |
display name for commits and %changelog, e.g. Bryan Joshua Pedini |
| variable | GIT_EMAIL |
email for commits and %changelog |
| variable | REGISTRY_USER |
your Gitea username for the container registry login (not GIT_NAME, which has spaces) |
| variable | GIT_HOST |
optional; defaults to git.bjphoster.com |
Also:
- enable Actions and Packages on
rpm/.autoupdate(repo Settings -> Advanced Settings), and make sure a runner with theubuntu-latestlabel serves therpmorg (an instance-wide runner does); - the
rpmorg's registry (/api/packages/rpm/...) needs no switch of its own: it answers whenever packages are enabled on the instance. Enabling Packages on the package repos only adds the Packages tab and lets you link the registry entry to the repo; - build the image once by hand (Actions -> Build Autoupdater Image -> Run
workflow) before the first nightly run, since
update.yamlpulls it; - list every package in the
matrix.packageof.gitea/workflows/update.yaml. A package repo that is not listed there is never processed.
The token is read back by autoupdate.sh through git credential fill, from the
~/.git-credentials the workflow writes, and handed to curl on stdin; it is
never on a command line or in a log.
Running by hand
Actions -> Update RPM Packages -> Run workflow. Inputs: package (blank for
the whole matrix), force_rebuild, force_refresh, no_git_push.
Installing packages
Fedora, RHEL and derivatives:
# dnf4 (EL 8/9/10; needs dnf-plugins-core)
sudo dnf config-manager --add-repo https://git.bjphoster.com/api/packages/rpm/rpm.repo
# dnf5 (Fedora 41+)
sudo dnf config-manager addrepo --from-repofile=https://git.bjphoster.com/api/packages/rpm/rpm.repo
sudo dnf install skeptical-updater
openSUSE:
sudo zypper addrepo https://git.bjphoster.com/api/packages/rpm/rpm.repo
sudo zypper refresh # accept the repository key when asked
sudo zypper install skeptical-updater
The .repo file enables gpgcheck against
https://git.bjphoster.com/api/packages/rpm/rpm/repository.key; Gitea signs
each package with that key on upload, so nothing is signed here.
Container image
dockerfile builds the environment the nightly job runs in, published to the
Gitea registry as git.bjphoster.com/rpm/autoupdate:latest:
fedora:latest with rpm-build, rpmdevtools, rpmlint, git, curl,
jq, util-linux (runuser), nodejs (actions/checkout is a JavaScript
action) and a builder user.
.gitea/workflows/build-image.yaml rebuilds and overwrites :latest monthly,
on any push touching the dockerfile, and on demand. It needs the same
REGISTRY_USER and GIT_TOKEN.
Testing locally
A dry run against a throwaway copy of the real package repo, with a forced
upstream version and a local tarball built exactly like Gitea's archive, and no
network at all. The test variables imply NO_GIT_PUSH=1 and NO_UPLOAD=1.
docker build -t git.bjphoster.com/rpm/autoupdate:latest .
t="$(mktemp -d)"
git clone --quiet --bare https://git.bjphoster.com/rpm/skeptical-updater "$t/origin/skeptical-updater"
git -C ~/projects/sources/skeptical-updater archive --format=tar \
--prefix=skeptical-updater/ HEAD | gzip -n > "$t/src.tar.gz"
docker run --rm --network none -v "$PWD:/src:ro" -v "$t:/t:ro" \
git.bjphoster.com/rpm/autoupdate:latest \
bash -c 'cp -r /src /w && cp -r /t /tt && chown -R builder:builder /w /tt && cd /w &&
runuser -u builder -- env GIT_NAME="Your Name" GIT_EMAIL=you@example.com \
PKG_REPO_BASE=file:///tt/origin WORKDIR=/tt/work \
AUTOUPDATE_TEST_VERSION=0.1.0 AUTOUPDATE_TEST_TARBALL=/tt/src.tar.gz \
./autoupdate.sh skeptical-updater &&
runuser -u builder -- git -C /tt/work/skeptical-updater show --stat HEAD'
The built package lands in /tt/work/.rpmbuild/skeptical-updater/RPMS/noarch/
inside the container. Lint the scripts with
docker run --rm -v "$PWD:/mnt:ro" -w /mnt koalaman/shellcheck:stable -x *.sh.