Nothing in the pipeline looked inside an artifact a pin refresh had just
pinned. check-apply-extra.sh — the only check that opens the payload — ran for
apps a PR *adds*, and the auto-update PRs it would have to run in are opened
with the default GITHUB_TOKEN, which does not trigger pr-checks at all. So a
version bump reached users unopened, and #130 is what that costs: upstream
renamed a file inside the .deb and the app became uninstallable.
Both update workflows now unpack the new payload the way an install does before
the pin ships. The daily sweep holds back an app whose artifact no longer
unpacks — it keeps its last installable pin, the PR body says which and why, and
the run goes red for a human — instead of failing the whole batch or shipping
the break. release-dispatch verifies its single app, and skips the download on
the common "nothing moved" path.
pr-checks widens the same check from new apps to any app whose sha256 moved: a
new payload is a payload no unpack script has seen, whoever bumped it. New apps
still qualify, since their pins arrive as added lines.
check-apply-extra.sh grows INSTALL_RUNTIME=1 so a workflow that only checks
payloads can fetch the runtime instead of building the app first.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The checker built its uid-0-no-caps sandbox with `bwrap --unshare-user`, which
distros that set kernel.apparmor_restrict_unprivileged_userns=1 deny outright —
Ubuntu 23.10+ and the GitHub Actions runners among them. There bwrap failed
while setting up the sandbox, before apply_extra ran at all, and every nonzero
exit was reported as "must unpack with --no-same-owner", pointing review at a
bug that wasn't there.
Probe for a route instead of assuming one. Real root via passwordless sudo is
preferred where available: it is what flatpak-system-helper actually does, so a
chown fails with the same EPERM users see, rather than the EINVAL an unmapped
uid produces. A dev box without sudo still falls back to the unprivileged user
namespace, which needs no privilege and is unaffected on hosts without the
AppArmor restriction. If neither route works, say so and name the sysctl.
On the real-root route the work dir is handed to root before the sandbox opens.
uid 0 there has no CAP_DAC_OVERRIDE, so a dir still owned by the invoking user
is neither traversable nor writable — which is why a system-wide install unpacks
into a root-owned /app/extra to begin with.
Also separate the two failure modes: bwrap's own setup errors are reported as a
problem with the checker's environment, and only a genuine apply_extra failure
is attributed to the unpack script.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Nothing in CI reached the code path that broke six apps for every user
who installed system-wide. build-app.sh only builds, and extra-data is
fetched at install time, so a green build says nothing about whether an
app installs. A --user install misses it too: there apply_extra runs as
the invoking user, and the ownership restore that fails under root never
happens.
scripts/check-apply-extra.sh closes that gap. It downloads the pinned
artifact, verifies its sha256, and runs the app's real apply_extra.sh in
a sandbox mirroring what flatpak-system-helper imposes: uid 0, all
capabilities dropped, no network, no /proc, runtime at /usr, extra-data
bound at /app/extra. bwrap --unshare-user gives us that without needing
root: a chown to an unmapped uid fails there just as a dropped CAP_CHOWN
does on the real thing.
pr-checks runs it for apps a PR *adds* -- the moment an unpack script and
an upstream builder's uid first meet review. Scoping it there keeps the
artifact download (hundreds of MB for some apps) off every pin bump and
off refactors like the one that prompted this. The playbook asks
contributors to run it by hand whenever they touch an unpack script or
re-pin to an artifact from a new builder, which is the gap that remains.
tests/test_check_apply_extra.sh is hermetic: it builds a zip carrying an
Info-ZIP 0x7875 field with uid/gid 1001 -- what a release archive built
on CI without fakeroot looks like -- serves it over file://, and asserts
the checker rejects an unpack without --no-same-owner and accepts one
with it. Verified it fails against a mutant checker that ignores the
exit code, so it is testing what it claims to.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>