Skip to content

ci: pin global.json and CI to the same SDK band - #379

Merged
Tim81 merged 2 commits into
mainfrom
ci/sdk-single-source
Aug 31, 2026
Merged

ci: pin global.json and CI to the same SDK band#379
Tim81 merged 2 commits into
mainfrom
ci/sdk-single-source

Conversation

@Tim81

@Tim81 Tim81 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Closes #231.

global.json named 10.0.204 with latestFeature; every actions/setup-dotnet
step asked for 10.0.x. Neither points at a fixed SDK, so each resolves
independently and the two only agreed by accident:

before after
global.json 10.0.204, latestFeature 10.0.400, latestPatch
local dev machine (SDKs 10.0.303 and 10.0.400 installed) resolves 10.0.400 via latestFeature roll-forward resolves 10.0.400, pinned
CI (actions/setup-dotnet, dotnet-version: '10.0.x', five places) resolves whatever band is newest at build time resolves the latest 10.0.4xx via global-json-file: global.json

Directory.Build.props sets TreatWarningsAsErrors and AnalysisLevel latest, so an
analyzer diagnostic new to a later feature band was previously a CI failure with no
local repro on an older SDK. Pinning both to the same version removes that gap.

Changes:

  • global.json: 10.0.400 / latestPatch, with a comment on rollForward
    explaining why it stays even though latestPatch is the host default —
    deleting it would freeze CI to exactly 10.0.400 and drop patch servicing.
  • All five actions/setup-dotnet@v5 steps (ci.yml build and AOT-smoke jobs,
    release.yml library and tool-publish jobs, docs.yml) now pass
    global-json-file: global.json instead of a hardcoded dotnet-version, so the two
    can't drift apart again. Action version tags (@v5) are untouched, matching a
    separate in-flight PR on that axis. ci.yml and release.yml also log the
    resolved SDK (dotnet --version) right after each setup-dotnet step.
  • CONTRIBUTING.md: the prerequisite now names the feature band and points at
    global.json for the exact pin, and a new note requires any global.json
    feature-band bump to be merged and CI-validated before the next release tag,
    since release.yml reads global.json from the tag's tree. The automated
    half of that signal (a dotnet-sdk Dependabot entry) is chore(deps): bump Verify.XunitV3 to 32.0.0, add dependabot #378.
  • CHANGELOG entry under ## [Unreleased] / ### Changed.

Rebuilt and retested the full solution under the resolved 10.0.400 SDK before
opening this: no new analyzer diagnostics surfaced in the 4xx band, so there was
nothing to fix.

Test plan

  • dotnet --version inside the worktree prints 10.0.400
  • dotnet build VellumPdf.slnx — 0 warnings, 0 errors
  • dotnet test VellumPdf.slnx — 5231 passed, 0 failed
  • dotnet format VellumPdf.slnx --verify-no-changes — clean
  • pwsh ./eng/clean-room-check.ps1 — clean
  • pwsh ./eng/aot/run-aot-smoke.ps1 — PASSED

global.json named 10.0.204 with latestFeature, while every
actions/setup-dotnet step asked for 10.0.x and floated to whatever
band was newest at tag time -- 10.0.400 as of this change. With
TreatWarningsAsErrors and AnalysisLevel latest, an analyzer diagnostic
new to a later band fails CI without a local repro on the older SDK.

Point global.json at 10.0.400 with latestPatch, and have all five
setup-dotnet steps resolve global-json-file: global.json instead of
repeating the version inline, so the two can no longer drift apart.

Closes #231
@Tim81 Tim81 added this to the v2.3 — Reader robustness milestone Aug 31, 2026
Security review on #379 flagged that the SDK band change removed the
automatic band uptake the old 10.0.x float gave developers, with no
human-facing note of the new constraint or the release-time risk.

- CONTRIBUTING.md: name the pinned feature band in the prerequisite
  and require any global.json bump to be merged and CI-validated
  before the next release tag, since release.yml reads global.json
  from the tag's tree.
- global.json: comment on rollForward explaining it is load-bearing
  here even though latestPatch is the host default -- without it,
  setup-dotnet would freeze CI to exactly 10.0.400.
- ci.yml, release.yml: log dotnet --version right after each
  setup-dotnet step so the resolved SDK lands in the build log.
Tim81 added a commit that referenced this pull request Aug 31, 2026
The dependency floor on xunit.v3.extensibility.core is [3.2.2, ), an
open lower bound already satisfied by the xunit.v3 3.2.2 pin, so this
is independent of the xunit v4 migration tracked separately (#200).

The only transitives that actually moved are DiffEngine (19.3.3 to
20.0.0, a major) and Microsoft.Bcl.AsyncInterfaces (10.0.10 to
10.0.11); Argon and SimpleInfoName were already in 31.28.0's closure.
DiffEngine 20.0.0 bundles native viewer executables and stamps a
runtimeconfig path by default, so CI now disables it explicitly and
the test projects opt out of the bundled viewer.

Adds .github/dependabot.yml, missing until now: nuget, github-actions,
and dotnet-sdk, weekly. Minor/patch bumps group per ecosystem; majors
stay individual so a snapshot-risk bump like this one is attributable
to its own PR. The nuget group excludes the one shipped runtime
dependency, whose patches change emitted CMS/PAdES bytes, from that
batch. The xunit majors are ignored pending #200's deliberate hold,
and dotnet-sdk gives the global.json feature-band pin from #379 the
automated update signal it was otherwise missing.
@Tim81
Tim81 merged commit 7c218a5 into main Aug 31, 2026
3 checks passed
Tim81 added a commit that referenced this pull request Aug 31, 2026
The dependency floor on xunit.v3.extensibility.core is [3.2.2, ), an
open lower bound already satisfied by the xunit.v3 3.2.2 pin, so this
is independent of the xunit v4 migration tracked separately (#200).

The transitives that actually moved: Verify itself (31.28.0 to
32.0.0, in lockstep with Verify.XunitV3), DiffEngine (19.3.3 to
20.0.0, a major), and Microsoft.Bcl.AsyncInterfaces (10.0.10 to
10.0.11). Argon and SimpleInfoName were already in 31.28.0's closure.

DiffEngine 20.0.0 reads its disable flag lazily instead of capturing
it once at type-init (VerifyTests/DiffEngine#825) and now installs a
bundled viewer as an always-available last-resort tool, so a
detection miss that was a harmless no-op in 19.x could launch a GUI.
CI sets DiffEngine_Disabled explicitly against that, and the test
projects opt out of the bundled viewer, which also keeps a
username-bearing absolute path out of runtimeconfig.json. Neither
setting reaches a developer's own installed diff tool; only
DiffEngine_Disabled does that.

Adds .github/dependabot.yml, missing until now: nuget, github-actions,
and dotnet-sdk, weekly. Minor/patch bumps group per ecosystem; majors
stay individual so a snapshot-risk major like this one is
attributable to its own PR. The nuget group excludes the one shipped
runtime dependency, whose patches change emitted CMS/PAdES bytes,
from that batch, and ignores xunit.v3* and xunit.runner.visualstudio
majors pending #200's hold. dotnet-sdk does not consider rollForward,
so it will propose SDKs outside the pinned 10.0.4xx band by design:
each such PR is the deliberate band-move signal #379's CONTRIBUTING
rule calls for.
Tim81 added a commit that referenced this pull request Aug 31, 2026
The dependency floor on xunit.v3.extensibility.core is [3.2.2, ), an
open lower bound already satisfied by the xunit.v3 3.2.2 pin, so this
is independent of the xunit v4 migration tracked separately (#200).

The transitives that actually moved: Verify itself (31.28.0 to
32.0.0, in lockstep with Verify.XunitV3), DiffEngine (19.3.3 to
20.0.0, a major), and Microsoft.Bcl.AsyncInterfaces (10.0.10 to
10.0.11). Argon and SimpleInfoName were already in 31.28.0's closure.

DiffEngine 20.0.0 reads its disable flag lazily instead of capturing
it once at type-init (VerifyTests/DiffEngine#825) and now installs a
bundled viewer as an always-available last-resort tool, so a
detection miss that was a harmless no-op in 19.x could launch a GUI.
CI sets DiffEngine_Disabled explicitly against that, and the test
projects opt out of the bundled viewer, which also keeps a
username-bearing absolute path out of runtimeconfig.json. Neither
setting reaches a developer's own installed diff tool; only
DiffEngine_Disabled does that.

Adds .github/dependabot.yml, missing until now: nuget, github-actions,
and dotnet-sdk, weekly. Minor/patch bumps group per ecosystem; majors
stay individual so a snapshot-risk major like this one is
attributable to its own PR. The nuget group excludes the one shipped
runtime dependency, whose patches change emitted CMS/PAdES bytes,
from that batch, and ignores xunit.v3* and xunit.runner.visualstudio
majors pending #200's hold. dotnet-sdk does not consider rollForward,
so it will propose SDKs outside the pinned 10.0.4xx band by design:
each such PR is the deliberate band-move signal #379's CONTRIBUTING
rule calls for.
Tim81 added a commit that referenced this pull request Aug 31, 2026
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
Tim81 added a commit that referenced this pull request Aug 31, 2026
#381)

* Pin CI's qpdf and poppler oracles instead of floating on ubuntu-latest

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

* Address review: pin the runner, survive apt's revision rotation

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

* Make the snapshot pin's own success observable, not assumed

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

* Stop piping oracle version banners into an early-exiting head

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
Tim81 added a commit that referenced this pull request Aug 31, 2026
The dependency floor on xunit.v3.extensibility.core is [3.2.2, ), an
open lower bound already satisfied by the xunit.v3 3.2.2 pin, so this
is independent of the xunit v4 migration tracked separately (#200).

The transitives that actually moved: Verify itself (31.28.0 to
32.0.0, in lockstep with Verify.XunitV3), DiffEngine (19.3.3 to
20.0.0, a major), and Microsoft.Bcl.AsyncInterfaces (10.0.10 to
10.0.11). Argon and SimpleInfoName were already in 31.28.0's closure.

DiffEngine 20.0.0 reads its disable flag lazily instead of capturing
it once at type-init (VerifyTests/DiffEngine#825) and now installs a
bundled viewer as an always-available last-resort tool, so a
detection miss that was a harmless no-op in 19.x could launch a GUI.
CI sets DiffEngine_Disabled explicitly against that, and the test
projects opt out of the bundled viewer, which also keeps a
username-bearing absolute path out of runtimeconfig.json. Neither
setting reaches a developer's own installed diff tool; only
DiffEngine_Disabled does that.

Adds .github/dependabot.yml, missing until now: nuget, github-actions,
and dotnet-sdk, weekly. Minor/patch bumps group per ecosystem; majors
stay individual so a snapshot-risk major like this one is
attributable to its own PR. The nuget group excludes the one shipped
runtime dependency, whose patches change emitted CMS/PAdES bytes,
from that batch, and ignores xunit.v3* and xunit.runner.visualstudio
majors pending #200's hold. dotnet-sdk does not consider rollForward,
so it will propose SDKs outside the pinned 10.0.4xx band by design:
each such PR is the deliberate band-move signal #379's CONTRIBUTING
rule calls for.
Tim81 added a commit that referenced this pull request Aug 31, 2026
The dependency floor on xunit.v3.extensibility.core is [3.2.2, ), an
open lower bound already satisfied by the xunit.v3 3.2.2 pin, so this
is independent of the xunit v4 migration tracked separately (#200).

The transitives that actually moved: Verify itself (31.28.0 to
32.0.0, in lockstep with Verify.XunitV3), DiffEngine (19.3.3 to
20.0.0, a major), and Microsoft.Bcl.AsyncInterfaces (10.0.10 to
10.0.11). Argon and SimpleInfoName were already in 31.28.0's closure.

DiffEngine 20.0.0 reads its disable flag lazily instead of capturing
it once at type-init (VerifyTests/DiffEngine#825) and now installs a
bundled viewer as an always-available last-resort tool, so a
detection miss that was a harmless no-op in 19.x could launch a GUI.
CI sets DiffEngine_Disabled explicitly against that, and the test
projects opt out of the bundled viewer, which also keeps a
username-bearing absolute path out of runtimeconfig.json. Neither
setting reaches a developer's own installed diff tool; only
DiffEngine_Disabled does that.

Adds .github/dependabot.yml, missing until now: nuget, github-actions,
and dotnet-sdk, weekly. Minor/patch bumps group per ecosystem; majors
stay individual so a snapshot-risk major like this one is
attributable to its own PR. The nuget group excludes the one shipped
runtime dependency, whose patches change emitted CMS/PAdES bytes,
from that batch, and ignores xunit.v3* and xunit.runner.visualstudio
majors pending #200's hold. dotnet-sdk does not consider rollForward,
so it will propose SDKs outside the pinned 10.0.4xx band by design:
each such PR is the deliberate band-move signal #379's CONTRIBUTING
rule calls for.
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.

CI compiles on a different SDK feature band than local, with warnings-as-errors

1 participant