Releasing¶
How a TrustSight version reaches users, and why the source tarball is built here rather than taken from GitHub.
The source is a release asset, not a generated archive¶
packaging/aur/PKGBUILD fetches a tarball this repository builds and attaches
to the release. It deliberately does not use
https://github.com/.../archive/refs/tags/vX.tar.gz.
Two failures drove that, and both reached users:
The bytes are not ours. GitHub generates the archive tarball on demand and does not guarantee it is stable. The gzip settings behind it have changed before, invalidating recorded checksums across every distribution at once. A package whose integrity check depends on a file somebody else regenerates is not pinned, it is hoping. A release asset is an immutable blob; nothing regenerates it.
The ordering was impossible. The generated archive cannot exist until the
tag does, so its checksum could only be recorded after tagging, by a second
commit. Between those two commits the PKGBUILD named a new pkgver beside the
previous release's checksum. That window opened on every release, and in
v0.13.1 the workflow that closes it failed, so the window never shut and a
user reported the mismatch.
The determinism contract¶
scripts/build_release_tarball.py builds the tarball as a pure function of the
paths and contents git archive selects. Anything that varies between builds
is removed:
- Every member mtime is rewritten to a fixed epoch.
git archiveotherwise stamps members with the commit date, which would move the checksum on every commit. uid,gid,unameandgnameare zeroed, so the builder's account cannot leak into the artifact.- Members are written in sorted order rather than tree order.
- gzip is given
mtime=0and no filename field. The default embeds the current time.
The property that makes a pre-tag checksum possible: packaging/ is
export-ignored by .gitattributes, so writing the checksum into the
PKGBUILD cannot change the tarball that checksum describes. Without that
exclusion the whole thing would be circular. dist/ is gitignored for the
same reason, so a built artifact never lands inside the next one.
One ordering rule¶
The checksum is computed last, after every other change is final.
Anything outside packaging/ is inside the tarball, including
pyproject.toml, the tests and this page. Editing any of it after hashing
invalidates the recorded value. The script therefore archives the working
tree by default rather than HEAD, because the content being released is
what is in the tree now, not what the previous commit held:
python scripts/build_release_tarball.py # working tree
python scripts/build_release_tarball.py --rev v0.13.2
python scripts/build_release_tarball.py --check <sha256>
tests/test_pkgbuild.py::test_recorded_checksum_matches_a_freshly_built_tarball
rebuilds the tarball and compares it against the recorded value, so a stale
checksum cannot be committed at all. This is the check that makes the v0.13.1
report unrepeatable.
The steps¶
The full checklist lives beside the package in
packaging/aur/README.md.
In outline, and note that every step happens before the tag:
- Land all content changes, including
versioninpyproject.toml. python scripts/build_release_tarball.pyand read the checksum.- Record it in
packaging/aur/PKGBUILD, regenerate.SRCINFO. This commit touches onlypackaging/, so it cannot move the checksum from step 2. - Build locally with
makepkg -si. - Push the final commit, then dispatch
Release softwarewith the intendedvX.Y.Ztag and commit. It builds the artifacts, verifies their metadata, test-installs the wheel and sdist, builds the Arch package, creates a draft release, verifies its checksum manifest, then publishes GitHub and PyPI.
Nothing is repaired afterwards. There is no post-tag step that can fail and leave the branch inconsistent, which was the whole defect.
What CI proves¶
publishing.yml is manually dispatched before a software release exists. It
requires the proposed tag to match pyproject.toml, PKGBUILD, .SRCINFO,
wheel metadata, and sdist metadata; runs the complete test suite, twine
check, isolated wheel/sdist smoke installs, and the Arch package check().
Only then does it create a private draft, upload the source archive and
SHA-256 manifest, verify the uploaded bytes, and publish GitHub followed by
PyPI. release-pkgbuild.yml remains a manual, post-publication audit.
pkgbuild.yml runs on every push and pull request. It builds the deterministic
tarball from the checked-out tree, verifies the PKGBUILD checksum against it,
and installs from that artifact with check() enabled. It does not wait for or
download a published release asset; the release workflow performs the
additional artifact checks before publication.
When check() runs from the tarball¶
The suite runs from inside the extracted archive during makepkg, where
packaging/ and .git are both absent. Tests that require either explicitly
skip there rather than fail. The exclusions are narrow: the whole PKGBUILD test
module skips when packaging/aur/PKGBUILD is absent; the checksum-rebuild and
archive-membership tests skip when there is no Git checkout; and the critical
paths gate skips only ARCHIVE_EXCLUDED_PATHS from its existence assertion.
The package check() command also excludes tests/test_fetcher.py and
tests/test_rebaseline.py; those modules require repository/network fixtures
that are not part of the shipped archive test environment.
Two missing exclusions took down the v0.13.1 release:
scripts/critical_paths.pylistsARCHIVE_EXCLUDED_PATHS, the critical pathsexport-ignorelegitimately removes. Thecritical paths are synchronisedgate skips their existence check when it is running from an archive and enforces it everywhere else. Before that, the gate requiredpackaging/aur/PKGBUILDto exist while.gitattributesguaranteed it would not, a contradiction that could never hold inside the tarball.- A test that shells out to
gitmust skip when there is no checkout. Inside amakepkgbuild the tree is owned by a different user than the one building, so git refuses withdetected dubious ownership, which says nothing about what the test was asking.