Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 149 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,27 @@ on:

jobs:
build:
runs-on: ubuntu-latest
# Pinned to the specific label, not the floating `ubuntu-latest`: every apt pin below is
# noble-only, and GitHub moves `ubuntu-latest` to a new release on its own schedule, which
# would break all of them at once with no commit to blame (#230).
runs-on: ubuntu-24.04
env:
# Every pinned oracle version in one place, referenced by both the install steps and the
# version-assert step below, so bumping one means editing one line rather than two.
VERAPDF_TAG: v1.30.2
QPDF_VERSION: '12.4.1'
QPDF_SHA256: 'db9122e88ec00c76ac6a14e09ffb92406db1773d47b968911ff6e69f28c09bf9'
POPPLER_VERSION: '24.02.0-1ubuntu9.9'
FONTS_DEJAVU_VERSION: '2.37-8'
FONTS_TEXGYRE_VERSION: '20180621-6'
# noble's apt pockets keep only the newest revision of each package, so a bare `=version`
# pin eventually 404s the moment Ubuntu ships another revision (poppler-utils alone has
# already rolled 9.1 through 9.8 off the archive). Point apt at the Ubuntu snapshot service
# for the date the versions above were measured, so the exact revision keeps resolving
# after the live archive rotates past it — see the "Install poppler and fonts" step.
APT_SNAPSHOT: '20260830T000000Z'
ZXING_VERSION: '3.1.1'
PILLOW_VERSION: '12.3.0'
steps:
# Full history: the clean-room check scans the branch's commit MESSAGES as well as its files,
# and a merged message cannot be corrected afterwards. The default depth-1 checkout gives it
Expand All @@ -18,7 +38,7 @@ jobs:
with:
fetch-depth: 0

- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

Expand Down Expand Up @@ -47,11 +67,60 @@ jobs:
- name: Build
run: dotnet build VellumPdf.slnx -c Release --no-restore

- name: Install PDF validators
run: sudo apt-get update && sudo apt-get install -y qpdf poppler-utils fonts-dejavu-core fonts-texgyre
- name: Install poppler and fonts (pinned)
# A font or poppler update changes fixture rendering and oracle output with no commit to
# blame (#230), so pin exact versions rather than taking apt's default. The `=version`
# pins alone are not enough: noble's -updates/-security pockets keep only the newest
# revision, so the moment Ubuntu ships another one, these exact revisions 404 out of the
# live archive and every PR goes red with no code change involved. `-o APT::Snapshot`
# adds the dated Ubuntu snapshot alongside whatever apt sources the runner already has
# configured (additive, not a replacement — ubuntu-24.04's own sources resolve through
# `mirror+file:/etc/apt/apt-mirrors.txt`, not a literal archive.ubuntu.com host, so this
# comment does not assume which host actually serves a given fetch). The assertion below
# is what makes the outcome observable instead of assumed: `apt-get update` exits 0 even
# when the snapshot fetch fails outright, so without it a silently-unpinned install would
# pass unnoticed. Verified in a fresh ubuntu:24.04 container against an already-rotated-
# off revision — poppler-utils 24.02.0-1ubuntu9.1, absent from today's live archive — a
# snapshot dated to when 9.1 was current resolved and installed it correctly. If a real
# bump is ever needed instead of just chasing a rotated revision — a new poppler feature,
# a font correction — update POPPLER_VERSION / FONTS_*_VERSION together with a new
# APT_SNAPSHOT: a UTC day at or after the new revision's publication (stamps are midnight
# UTC, so a same-day publication needs the next day's stamp), confirmed with
# `curl -sI https://snapshot.ubuntu.com/ubuntu/<stamp>/dists/noble/InRelease` — a future-
# dated or mistyped stamp silently serves latest instead of erroring. Mention the change
# in CONTRIBUTING.md too.
run: |
set -euo pipefail
sudo apt-get update -o APT::Snapshot="${APT_SNAPSHOT}"
# The step comment above explains why this can't be skipped: a failed snapshot fetch
# does not fail this command on its own.
apt_lists=$(ls /var/lib/apt/lists/)
if ! grep -q "^snapshot\.ubuntu\.com_ubuntu_${APT_SNAPSHOT}_" <<< "$apt_lists"; then
echo "::error::apt fell back to the live archive; snapshot ${APT_SNAPSHOT} was not used"
exit 1
fi
sudo apt-get install -y -o APT::Snapshot="${APT_SNAPSHOT}" \
"poppler-utils=${POPPLER_VERSION}" \
"fonts-dejavu-core=${FONTS_DEJAVU_VERSION}" \
"fonts-texgyre=${FONTS_TEXGYRE_VERSION}"

- name: Install qpdf (pinned)
# apt's qpdf on ubuntu-24.04 is 11.9.0, two majors behind upstream; `--check`'s output
# has changed across qpdf majors and #186's acceptance criterion is exactly that output
# (#230). Installed from the official release artifact instead, matched by checksum.
run: |
set -euo pipefail
asset="qpdf-${QPDF_VERSION}-bin-linux-x86_64.zip"
curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o /tmp/qpdf.zip \
"https://github.com/qpdf/qpdf/releases/download/v${QPDF_VERSION}/${asset}"
echo "${QPDF_SHA256} /tmp/qpdf.zip" | sha256sum -c -
sudo mkdir -p "/opt/qpdf-${QPDF_VERSION}"
sudo unzip -o -q /tmp/qpdf.zip -d "/opt/qpdf-${QPDF_VERSION}"
echo "/opt/qpdf-${QPDF_VERSION}/bin" >> "$GITHUB_PATH"
echo "QPDF_HOME=/opt/qpdf-${QPDF_VERSION}" >> "$GITHUB_ENV"

- name: Pull veraPDF Docker image
run: docker pull verapdf/cli:v1.30.2
run: docker pull verapdf/cli:${{ env.VERAPDF_TAG }}

- name: Install veraPDF shim
run: |
Expand All @@ -61,18 +130,83 @@ jobs:
# /tmp lets the tests' temp-dir PDF paths resolve identically in-container.
sudo tee /usr/local/bin/verapdf > /dev/null << 'SHIM'
#!/bin/sh
exec docker run --rm -v /tmp:/tmp -w /tmp verapdf/cli:v1.30.2 "$@"
exec docker run --rm -v /tmp:/tmp -w /tmp verapdf/cli:${{ env.VERAPDF_TAG }} "$@"
SHIM
sudo chmod +x /usr/local/bin/verapdf

- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: '3.x'
# Pinned for the same reason as the pip versions below: a deterministic interpreter
# for the barcode oracle rather than whatever '3.x' resolves to on a given run.
python-version: '3.14'

- name: Install barcode decode oracle (zxing-cpp)
# Pinned: the EAN add-on text format differs across zxing-cpp versions. setup-python's
# toolcache interpreter avoids the PEP 668 externally-managed block on the system python.
run: python -m pip install zxing-cpp==3.0.0 pillow
# Pinned for CI reproducibility (#230), not because 3.1.1 fails the EAN add-on assertion
# it was originally held against — that test only asserts the main digits and tolerates
# the add-on's presentation varying by version (see CONTRIBUTING.md), and it landed in
# the same commit as this pin, passing under 3.1.1. pillow is pinned alongside it so a
# Pillow release can't shift fixture rendering the same way an unpinned apt font package
# could.
run: python -m pip install "zxing-cpp==${ZXING_VERSION}" "pillow==${PILLOW_VERSION}"

- name: Assert pinned oracle versions
# Prints and checks each pinned tool's own version report against the job-level env vars
# above, so drift between this file and what actually got installed fails loudly instead
# of silently changing what every downstream oracle test validates against (#230).
# Comparisons are exact-line or exact-value, not substring greps: "12.4.1" is a substring
# of "12.4.10", so a careless `grep -qF` would pass against a future revision it was
# never meant to accept.
run: |
set -euo pipefail

qpdf_out=$(qpdf --version)
qpdf_line=${qpdf_out%%$'\n'*}
echo "$qpdf_line"
[ "$qpdf_line" = "qpdf version ${QPDF_VERSION}" ]

# pdftotext -v reports only poppler's upstream version (e.g. "24.02.0"), never the
# Ubuntu package revision after the dash — apt can ship a new revision of the same
# upstream release, and fonts have no version-reporting flag at all. dpkg-query reads
# the installed package record directly, which is the only way to see the full pin —
# but it proves the package record, not the binary the tests actually invoke. CLAUDE.md
# documents pdftotext resolving to two different programs on a developer machine
# depending on which shell launched the test host, so keep the identity check too.
poppler_out=$(pdftotext -v 2>&1)
poppler_line=${poppler_out%%$'\n'*}
echo "$poppler_line"
[ "$poppler_line" = "pdftotext version ${POPPLER_VERSION%%-*}" ]

echo "--- installed package versions ---"
dpkg-query -W -f='${Package} ${Version}\n' poppler-utils fonts-dejavu-core fonts-texgyre
assert_pkg_version() {
local pkg="$1" expected="$2" actual
actual=$(dpkg-query -W -f='${Version}' "$pkg")
if [ "$actual" != "$expected" ]; then
echo "::error::$pkg is $actual, expected $expected"
exit 1
fi
}
assert_pkg_version poppler-utils "$POPPLER_VERSION"
assert_pkg_version fonts-dejavu-core "$FONTS_DEJAVU_VERSION"
assert_pkg_version fonts-texgyre "$FONTS_TEXGYRE_VERSION"

verapdf_out=$(verapdf --version 2>&1)
verapdf_line=${verapdf_out%%$'\n'*}
echo "$verapdf_line"
[ "$verapdf_line" = "veraPDF ${VERAPDF_TAG#v}" ]

python -c "
import importlib.metadata as m
import os
zxing_expected = os.environ['ZXING_VERSION']
pillow_expected = os.environ['PILLOW_VERSION']
zxing_actual = m.version('zxing-cpp')
pillow_actual = m.version('pillow')
print('zxing-cpp', zxing_actual)
print('pillow', pillow_actual)
assert zxing_actual == zxing_expected, zxing_actual
assert pillow_actual == pillow_expected, pillow_actual
"

- name: Test
# REQUIRE_VERAPDF makes the conformance oracle fail (not silently skip) if the verapdf
Expand Down Expand Up @@ -156,6 +290,9 @@ jobs:
run: dotnet pack VellumPdf.slnx -c Release --no-build -o ${{ runner.temp }}/packages

aot-smoke:
# Deliberately left on the floating ubuntu-latest label: this job carries no apt version
# pins, so it has nothing for a label move to break the way the build job's oracle installs
# would (#230).
strategy:
fail-fast: false
matrix:
Expand All @@ -164,7 +301,7 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docs-inventory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: '3.13'
python-version: '3.14'

- name: Inventory is up to date
run: python eng/generate-pdf20-inventory.py --check
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

Expand Down Expand Up @@ -121,7 +121,7 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

Expand Down
33 changes: 32 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **CI's external oracles are now pinned instead of floating on whatever the runner image ships.**
The build job itself moves off the floating `ubuntu-latest` label onto `ubuntu-24.04`, since
every apt pin below is noble-only and would all break at once the day GitHub retargets the
label. qpdf was the worst offender: apt's `qpdf` on the runner is 11.9.0, two majors behind
upstream 12.4.1, and `--check`'s output has changed across qpdf majors while #186's acceptance
criterion is exactly that output. CI now installs qpdf 12.4.1 from the official release artifact
(checksum-verified) instead of apt; `poppler-utils`, `fonts-dejavu-core` and `fonts-texgyre`
stay on apt, pinned to the exact versions `ubuntu-24.04`'s noble archive serves today
(24.02.0-1ubuntu9.9, 2.37-8, 20180621-6 respectively), so a font update can no longer silently
shift fixture rendering out from under every downstream oracle. Since noble's apt pockets keep
only the newest revision of a package, the install step also pins apt to the Ubuntu snapshot
service for the date those versions were measured, so the exact revisions keep resolving after
the live archive rotates past them instead of 404ing with no code change involved — verified
against an already-rotated-off revision, not merely today's current one. Since apt reports
success even when that snapshot fetch itself silently fails, the step also asserts the
snapshot's own index files actually landed, so a silent fallback to the live archive fails the
build instead of quietly un-pinning the install. The veraPDF
Docker tag, already pinned, was duplicated across the image pull and the shim that backs it;
both now read one job-level `VERAPDF_TAG`. zxing-cpp moves from 3.0.0 to 3.1.1 (with `pillow`
newly pinned to 12.3.0) — the barcode oracle suite passes unchanged, including the EAN add-on
case the 3.0.0 pin was recorded against, since that test only asserts the main 13 digits and
treats the add-on's presentation as version-dependent; `setup-python`'s interpreter is pinned
to 3.14 rather than floating too, for the same reproducibility reason as the pip versions
alongside it. A new version-assert step checks each pinned tool's own version report after
install — the apt packages' full Ubuntu revision via `dpkg-query`, since `pdftotext -v` reports
only poppler's upstream version and fonts cannot self-report a version at all, alongside a
`pdftotext -v` identity check proving which binary the tests actually invoke — so drift between
the workflow and what actually landed fails the build instead of changing what CI validates
against with no commit to blame.
`actions/setup-dotnet` and `actions/setup-python` also move to their current majors (v6 and v7)
across all four workflow files. (#230)
- The roadmap now describes the scope past 2.5 as two parallel tracks, Kernel and conformance
alongside Layout, and adds the milestones covering the ISO/TS extension series, embedded files,
graphics, fonts, tagged PDF, PDF/UA-2 and signature verification. The previous table had drifted:
Expand Down Expand Up @@ -255,7 +286,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
`-tsv` flag; the version banner alone does not tell the two apart, so `pdftoppm` needs the same
check via `-png`. veraPDF gets that same `VERAPDF_HOME`-first treatment only on Windows, where it
needs the variable to find its `.bat` launcher at all; on every other platform, including CI's
ubuntu-latest runner, `VERAPDF_HOME` is not read here and veraPDF resolves by bare name, same as
ubuntu-24.04 runner, `VERAPDF_HOME` is not read here and veraPDF resolves by bare name, same as
before this fix. The barcode decode oracle's `python` leg is unchanged too: it has no `*_HOME`
and no identity check of its own, resolving by bare name everywhere, the ambiguity this fix
removes for the other four. A hand-check in the "wrong" shell would never catch the swap, and the
Expand Down
Loading