Pin CI's qpdf and poppler oracles instead of floating on ubuntu-latest - #381
Merged
Conversation
apt's qpdf on the runner is 11.9.0, two majors behind upstream, and --check's output has changed across qpdf majors while #186's acceptance criterion is exactly that output. Install qpdf 12.4.1 from the official release artifact (checksum-verified) instead; pin poppler-utils, fonts-dejavu-core and fonts-texgyre to the exact versions the runner's apt archive serves today, since an unpinned font update silently changes fixture rendering for every downstream oracle. Also deduplicate the veraPDF Docker tag into one job-level env var, bump zxing-cpp to 3.1.1 with pillow pinned alongside it (the barcode oracle suite passes unchanged), bump setup-dotnet/setup-python to their current majors, and add a step that asserts each pinned tool's own version report so drift fails loudly instead of quietly changing what CI validates against. Closes #230
runs-on moves from the floating ubuntu-latest to ubuntu-24.04, since every apt pin in this job is noble-only and would all break at once the day GitHub retargets the label. The poppler/font =version pins alone were a scheduled outage: noble's -updates/-security pockets keep only the newest revision, so the exact revisions here would 404 the moment Ubuntu ships another one. apt-get now passes -o APT::Snapshot for the date those versions were measured, which rewrites the runner's existing sources to the dated Ubuntu snapshot and keeps the pinned revisions resolvable indefinitely; verified in a container with the live archive already past the pinned revision. setup-python's interpreter is pinned to 3.14 (zxing-cpp 3.1.1 has no wheel past it) instead of floating, and docs-inventory.yml's own setup-python, missed by the previous sweep, moves to v7 to match. The version-assert step now reads every pinned version from job-level env instead of re-hardcoding them, asserts the apt packages' full Ubuntu revision via dpkg-query (pdftotext -v can't see it and fonts can't self-report at all), and compares full lines/values instead of substrings so a future point release can't slip past a careless grep. Restored the zxing-cpp pin's inline rationale in ci.yml, which the previous edit dropped, and updated the CHANGELOG/CONTRIBUTING/CLAUDE.md prose to match the runner label, the snapshot mechanism and the full apt version. tests/VellumPdf.Reader.Tests/Fixtures/Encrypted/README.md's CI/local qpdf version claim was stale in both directions; both are 12.4.1 now. Reran its two qpdf-12.3.2-specific behavioral claims (catalog-in-object-stream under encryption, and /V4 /CFM /V2 ignoring /Length) against 12.4.1 and recorded that both still hold. Refs #230
apt-get update exits 0 even when the -o APT::Snapshot fetch itself fails outright, and silently carries on with whatever else is already cached or configured. Reproduced both failure shapes in a container (a TLS trust failure from missing ca-certificates, and a blackholed host) and confirmed apt gives no other signal in either case, so the poppler/fonts install step now greps /var/lib/apt/lists/ for the snapshot's own index files right after the update and fails loudly if they never landed. The mechanism comment, CONTRIBUTING's paragraph, and the qpdf-1ubuntu9.1 verification claim all overstated or misdescribed how -o APT::Snapshot works: it adds the dated snapshot alongside the runner's existing sources rather than rewriting them, ubuntu-24.04 resolves its own sources through mirror+file:/etc/apt/apt-mirrors.txt rather than a literal archive.ubuntu.com host, and the earlier "verified against the live archive already past this revision" claim was false since 9.9 is still current there today. Re-verified instead against poppler-utils 24.02.0-1ubuntu9.1, a revision already absent from the live archive: a snapshot dated to when 9.1 was current resolves and installs it correctly with the live sources still present. The bump-procedure guidance is corrected too: apt-cache policy cannot produce an APT_SNAPSHOT date on its own, so it now documents picking a UTC day at or after the new revision's publication and confirming the stamp resolves before using it. Also: setup-python's 3.14 pin was justified by a wheel-availability cliff at 3.15 that does not exist (zxing-cpp 3.1.1's wheel is cp312-abi3 with no upper Python bound) — reworded to interpreter determinism, the same reason the pip versions beside it are pinned. Restored a pdftotext -v identity check alongside the dpkg-query asserts, since the package record proves the installed version but not which binary the tests actually invoke. Fixed a stale ci.yml line-number citation in OracleGate.cs, a stale ubuntu-latest reference in an unrelated CHANGELOG entry sitting in the same Unreleased section, and noted the --allow-weak-crypto flag the RC4 recipe in Fixtures/Encrypted/README.md needs against qpdf 12.4.1. aot-smoke's own ubuntu-latest is noted as deliberate, since it carries no apt pins. Merged main (PR #379: global.json pins the SDK band, all setup-dotnet steps read global-json-file) — no conflicts. Refs #230
The real runner failed the assert step at exit 141 (128+SIGPIPE) right
after the dpkg-query lines: `<tool> ... | head -n1` under `set -euo
pipefail` lets head exit the moment it has its one line, and the
producer (writing a multi-line banner) can still be mid-write when that
happens, so the write hits a closed pipe. Local containers never showed
it because the timing has to line up, which the runner's own buffering
apparently does more often than a clean container does.
Fixed by capturing the full output first and taking the first line with
a bash parameter expansion instead of a second process in the pipeline:
`out=$(cmd); line=${out%%$'\n'*}`. Applied to all three version-banner
reads (qpdf, pdftotext, veraPDF). The snapshot-verification guard had
the same shape with `grep -q` instead of `head` (`ls ... | grep -q ...`)
and got the same treatment: capture the listing, then grep it through a
here-string, which reads a fixed string rather than a live pipe and so
cannot race a still-writing producer.
Re-verified in containers: qpdf/poppler assert logic against a real
install (exit 0), the veraPDF line extraction against real multi-line
`verapdf --version` output, and the guard in both its failing and
passing shapes.
Refs #230
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
CI installed qpdf, poppler-utils, fonts-dejavu-core and fonts-texgyre unpinned, so the oracle
suite validated against whatever the runner image happened to ship that day, with no commit to
blame when that changed underneath the repository.
runs-onmoves from the floatingubuntu-latesttoubuntu-24.04. Every apt pinbelow is noble-only, and would all break at once the day GitHub retargets the
ubuntu-latestlabel to a newer release.
aot-smokestays onubuntu-latest, noted as deliberate — it carriesno apt pins, so a label move has nothing there to break.
--check's output has changed across qpdf majors, and Reader: write out a decrypted copy of an encrypted document #186's acceptance criterion is exactlythat output. CI now downloads
qpdf-12.4.1-bin-linux-x86_64.zipfrom the official GitHubrelease, verifies its sha256, extracts it to
/opt/qpdf-12.4.1, and addsbin/toPATHplussets
QPDF_HOME(the env vartests/VellumPdf.TestSupport/ExternalTool.csalready honors).ubuntu-24.04's noble archive serves — measured viaapt-cache policyin anubuntu:24.04container:
poppler-utils=24.02.0-1ubuntu9.9,fonts-dejavu-core=2.37-8,fonts-texgyre=20180621-6. Fonts feed fixture generation, so an unpinned update silentlychanges rendered output for every downstream oracle. The
=versionpins alone are a scheduledoutage, though: noble's
-updates/-securitypockets keep only the newest revision of eachpackage (poppler-utils has already rolled
-1ubuntu9.1through-1ubuntu9.8off the archive),so the exact revisions above would eventually 404 with no code change involved. The install
step now also passes
-o APT::Snapshot=$APT_SNAPSHOTonapt-get update/install, whichadds the dated Ubuntu snapshot service alongside whatever
sources the runner already has (additive, not a replacement —
ubuntu-24.04resolves its ownsources through
mirror+file:/etc/apt/apt-mirrors.txtrather than a literalarchive.ubuntu.comhost, so the mechanism comment does not assume which host actually servesa given fetch). Verified against poppler-utils
24.02.0-1ubuntu9.1, a revision genuinelyabsent from today's live archive: a snapshot dated to when 9.1 was current resolves and
installs it correctly with the live sources still present.
apt-get updatereports success(exit 0) even when the snapshot fetch itself fails outright — reproduced two ways in a
container (a TLS trust failure from missing
ca-certificates, and a blackholed host) — so thestep now also greps
/var/lib/apt/lists/for the snapshot's own index files right after andfails the build if they never landed, rather than silently continuing on whatever else was
already cached or configured. CONTRIBUTING.md documents the bump procedure for a genuine
version bump, including picking an
APT_SNAPSHOTstamp (a UTC day at or after the newrevision's publication) and confirming it resolves before using it.
v1.30.2), but the tag was duplicated across the image pull andthe shim it backs. Both now read one job-level
VERAPDF_TAG.3.0.0→3.1.1, withpillownewly pinned to12.3.0. The 3.0.0 pinwas recorded against an EAN add-on text-format difference; the affected test
(
EanBarcode_Ean13WithAddOn_MainDigitsExact_AddOnTolerant) only ever asserted the main 13digits and treats the add-on's presentation as version-dependent by design, so it passes
unchanged. Ran the full barcode oracle suite locally against 3.1.1 with
REQUIRE_BARCODE_ORACLE=1: 1051/1051 passed, no failures.setup-python's interpreter ispinned to
3.14rather than left floating too, for the same interpreter-determinism reason asthe pip versions beside it — not because of a wheel-availability cliff at 3.15, which doesn't
exist (zxing-cpp 3.1.1's selected wheel is
cp312-abi3,requires_python >=3.9, no upperbound).
actions/setup-dotnet@v5→@v6(acrossci.yml,release.yml,docs.yml) andactions/setup-python@v6/@v5→@v7(ci.yml, anddocs-inventory.yml,which the first pass missed). These ride along in a pinning PR because the whole point of this
issue is deliberate versions instead of environment drift, and a stale action major is the same
kind of drift as a stale apt package — worth clearing out at the same time rather than leaving
for a separate PR to rediscover.
dotnet-version:inputs are untouched, as agreed, to keep thisconflict-free against a separate SDK-pinning PR (merged as ci: pin global.json and CI to the same SDK band #379 while this PR was open —
global.jsonnow pins the SDK band and everysetup-dotnetstep readsglobal-json-file;merged main into this branch, no conflicts,
@v6andglobal-json-fileboth intact on allfive).
env:block instead ofre-hardcoding it, so bumping a pin means editing one line. Checks full-line or full-value
equality rather than substring greps (
"12.4.1"is a substring of"12.4.10"), and asserts theapt packages' complete Ubuntu revision via
dpkg-queryalongside apdftotext -videntitycheck —
dpkg-queryproves the package record (pdftotext -vcan't see the Ubuntu revisionafter the dash, and fonts can't self-report a version at all), but not which binary the tests
actually invoke, which is a real ambiguity on Windows per CLAUDE.md.
CONTRIBUTING.md's prerequisites record the pinned versions, the qpdf install method,and the poppler/font bump procedure;
CLAUDE.md's external-oracles section does the same, so"set
REQUIRE_ORACLES=1to reproduce CI locally" has actual versions to match instead of anaspiration.
CLAUDE.mditself is untracked (.git/info/exclude) — its edit is applied directlyto the maintainer's working copy and can't appear in this PR's diff.
tests/VellumPdf.Reader.Tests/Fixtures/Encrypted/README.mdrecorded CI as apt/11.9.0 and localas 12.3.2; both are now false, so it's updated to 12.4.1 both places. Its two qpdf-12.3.2-specific
behavioral claims (qpdf pulls the catalog out of an object stream the moment the same run also
encrypts; qpdf ignores both
/Lengthentries at/V 4/CFM /V2and always derives a 16-bytekey) were re-run against 12.4.1 and both still hold — recorded inline, along with the
--allow-weak-cryptoflag the RC4 half of the first one needs against 12.4.1 (exit 2 withoutit: "refusing to write a file with weak crypto").
tests/VellumPdf.TestSupport/OracleGate.cscited aci.ymlline range for the Test step's envblock; the range moved once already in this PR, so the comment now names the step instead of a
line number.
Verification
dotnet build VellumPdf.slnx -c Release— 0 warnings, 0 errors.dotnet test VellumPdf.slnx -c Release— all green (oracle tests requiring qpdf/poppler skiplocally as expected without
REQUIRE_ORACLES; ran the barcode suite separately underREQUIRE_BARCODE_ORACLE=1against zxing-cpp 3.1.1, see above).dotnet format VellumPdf.slnx --verify-no-changes— clean.pwsh ./eng/clean-room-check.ps1— clean.pwsh ./eng/aot/run-aot-smoke.ps1— passed.ubuntu:24.04container (download,sha256 check, extract,
PATH,qpdf --versionreports12.4.1) before adding it to theworkflow, and re-verified after adding
--retry/unzip -o.ubuntu:24.04container:the success path (pinned versions install correctly, guard passes silently), an
already-rotated-off revision (9.1) resolving correctly with live sources still present, and
both snapshot-unreachable failure modes (TLS trust failure, blackholed host) — confirmed the
guard fires (exit 1) in both.
gh release view v12.4.1 -R qpdf/qpdf --json assets.Closes #230