Repository navigation
repository integrity
ci/check-repo.py runs on every pull request to noetl/apt and reads the
published tree in the working copy — pool/main/*.deb,
dists/*/main/binary-*/Packages{,.gz}, and dists/*/Release. It is the only
thing standing between a bad index and an apt-get failure on a user's machine,
because the repository is served as static files and nothing validates it at
request time.
| check | fails when | |
|---|---|---|
| 1 | checksum agreement | a .deb's SHA256 differs from its Packages stanza |
| 2 | .gz currency |
Packages.gz does not decompress to the committed Packages
|
| 3 | Release checksums |
a Release entry's size or hash does not match the file |
| 4 | index completeness | a .deb in pool/ appears in no index |
| 5 | architecture coverage | an architecture present in pool/ is indexed nowhere |
| 6 | architecture leakage | a binary-<arch> index lists a stanza whose Architecture: is neither <arch> nor all
|
| 7 | Release identity |
Origin, Label, Suite, Codename, Architectures or Components is missing |
| 8 | no self-entry | a Release lists Release in its own checksum table |
Everything fails the build. Checks 4–8 were reported rather than failed from the gate's introduction until 2026-10-04, because the release pipeline could not satisfy them; see Why checks 4–8 were advisory first.
Check 7 does not require a Version field. It is optional in a Debian Release
file and apt-ftparchive does not emit one here. Suite and Codename are the
fields apt actually matches a sources.list entry against.
Requiring a field that correct output lacks is how a gate earns a permanent
|| true.
── index completeness
.debs in pool: 26 listed in an index: 26 orphans: 0 of 26
architectures in pool: [amd64, arm64] indexed: [amd64, arm64]
0 failures is also what a check that examined nothing looks like, so each
section prints the population it measured and fails below a floor. This is not
hypothetical for this repository: a check run over an empty pool/ otherwise
reports exactly the same clean result as a healthy repository.
The gate landed with real defects it could not demand be fixed:
-
22 of 23
.debs were in no index.dpkg-scanpackagesran without--multiversion, so only the newest version of each package was indexed andapt-get install noetl=<older>could not resolve. -
arm64 was in
pool/but indexed nowhere. The index path was hardcoded tobinary-amd64. -
Releasecarried onlyDate, and listed itself at 38 bytes against a real 1792 —apt-ftparchive release D > D/Releasescans the directory it is writing into, so the redirect creates the file before the scan reads it.
The blocker was a product question rather than a technical one: the newest arm64
.deb was 2.8.7 while amd64 was at 5.0.1, so simply indexing what was
there would have advertised arm64 as supported at a version years behind.
noetl/cli#93 resolved it by building
a current arm64 .deb every release — build-deb is a two-entry matrix over
(ubuntu-latest, amd64) and (ubuntu-24.04-arm, arm64), not a cross-compile.
Measured on the live published repository afterwards:
.deb in no index 22 of 23 -> 0 of 26
architectures indexed [amd64] -> [amd64, arm64]
Release header fields 1 (Date) -> 8
Release self-entry present -> absent, both suites
newest arm64 2.8.7 -> 5.0.3
Nothing was removed. The older arm64 files remain installable as the genuinely
older releases they are, which is what --multiversion exists for.
Without --arch, dpkg-scanpackages writes every .deb it finds into
whichever index it is told to produce — the Architecture: field is ignored.
That was harmless while only amd64 shipped, and it would have started serving
arm64 binaries to amd64 clients the moment arm64 existed. It was added as a check
in the same change that promoted the others.
In noetl/cli's release.yml, after the matrix
.debs are downloaded into pool/main/:
ARCHES="$(find pool/main -maxdepth 1 -name '*.deb' \
| sed -nE 's/.*_([a-z0-9]+)\.deb$/\1/p' | sort -u | tr '\n' ' ')"
for ARCH in ${ARCHES}; do
DEST="dists/${DIST}/main/binary-${ARCH}"
dpkg-scanpackages --multiversion --arch "${ARCH}" pool/main /dev/null > "${DEST}/Packages"
gzip -9c "${DEST}/Packages" > "${DEST}/Packages.gz"
done
rm -f "dists/${DIST}/Release"
apt-ftparchive -o APT::FTPArchive::Release::Origin=NoETL ... \
release "dists/${DIST}" > "${RUNNER_TEMP}/Release.${DIST}"
mv "${RUNNER_TEMP}/Release.${DIST}" "dists/${DIST}/Release"Three details carry the fixes, and each is easy to undo by accident:
-
--multiversionkeeps every version in the index, not just the newest. -
--arch "${ARCH}"is what makes check 6 pass. -
writing
Releaseto$RUNNER_TEMPandmving it in is what keepsReleaseout of its own checksum table. A redirect straight into the directory reintroduces the self-entry.
sed -nE … p rather than sed -E s/// matters too: the substitution form passes
non-matching filenames through unchanged, which turns a stray file in pool/ into
an "architecture" named after its own path.
Each check is injected one at a time against the real published tree before being trusted:
drop all but one stanza from an index -> exit 1
put an arm64 stanza in the amd64 index -> exit 1
remove Codename from Release -> exit 1
re-introduce the Release self-entry -> exit 1
all restored -> exit 0
A check that has never produced a failure is indistinguishable from a check that cannot produce one. Add the positive control in the same change as the check.
-
noetl/clirelease pipeline — where the indexes are built. - ai-meta: Continuous Integration — fleet-wide gate status.
- noetl/ai-meta#390 — the issue these checks came from.