Skip to content

fix(release): reproducible packages, a deb that carries its copyright, and an apt source that resolves - #9

Merged
leinardi merged 3 commits into
mainfrom
fix/reproducible-packages
Sep 10, 2026
Merged

leinardi merged 3 commits into
mainfrom
fix/reproducible-packages

Conversation

@leinardi

Copy link
Copy Markdown
Owner

Three defects found by verifying the v0.5.0 release rather than reading the green ticks, plus one disclosure section.

1. The deb and rpm were not reproducible

nfpm stamps the package and its files with the clock unless told otherwise, and mod_timestamp covers only the Go binary. So the two goreleaser runs inside a single release — the check build and the publishing build — produced different package bytes, while the three archives came out identical:

x86_64.rpm   093793c9…  →  c157240a…
aarch64.rpm  331baca6…  →  eef48bea…
amd64.deb    633db06a…  →  3a275f3d…
arm64.deb    1af43504…  →  0ceb7292…

Nothing was published inconsistently: the publishing run rebuilds dist from scratch and checksums what it uploads, and I verified the published checksums.txt against the published archive. But the recovery procedure rebuilds a tagged commit and expects the same artifacts, and docs/release.md claimed that reproducibility for every artifact.

Fixed with nfpms.mtime, plus a file_info.mtime per declared content — a declared entry otherwise keeps the mtime it had on the runner, which is the checkout time. docs/release.md now says which mechanism covers which artifact, and records that v0.5.0 did not have it.

2. The deb shipped without its copyright file

type: license and type: readme are rpm markers (%license, %doc), and nfpm drops an entry carrying one from the deb without reporting it. So monmux_0.5.0_amd64.deb contains /usr/bin/monmux and nothing else — no /usr/share/doc/monmux/copyright, which Debian policy requires:

$ dpkg-deb -c monmux_0.5.0_amd64.deb
root/root ./usr/
root/root ./usr/bin/
root/root ./usr/bin/monmux

Each file is now declared twice, scoped with packager:, so the rpm keeps its markers and the deb gets plain files.

3. The documented apt source answered 404

The README named deb/any-distro any-version, which is where the release workflow pushes but not where anything is served from:

deb/any-distro/dists/any-version/InRelease                   404
deb/any-distro/dists/any-version/main/binary-amd64/Packages  404
deb/ubuntu/dists/resolute/InRelease                          200
deb/ubuntu/dists/noble/InRelease                             200
deb/debian/dists/bookworm/InRelease                          200

Cloudsmith serves a package uploaded to any-distro/any-version through each real distribution index. The source is now deb822 in a .sources file, with $ID and $VERSION_CODENAME from /etc/os-release, an Architectures line naming the only two the repository holds, and an armored .asc key that apt reads directly — no gpg --dearmor step. A note explains that the suite does not affect which bytes arrive, so a derivative whose $ID Cloudsmith does not know can name its upstream. The rpm section was already correct.

4. How this was built

A short README section saying monmux is written largely with agentic AI coding tools under human review, pointing at where that review bites: the catalog evidence rule, and the rule that no agent writes to a monitor at any point.

Monitor writes

None. Nothing in this PR runs monmux, and no step of either workflow touches a monitor. The --unsafe-model surface is unchanged, and no catalog value moved — internal/catalog/models.yaml is not in the diff, so the catalog release-note section of the next release will be empty and the version rule will not raise anything.

Testing

Every claim above was checked, not inferred.

  • Reproducibility: built the same commit twice with the pinned goreleaser v2.18.1 — all four packages now identical SHA-256, where before the fix they differed between runs of the same release.
  • deb contents: dpkg-deb -c lists copyright and README.md stamped 2026-09-10 19:39, the commit's own time, not the build clock. The rpm still carries both files.
  • The cause of 2 was isolated by removing type: and rebuilding, which made both files appear — that is what identified the field as the culprit rather than a guess at it.
  • The apt source was exercised in a throwaway APT root under a scratchpad (Dir=, its own lists, cache and dpkg status), so no system configuration was touched: apt parses the deb822 file, resolves it to deb/ubuntu/dists/resolute, fetches InRelease and both Packages indexes with no signature warning — the index is signed by 7F28A3C96319158609805131D5A5560F8D6A68B5, the fingerprint the README documents — and reports monmux 0.5.0 as the candidate, whose SHA256 3a275f3d… is the published amd64 deb.
  • goreleaser check passes; make check-stage clean.

Note on versioning

All three commits are fix/docs, and nothing here changes a byte monmux can send, so the next release is a patch as far as the catalog rule is concerned.

Two defects found while verifying v0.5.0.

The deb and rpm were not reproducible. nfpm stamps the package and its files
with the clock unless told otherwise, and mod_timestamp only covers the Go
binary, so the two goreleaser runs inside a single release -- the check build
and the publishing build -- produced different package bytes while the three
archives came out identical:

  x86_64.rpm   093793c9...  ->  c157240a...
  aarch64.rpm  331baca6...  ->  eef48bea...
  amd64.deb    633db06a...  ->  3a275f3d...
  arm64.deb    1af43504...  ->  0ceb7292...

Nothing was published inconsistently, because the publishing run rebuilds dist
from scratch and checksums what it uploads, but the recovery procedure rebuilds
a tagged commit and expects the same artifacts, and docs/release.md claimed
that reproducibility for every artifact. nfpms.mtime fixes the package, and a
file_info.mtime per declared content fixes the files inside it: a declared entry
otherwise keeps the mtime it had on the runner, which is the checkout time.

The deb also shipped without its copyright file. `type: license` and
`type: readme` are rpm markers, %license and %doc, and nfpm drops an entry
carrying one from the deb without reporting it, so monmux_0.5.0_amd64.deb
contains /usr/bin/monmux and nothing else -- no /usr/share/doc/monmux/copyright,
which Debian policy requires. Each file is now declared twice, scoped with
`packager:`, so the rpm keeps its markers and the deb gets plain files.

Verified locally with the pinned goreleaser v2.18.1: building the same commit
twice now yields identical SHA-256 for all four packages, the deb lists
copyright and README.md with the commit's timestamp, and the rpm still carries
both.
The documented apt line named `deb/any-distro any-version`, which is where the
release workflow pushes but not where anything is served from. Every path under
it answers 404, so `sudo apt update` failed for anyone who followed the README:

  deb/any-distro/dists/any-version/InRelease                   404
  deb/any-distro/dists/any-version/main/binary-amd64/Packages  404

Cloudsmith serves a package uploaded to any-distro/any-version through each
real distribution index instead, so the working form names one:

  deb/ubuntu/dists/resolute/InRelease                     200
  deb/ubuntu/dists/noble/InRelease                        200
  deb/debian/dists/bookworm/InRelease                     200

The source is now written in deb822 to a `.sources` file, which is what a
current apt prefers and what a modern system's /etc/apt/sources.list.d already
looks like, with $ID and $VERSION_CODENAME read from /etc/os-release and an
Architectures line naming the only two the repository holds. A note says the
suite does not affect which bytes arrive -- the packages are built per
architecture and carry nothing distribution-specific -- so a derivative whose
$ID Cloudsmith does not know can name its upstream, and that the one-line
.list form still works.

The rpm section was already correct: rpm/any-distro/any-version/$basearch
serves repodata/repomd.xml.

Verified against the live repository, in a throwaway APT root so that no system
configuration was touched: apt parses the file, resolves it to
deb/ubuntu/dists/resolute, fetches InRelease and both Packages indexes with no
signature warning -- it is signed by 7F28A3C96319158609805131D5A5560F8D6A68B5,
the fingerprint the README documents -- and reports monmux 0.5.0 as the
candidate, whose SHA256 3a275f3d... is the published amd64 deb.
A reader deciding whether to let this write to their monitor should know how it
was written. The section also points at where the review actually bites: the
catalog evidence rule, and the rule that no agent writes to a monitor at any
point, including during review.
@leinardi
leinardi merged commit 32d880c into main Sep 10, 2026
12 checks passed
@leinardi
leinardi deleted the fix/reproducible-packages branch September 10, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant