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.