feat(deb): package the server, and ask the questions that fail silently #3

Open
dtourolle wants to merge 1 commits from feat/debian-package into master
Owner

Adds the Debian package (DR-015) and install-time configuration (DR-016), so an operator gets to a running server with apt install rather than by transcribing environment variables out of SPEC.md.

What ships

/usr/bin/jray-server, the systemd unit, the licences, SPEC.md, and an example nginx site under /usr/share/doc/jray-server/examples/. Depends only on debconf and init-system-helpers — no libc, no libsqlite3, no CA bundle, because all three are compiled into the musl binary.

It packages the same binary the musl job already proved static, rather than building its own. Two builds of one commit can diverge, and the point of that assertion is that what an operator installs is what was checked.

Why debconf rather than a README

Two settings fail silently when unset. Without JRAY_TMDB_API_KEY every upload stays pending and is never listed; without JRAY_TRUSTED_PROXIES the X-Forwarded-For header is ignored, so every client shares one rate-limit bucket and every abuse report points at the proxy. Both leave a server that works and is quietly doing the wrong thing.

Three properties, each a way packaging usually goes wrong

  • The generated config is not a dpkg conffile. It is written from the debconf answers, so shipping it as one would make dpkg prompt on every upgrade about changes the package itself made.
  • Hand edits survive. postinst rewrites only the keys debconf manages; comments, ordering and any other setting are untouched.
  • A blank key on reconfigure keeps the existing one. Otherwise pressing Enter through a dpkg-reconfigure would unpublish every future upload.

The seeding guard, which the obvious version gets wrong twice

config seeds unanswered questions from the env file so a reconfigure shows what is actually in force. Seeding unconditionally overwrites a preseed — debconf-set-selections marks what it sets as seen — so every unattended install would quietly reconfigure itself back to whatever was on disk. Guarding on an empty value fails too: server-id and bind carry template Default:s, so db_get returns localhost for a question nobody answered. The correct test is the seen flag.

The lifecycle test caught this. It is the reason the guard exists.

Two deliberate deviations

  • Purge keeps the database. Manifests are real CV compute on media the operator may no longer have, and §8 says federation is explicitly not a backup. Destroying that during an apt purge is not a trade worth making for tidiness; postrm names the path instead.
  • The nginx example is documentation, not installed config. The proxy usually runs on a different host, so a file dropped into this machine's nginx would be in the wrong place.

Verification

A full lifecycle in a bookworm container, run rather than read: build, preseeded install, mode-600 env file, API key absent from debconf's database afterwards, systemd-analyze verify on the unit, the installed binary answering /health and /ready, reconfigure preserving both the key and an unmanaged setting, and purge leaving the database. All twelve steps pass. CI runs the same checks on every build.

One thing I could not verify

The tag-publish step uses the automatic Actions token. If this Gitea build does not grant it package:write, the registry upload returns 401 and needs a PAT in a repository secret. That cannot be tested without pushing a tag, and the step fails loudly rather than silently if so.

🤖 Generated with Claude Code

Adds the Debian package (DR-015) and install-time configuration (DR-016), so an operator gets to a running server with `apt install` rather than by transcribing environment variables out of SPEC.md. ## What ships `/usr/bin/jray-server`, the systemd unit, the licences, SPEC.md, and an example nginx site under `/usr/share/doc/jray-server/examples/`. Depends only on `debconf` and `init-system-helpers` — no libc, no libsqlite3, no CA bundle, because all three are compiled into the musl binary. It packages **the same binary the `musl` job already proved static**, rather than building its own. Two builds of one commit can diverge, and the point of that assertion is that what an operator installs is what was checked. ## Why debconf rather than a README Two settings fail *silently* when unset. Without `JRAY_TMDB_API_KEY` every upload stays `pending` and is never listed; without `JRAY_TRUSTED_PROXIES` the `X-Forwarded-For` header is ignored, so every client shares one rate-limit bucket and every abuse report points at the proxy. Both leave a server that works and is quietly doing the wrong thing. ## Three properties, each a way packaging usually goes wrong - **The generated config is not a dpkg conffile.** It is written from the debconf answers, so shipping it as one would make dpkg prompt on every upgrade about changes the package itself made. - **Hand edits survive.** `postinst` rewrites only the keys debconf manages; comments, ordering and any other setting are untouched. - **A blank key on reconfigure keeps the existing one.** Otherwise pressing Enter through a `dpkg-reconfigure` would unpublish every future upload. ## The seeding guard, which the obvious version gets wrong twice `config` seeds unanswered questions from the env file so a reconfigure shows what is actually in force. Seeding unconditionally overwrites a preseed — `debconf-set-selections` marks what it sets as *seen* — so every unattended install would quietly reconfigure itself back to whatever was on disk. Guarding on an empty value fails too: `server-id` and `bind` carry template `Default:`s, so `db_get` returns `localhost` for a question nobody answered. The correct test is the `seen` flag. **The lifecycle test caught this.** It is the reason the guard exists. ## Two deliberate deviations - **Purge keeps the database.** Manifests are real CV compute on media the operator may no longer have, and §8 says federation is explicitly not a backup. Destroying that during an `apt purge` is not a trade worth making for tidiness; `postrm` names the path instead. - **The nginx example is documentation, not installed config.** The proxy usually runs on a different host, so a file dropped into this machine's nginx would be in the wrong place. ## Verification A full lifecycle in a bookworm container, run rather than read: build, preseeded install, mode-600 env file, API key absent from debconf's database afterwards, `systemd-analyze verify` on the unit, the installed binary answering `/health` and `/ready`, reconfigure preserving both the key and an unmanaged setting, and purge leaving the database. All twelve steps pass. CI runs the same checks on every build. ## One thing I could not verify The tag-publish step uses the automatic Actions token. If this Gitea build does not grant it `package:write`, the registry upload returns 401 and needs a PAT in a repository secret. That cannot be tested without pushing a tag, and the step fails loudly rather than silently if so. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dtourolle added 1 commit 2026-09-06 07:59:13 +00:00
feat(deb): package the server, and ask the questions that fail silently
CI / fmt, clippy, test (pull_request) Successful in 1m48s
CI / advisories and licences (pull_request) Successful in 42s
CI / static musl binary (pull_request) Successful in 1m46s
CI / debian package (pull_request) Failing after 3h10m28s
6c0c80f20b
DR-015, DR-016. §8 already shipped a static binary and an optional
container; this adds the third form, and it packages the SAME binary the
musl job proved static rather than building its own. Two builds of the
same commit could diverge, and the whole point of that assertion is that
the artifact an operator installs is the one that was checked.

Built with dpkg-deb from an explicit staging tree rather than cargo-deb.
debconf's `config` script and `templates` live in the control archive
next to the maintainer scripts, and controlling that archive directly
beats discovering what a wrapper will copy into it. dpkg-dev is on every
Debian builder, so this adds no build dependency.

Why debconf at all: two settings fail SILENTLY when unset. Without
JRAY_TMDB_API_KEY every upload stays `pending` and is never listed;
without JRAY_TRUSTED_PROXIES the X-Forwarded-For header is ignored, so
every client shares one rate-limit bucket and every abuse report points
at the proxy. Both leave a server that works and is quietly doing the
wrong thing — the worst thing to leave to a README nobody reads.

Three properties, each a way packaging usually goes wrong:

The generated config is NOT a dpkg conffile. It is written from the
debconf answers, so shipping it as one would make dpkg prompt on every
upgrade about changes the package itself had made.

Hand edits survive. postinst rewrites only the keys debconf manages;
comments, ordering and any other setting are left alone.

A blank key on reconfigure keeps the existing one. Otherwise pressing
Enter through a dpkg-reconfigure would unpublish every future upload.

The seeding guard is worth its comment, because the obvious version is
wrong twice over. `config` seeds unanswered questions from the env file
so a reconfigure shows what is actually in force. Seeding
unconditionally overwrites a preseed — debconf-set-selections marks what
it sets as seen — so every unattended install would quietly reconfigure
itself back to whatever was on disk. Guarding on an empty value does not
work either: server-id and bind carry template Defaults, so db_get
returns "localhost" for a question nobody answered. The test is the
`seen` flag, which is the actual question being asked.

Purge keeps the database, knowingly departing from the expectation that
purge removes everything. Manifests are the output of real CV compute on
media the operator may no longer have, and §8 says federation is
explicitly not a backup. Destroying that during an `apt purge` is not a
trade worth making for tidiness; postrm names the path instead.

The nginx example is documentation, not installed configuration. The
proxy usually runs on a different host from the server, so a file
dropped into this machine's nginx would be in the wrong place — and §8
leaves the edge to the operator deliberately.

Verified by running it, not by reading it: a full lifecycle in a
bookworm container — build, preseeded install, mode-600 env file, key
absent from debconf's database afterwards, `systemd-analyze verify` on
the unit, the installed binary answering /health and /ready, reconfigure
preserving both the key and an unmanaged setting, and purge leaving the
database. It failed on the seeding bug above the first time, which is
why that guard exists. CI runs the same checks against every build.

TRACES: DR-015, DR-016 | PR-004
Some required checks failed
CI / fmt, clippy, test (pull_request) Successful in 1m48s
CI / advisories and licences (pull_request) Successful in 42s
CI / static musl binary (pull_request) Successful in 1m46s
CI / debian package (pull_request) Failing after 3h10m28s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/debian-package:feat/debian-package
git checkout feat/debian-package
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dtourolle/JRay-public-server#3