Published on [Permalink]
Reading time: 5 minutes

A soft dependency, a broken restore, and a watcher for loose ends

Yesterday ended on a high: two clean servers, two domains, 100% on the XMPP Compliance Suite, real federation. Today was the unglamorous other half — the part where you find out what your package does when the world doesn’t cooperate. I paired with Claude again.

One flaky download shouldn’t sink an install

The ejabberd app installs coturn for audio/video calls. Sensible: without a TURN relay, two people behind NAT can’t reach each other. But I had it as a hard dependency, so if yunohost app install coturn failed for any reason — GitHub having a moment, the app catalog hiccuping, a CI container with no route out — the entire ejabberd install died with it.

That’s the wrong trade. STUN/TURN adds NAT traversal for calls. Everything else — accounts, messages, MUC, file upload, federation — doesn’t need it at all. Losing a perfectly good XMPP server because an optional extra couldn’t be fetched is a bad deal for whoever’s installing it.

So coturn is now a soft dependency: try to install it, and if that fails, warn clearly, carry on, and tell the admin how to add it later. The config template grew a conditional so the STUN/TURN module simply isn’t emitted when there’s no coturn to point at. (An empty module would have made ejabberd refuse to start, which would have been a very silly way to fail.)

The interesting part was measuring it. In a container with no outbound git, the failure took 283 seconds — nearly five minutes — because git has no connection timeout of its own and cheerfully retries into a black hole. Non-fatal but five minutes late is still a bad install. Now a 30-second probe checks reachability first. Install time in that environment went from 14 minutes to 7.

The restore that had never actually been restored

Then the test suite got far enough to try something nobody had tried: backup, remove, restore.

It failed. Not subtly:

sed: can't read /etc/ejabberd/ejabberd.yml: Permission denied
/usr/sbin/ejabberdctl: line 336: cd: /var/lib/ejabberd: Permission denied
ERROR: can not access directory /var/lib/ejabberd

YunoHost’s restore helper hands every restored file back with deliberately restrictive ownership — root, no group access, no traversal. Sane default for an app served by the web server. Fatal for a daemon that runs as its own user: ejabberd couldn’t read its own config, and couldn’t even cd into its own database directory.

My favourite detail: ownership alone wasn’t the fix. A directory left at mode 400 cannot be entered even by the user who owns it — you need the execute bit to traverse a directory. Restore now re-asserts exactly the permissions the Debian package ships, before anything tries to start.

This bug had been sitting there since the first line of the package. It only surfaced because the test suite finally got past the step that used to fail. That’s the whole argument for automated packaging tests in one sentence.

Green, and “Broken”

End result: all four package_check tests pass — linter, install, backup/restore, upgrade. First all-green run.

The summary then cheerfully prints “Global level 0 (Broken)", which is a fun thing to see after a perfect run. Reading the scoring code explains it: level 1 requires zero linter criticals, and the two remaining criticals are “this app is not in YunoHost’s application catalog” and “not flagged as working in the catalog” — which is precisely what my open catalog PR fixes. Chicken, meet egg. The one linter error that was genuinely mine (add_header in the nginx config, which silently stops applying as soon as any nested block sets a header of its own) is fixed.

Upstream, meanwhile

A small win: the Debian 13 keyring permission bug I reported — the one that quietly breaks every app install — got the reply “In any case, a chmod is good practice. Thanks for the report, we’ll implement that." Neither of us could reproduce it in a clean environment, and it’s being hardened anyway. That’s a good maintainer.

Teaser: a watcher for my own loose ends

Which brings me to the thing I actually didn’t plan to build today.

Counting up, I now have eleven open items sitting on other people’s infrastructure: pull requests to YunoHost and Lexicon, bug reports to Debian and ProcessOne, a catalog submission, plus my own tracker. Three different systems, none of which talk to each other. Every morning I was checking them by hand, remembering yesterday’s state in my head, and missing things — a maintainer answered one of them at 05:50 and nothing told me.

I went looking for something that already did this. The closest is bugwarrior, which pulls GitHub, GitLab, Debian’s BTS and more into Taskwarrior — but it has no Forgejo support, and its model is import as tasks, not tell me what changed. gh-dash is GitHub-only. tea is Forgejo-only. bts is Debian-only. Nothing spans the set.

So: a small watcher that reports what moved. A hand-kept list of what to watch, three read-only APIs, and a digest that stays silent unless something actually happened — a new comment (with who said it and what), a merge, a state change. The neat part is where it keeps its memory: instead of a state file next to the data, the snapshot is the state. It’s a recutils database, so recsel queries it, and git diff on it shows exactly what the world did while I wasn’t looking.

It’s built and running against all eleven items. The best moment came from the code review: a reviewer noticed that Debian bug reports arrive as email, email headers can fold across lines, and a folded From: would have written a stray line into that database — quietly making the whole file unreadable to recsel while my own lenient parser shrugged and truncated it. Not a bug that would have shown up today. A bug that would have shown up in three months, on a bug report from someone with a long name.

More on that one soon.

Mastodon