Files
.autoupdate/README.md
T

11 KiB

DEB Auto Update

Nightly reconciler that keeps the deb/* package repos and the deb org's Gitea package registry in sync with their upstream releases.

It mirrors aur/.autoupdate: a new upstream tag, a debian/ edited by hand and pushed to origin, and an upload that failed last night all take the same path. On every pass the version in debian/changelog 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 Debian toolchain image the nightly job runs in
.gitea/workflows/update.yaml nightly job: plan reads UPDATE_PACKAGES, update runs one matrix entry per package
.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

deb/<pkg> carries:

  • debian/ (at the top of the repo): changelog whose top version is <upstream>-<integer revision>, control, rules, source/format, and so on. It is dropped into the unpacked upstream tarball at build time, replacing any debian/ upstream might ship.
  • .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-1: the first nightly run bumps it to the newest tag.

Package list

The nightly job updates the packages named in the Gitea Actions variable UPDATE_PACKAGES; nothing in this repo lists them. Set it on the deb org (Settings -> Actions -> Variables), which is the recommended place; a variable of the same name on deb/.autoupdate itself overrides the org one.

One package per line. A leading - is optional, so YAML-list lines and bare names both work and can be mixed; blank lines and # comments (whole-line or trailing) are ignored, and duplicates are dropped. A name must match ^[A-Za-z0-9][A-Za-z0-9._+-]*$, otherwise the run fails naming it. The current value:

- skeptical-updater

The workflow's plan job turns the variable into the update matrix, one job per package. A manual run's package input (Actions -> Update Debian Packages -> Run workflow) overrides the variable and runs that one package only, listed or not. An unset or empty variable fails the run on purpose: an empty matrix would pass and look exactly like a night with nothing to update.

This needs Gitea 28.0 or newer, the first release that expands a matrix from another job's outputs. Older servers expand it before plan has run, start a single update job with an empty package name, and that job fails.

What a run does

  1. Clone https://git.bjphoster.com/deb/<pkg> and read .autoupdate.

  2. 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.

  3. If the tag is newer than the changelog's upstream version, prepend a stanza in exactly the format dch --newversion X-1 --distribution stable writes:

    <source> (X-1) stable; urgency=medium
    
      * Updated to X
    
     -- <GIT_NAME> <<GIT_EMAIL>>  Sat, 10 Oct 2026 00:00:00 +0000
    
    

    Nothing below it changes, and dpkg-parsechangelog must read back exactly X-1 and parse every stanza without a warning before anything is committed. The commit is Updated <pkg> to X-1.

  4. Ask the registry whether it already holds that version.

  5. If it does not, download https://git.bjphoster.com/source/<srcname>/archive/<X>.tar.gz, place it as <source>_<X>.orig.tar.gz next to the unpacked tree, build with dpkg-buildpackage -us -uc -b as the unprivileged builder user, run lintian (advisory, never fatal) and upload every built .deb to the stable distribution, main component.

  6. 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.

The image is Debian bookworm: a package built against the older dpkg and debhelper installs on bookworm, trixie and Ubuntu alike.

Versions

X-N: everything before the dash is upstream, -N is the Debian revision.

  • newer upstream tag: X-1
  • force_rebuild with no new tag: X-(N+1), changelog * Rebuilt X
  • an older tag (a tag deleted upstream) never downgrades the package
  • a hand edit that adds a changelog stanza and is pushed to origin is published on the next run, because the registry lacks that version
  • an epoch (1:X-N) is carried over unchanged

Environment and inputs

workflow input variable effect
package run only this package instead of UPDATE_PACKAGES (blank = every listed package)
force_rebuild FORCE_REBUILD=1 bump the Debian revision even with no new upstream version; replaces a registry copy of that exact version 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
DEB_DISTRIBUTION registry distribution and changelog distribution (default stable)
DEB_COMPONENT registry component (default main)
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/deb)

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 deb 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 deb/*) and write:package (registry upload, image push, image pull)
variable GIT_NAME display name for commits and the changelog, e.g. Bryan Joshua Pedini
variable GIT_EMAIL email for commits and the changelog
variable REGISTRY_USER your Gitea username for the container registry login (not GIT_NAME, which has spaces)
variable UPDATE_PACKAGES the packages to update, one per line (see Package list); currently skeptical-updater
variable GIT_HOST optional; defaults to git.bjphoster.com

Also:

  • enable Actions and Packages on deb/.autoupdate (repo Settings -> Advanced Settings), and make sure a runner with the ubuntu-latest label serves the deb org (an instance-wide runner does);
  • the deb org's registry (/api/packages/deb/...) 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.yaml pulls it;
  • list every package in UPDATE_PACKAGES (Package list). 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 Debian Packages -> Run workflow. Inputs: package (blank for every package in UPDATE_PACKAGES), force_rebuild, force_refresh, no_git_push.

Installing packages

Debian, Ubuntu and derivatives:

sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL https://git.bjphoster.com/api/packages/deb/debian/repository.key \
  -o /etc/apt/keyrings/git.bjphoster.com-deb.asc
echo "deb [signed-by=/etc/apt/keyrings/git.bjphoster.com-deb.asc] https://git.bjphoster.com/api/packages/deb/debian stable main" \
  | sudo tee /etc/apt/sources.list.d/git.bjphoster.com-deb.list
sudo apt update
sudo apt install skeptical-updater

The bare source line is deb https://git.bjphoster.com/api/packages/deb/debian stable main; Gitea signs the repository's Release files with https://git.bjphoster.com/api/packages/deb/debian/repository.key.

Container image

dockerfile builds the environment the nightly job runs in, published to the Gitea registry as git.bjphoster.com/deb/autoupdate:latest: debian:bookworm-slim with dpkg-dev, debhelper, build-essential, fakeroot, devscripts, lintian, git, curl, jq, ca-certificates, 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/deb/autoupdate:latest .

t="$(mktemp -d)"
git clone --quiet --bare https://git.bjphoster.com/deb/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/deb/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/.debbuild/skeptical-updater/ inside the container. Lint the scripts with docker run --rm -v "$PWD:/mnt:ro" -w /mnt koalaman/shellcheck:stable -x *.sh.