4.1 KiB · text History d82bd83

Debian packaging

The forge, as an installable .deb. CI builds it on every push to master and publishes it to this forge’s own apt repository — which is the point: the registry that ships in slice 70, used the way an operator would use it.

control.in       the control stanza; @PLACEHOLDERS@ filled by build-deb.sh
gitgud.service   systemd unit — runs /opt/gitgud/bin/server as `gitgud`
gitgud.env       configuration, installed as a dpkg conffile
postinst         creates the service user + /var/lib/gitgud, enables the unit
prerm/postrm     stop, disable; never deletes repositories or config
build-deb.sh     assembles all of the above around an OTP release

Building one

build-deb.sh takes a directory mix release produced, so the package CI publishes is the package you can build by hand:

MIX_ENV=prod mix release
packaging/deb/build-deb.sh _build/prod/rel/git_gud dist

CI gets that directory out of the release-export stage in the Dockerfile instead of installing a build toolchain on the runner:

docker buildx build --target release-export --output type=local,dest=./release .
packaging/deb/build-deb.sh ./release dist

That stage is FROM scratch, so the export is the release tree and nothing else, and it shares the image build’s cache — the deb job needs: [build] for exactly that reason.

Choices worth knowing

The release bundles ERTS. No erlang dependency, no version skew with the distro’s OTP. The trade is that the package is built for one Debian release — the Dockerfile‘s runner image, trixie — and its Depends are the handful of things the release genuinely can’t carry: git and curl (the Cli git backend and the push hooks shell out to them), openssh-client for ssh-keygen, plus libstdc++6, libncurses6, openssl and ca-certificates. Verified by booting the exported release in a bare debian:trixie-slim container with exactly that list installed and nothing else.

The unit sets LANG=C.UTF-8. systemd starts services with no locale at all, and the BEAM then comes up with native name encoding latin1 — Elixir warns about it on every boot and non-ASCII filenames mangle. The Dockerfile solves this with locales + locale-gen en_US.UTF-8; a package shouldn’t need to generate a locale on the target machine, and C.UTF-8 is built into glibc, so locales isn’t a dependency here.

Installed, enabled, not started. The shipped gitgud.env has CHANGEME in it. Starting against that produces a crash loop in the journal and nothing useful, so the last line of the install is the operator’s: edit the file, run /opt/gitgud/bin/migrate, then systemctl start gitgud.

Purge keeps the data. /var/lib/gitgud holds every repository, package and uploaded blob on the machine, and /etc/gitgud holds the credentials. An accidental dpkg -P should not be the same thing as losing them, so postrm purge says where they are rather than deleting them.

Versions sort as pre-releases. 0.1.0~ci20260912…g<sha>; ~ sorts before 0.1.0, so a future tagged release upgrades over CI builds instead of being shadowed by them. The date is the commit’s, not the clock’s, so rebuilding a commit reproduces its version.

One build, two suites. SUITE=<suite> appends +<suite> to the version, and CI runs the script once per suite (trixie, forky) around a single release build. The two packages are the same bytes: a trixie-linked release runs on forky — glibc is forward-compatible and the libssl / libncurses sonames match — and hexpm/elixir publishes no forky-based builder image to compile a native one against. The suffix is what gives each suite its own pool filename, since a .deb is identified by <name>_<version>_<arch>.deb and one file can only be in one suite’s index. If a forky builder image appears, only the release build changes; the suffix, the dists and the deb lines stay as they are.

test/git_gud_web/gitgud_deb_package_test.exs renders control.in and pushes a package built from it through the forge’s own apt registry, so a broken control stanza fails the suite rather than apt install.