ci: harden the .deb job's apt bootstrap against bullseye mirror-skew 404s - #564
Merged
Conversation
…r-skew 404s Placeholder commit to open the PR early; the real fix follows. Co-Authored-By: Claude <noreply@anthropic.com>
…self-heals The Native install packages workflow's deb job (package.yml, container: debian:11) fails ~8s in on "Get:N ... 404 Not Found" for a DIFFERENT package each run (libexpat1 arm64 in one run, libperl5.32 amd64 in the same run) -- run 33948322114. apt-get update already runs immediately before apt-get install in the same step, ruling out Docker-layer caching. bullseye-security's pool prunes superseded .deb files as point releases land, so the Packages index served by one mirror/edge can briefly disagree with the pool state a different edge serves -- a live probe just returned 200 on both files that 404'd in CI, confirming this is transient skew, not a removed package (and archive.debian.org has no bullseye-security yet, so this is not an EOL/ archival cutover either). Retry the WHOLE update+install sequence as a unit (5 attempts, linear backoff) in every place this exact bootstrap step exists: package.yml's deb job, build-binaries.yml's build-linux job (the actual release-binary builder, same container, same two-line bootstrap -- a local composite action can't run before actions/checkout, so this can't be hoisted into one shared step), and setup-linux-build/action.yml's own toolchain install (same container, same failure class, invoked by both callers). Each retry gets an independent shot at an index+pool pair that agree. Add -o Acquire::Retries=3 --fix-missing as belt-and-braces for genuine transient network blips. Base image kept at debian:11 (bullseye, glibc 2.31): the glibc floor is a deliberate, explicitly-gated pin (dig_ecosystem#1736/#1741) tied 1:1 to this container's own glibc, verified by ldd --version in setup-linux-build. Since bullseye is not actually EOL/archived, there is no forcing reason to raise it, and doing so silently would regress the very floor it exists to hold. CI-infra only -- no Rust source touched, no version bump. Co-Authored-By: Claude <noreply@anthropic.com>
…sks a dep Observed on a real run of the previous commit (dig-node CI 33949346891, job 101261055756, build .deb linux-amd64): attempt 1 hit the 404 on libperl5.32, and --fix-missing let apt UNPACK ca-certificates/curl/git anyway and attempt to configure perl/git/liberror-perl despite the missing dependency -- leaving them half-configured (dpkg: "dependency problems prevent configuration of git: git depends on perl; however: Package perl is not configured yet"). Every later attempt then reported "git is already the newest version" and never touched the network for libperl5.32 again, so attempts 2-5 failed on dependency resolution alone, not on a fresh fetch. The retry loop never got a second real shot. Without --fix-missing, a failed fetch aborts the whole apt-get install before unpacking anything (this is exactly what the ORIGINAL failing run, 33948322114, shows: "Unable to fetch some archives" with zero "Setting up" lines) -- so every retry attempt starts from a genuinely clean container state. -o Acquire::Retries=3 is unaffected by this and stays: it only retries the same HTTP fetch at the transport level and cannot mask a missing package the way --fix-missing does. Corroborating evidence from that same run: the linux-arm64 leg succeeded on attempt 1, cleanly installing libexpat1 -- the exact package that 404'd in the original failing run -- confirming the underlying mirror/pool state is transient and does self-heal. Co-Authored-By: Claude <noreply@anthropic.com>
main advanced to 0.254.79 (#561 merged) while this branch was in flight, and #562 holds 0.254.80 -- taking 0.254.81 per coordinator assignment (only the coordinator can see every sibling's claim). Cargo.lock's dig-node-service entry refreshed to match via `cargo update -p dig-node-service --offline` (one line changed, no other dependency moved). Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
force-pushed
the
ci/apt-bootstrap-retry
branch
from
September 5, 2026 06:29
2e75679 to
3cd37ea
Compare
Defense in depth on top of dropping --fix-missing (previous commit), per review: apt-get update can serve a cached index via a conditional GET (If-Modified-Since/ETag) instead of genuinely re-fetching, so a retry could in principle keep reading the same not-yet-caught-up index -- rm -rf /var/lib/apt/lists/* forces an unconditional refetch on the next attempt. dpkg --configure -a + apt-get -f install clean up any half-configured package regardless of how it got that way, rather than relying solely on --fix-missing staying absent to prevent one. Also corrected the comment to precisely attribute the half-configured-dpkg evidence (CI run 33949346891) to commit 2e75679 -- the version that still carried --fix-missing, one commit before its removal -- rather than implying it was observed on the fixed code. Co-Authored-By: Claude <noreply@anthropic.com>
…oot cause The retry loop and state reset (previous two commits) were never load-bearing for this failure. Root cause, verified directly: deb.debian.org's debian-security POOL is missing objects for this suite that the canonical security.debian.org serves -- same path, same filename, different host: 404 http://deb.debian.org/debian-security/pool/updates/main/p/perl/libperl5.32_5.32.1-4+deb11u5_amd64.deb 200 http://security.debian.org/debian-security/pool/updates/main/p/perl/libperl5.32_5.32.1-4+deb11u5_amd64.deb The index was never stale -- checked directly: Last-Modified: Mon, 31 Aug 2026 19:41:45 GMT Age: 67 X-Cache: HIT Package: libperl5.32 Version: 5.32.1-4+deb11u5 A correctly-current index named this exact file, on a host whose pool does not have it. u5, u6 and u7 all 404 the same way on deb.debian.org -- a persistent gap, not a point-in-time rotation, so no amount of retrying or index-refreshing could ever have fixed it (five independent, verified-fresh re-fetches all returned the identical 404 in the prior commit's run). Fix: sed the security line in /etc/apt/sources.list to security.debian.org before any apt-get call, in all three locations carrying this bootstrap. Verified locally end-to-end (docker run debian:11): without the sed, `apt-get install ca-certificates curl git` 404s on libperl5.32 every time; with it, every package installs and configures cleanly. The retry loop + state reset stay -- cheap and correct for genuine transients -- but are demoted from "the fix" to defense-in-depth. Comments corrected accordingly. NOT addressed here, and deliberately not papered over: bullseye-security's Release file states Valid-Until: Mon, 07 Sep 2026 21:13:04 UTC (two days out). Bullseye is oldoldstable; once that date passes apt rejects the index outright regardless of host. This is the glibc-floor base-image decision from the family's original brief, now with a concrete deadline, and is reported to the coordinator as its own decision rather than resolved in this PR. Co-Authored-By: Claude <noreply@anthropic.com>
…ent cause The security.debian.org host fix (previous commit) is correct and confirmed working: libperl5.32 now fetches successfully where it 404'd on deb.debian.org every time. That was CAUSE 1 (permanent, per-host pool gap), measured repeatedly: libperl5.32 u5 is 6/6 on security.debian.org, 0/6 on deb.debian.org. CAUSE 2 is different: security.debian.org itself can still 404 on an object that demonstrably exists (libexpat1 u7 -- u8 does not exist, u7 is current), because a specific Fastly POP is serving a stale or negative cache entry for it. Measured: libexpat1 u7 is 20/20 from one vantage while CI 404'd the identical URL from POP 151.101.130.132. Add -o Acquire::http::No-Cache=true to the install (alongside the existing Acquire::Retries=3) so a retry forces revalidation instead of being served the same cached response -- rm -rf /var/lib/apt/lists/* already forces a fresh INDEX; this forces fresh PACKAGE fetches, the layer actually failing. Lengthen the backoff from attempt*5 to attempt*10: the previous 5-attempt run completed in about 25 seconds, not long enough for an edge cache to have a real chance to invalidate. Do NOT add --fix-missing (unchanged reasoning, confirmed correct again). Comments rewritten to describe BOTH causes precisely, with their measurements, replacing the earlier (wrong) "pool prunes superseded files" framing that matched neither. Not touched: bullseye-security's Release Valid-Until (2026-09-07) -- tracked as dig-node#565, the coordinator's call, not this PR's. Co-Authored-By: Claude <noreply@anthropic.com>
…shot The real root cause was neither a stale index nor an edge-cache miss: bullseye's live mirror network (both deb.debian.org and security.debian.org) is internally inconsistent as the suite retires -- each host's Packages index names versions its own pool no longer serves, and the mismatch differs per architecture (openssl 1.1.1w-0+deb11u8 404s on arm64 while the identical version installs fine on amd64). No mirror choice and no retry strategy can fix an index that disagrees with its own pool. Fix: stop depending on the live mirror network. Pin all three suite lines (bullseye, bullseye-updates, bullseye-security) to the same immutable snapshot.debian.org timestamp (20260901T000000Z), whose index and pool were captured together and verified end-to-end -- every package that 404'd against the live mirrors returns 200 here, on both archives. Check-Valid-Until is disabled because a pinned snapshot's Release file will always age past its Valid-Until; that is not the live-suite EOL signal dig-node#565 is about. Also corrects this step's comments, which blamed a Fastly edge-cache miss and a deb.debian.org-specific pool gap -- both real symptoms along the way, but neither the underlying cause. Confirmed locally in a fresh debian:11 container (amd64): apt-get install fetches 22 packages / 17.9 MB from snapshot.debian.org and configures cleanly on the first attempt, including openssl 1.1.1w-0+deb11u8, which 404'd against every live mirror tried previously. Co-Authored-By: Claude <noreply@anthropic.com>
…traps build-binaries.yml's build-linux job and setup-linux-build/action.yml's shared toolchain install carried byte-identical copies of the same stale security.debian.org-only fix and its now-corrected comment (both matched the pre-fix package.yml block by hash). Left alone, both would keep failing the same way as bullseye-security retires -- build-binaries.yml in particular is the job that produces the actual shipped release binaries, so it is higher stakes than the .deb packaging job this PR started from. Same fix in both places: pin all three suite lines to the verified snapshot.debian.org 20260901T000000Z snapshot instead of the live mirror network, with Check-Valid-Until disabled for the same pinned-snapshot reason documented in package.yml. Comments are shortened to point at package.yml's full write-up rather than tripling the same essay. Co-Authored-By: Claude <noreply@anthropic.com>
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.
DO NOT MERGE -- gate round in progress
Version: 0.254.81 (main is at 0.254.79 after #561; #562 holds 0.254.80 -- 0.254.81 assigned by
the coordinator, since only they can see every sibling's claim). Rebased onto origin/main cleanly --
no
droppingin the rebase output, no conflicts (this branch never touched Cargo.toml before thebump, so there was nothing to drop).
Cargo.lock'sdig-node-serviceentry refreshed viacargo update -p dig-node-service --offline-- exactly one line changed, no other dependency moved.Fixing the
deb (linux-amd64)/deb (linux-arm64)apt-bootstrap 404s (Native install packagesworkflow). Draft until the two deb legs have run green for real on this branch.
Root cause -- FINAL (superseded two earlier hypotheses; keep this so nobody re-derives them)
deb.debian.org'sdebian-securityPOOL is missing objects for this suite that the canonicalsecurity.debian.orgserves. Same path, same filename, different host, checked directly:The index was never stale -- checked directly, which is what rules out re-fetching (retries,
rm -rf /var/lib/apt/lists/*, a freshapt-get update) as a fix:A correctly-current index named this exact file, on a host whose pool does not have it.
u5,u6and
u7oflibperl5.32all 404 the same way ondeb.debian.org-- a persistent gap, not apoint-in-time rotation. Reproduces specifically on
amd64because GitHub'subuntu-latestpool isrouted (by
deb.debian.org's anycast/CDN) to an edge (146.75.30.132) with this gap; other networkpaths (this session's own sandbox, and evidently every path to
security.debian.org) reach aworking edge -- which is exactly what made this look like transient mirror/CDN skew across two
earlier rounds of hypothesis-and-fix in this PR.
Fix, verified locally end-to-end (
docker run debian:11, not blind):sedthedebian-securityline in/etc/apt/sources.listtosecurity.debian.orgbefore anyapt-getcall. Without it,
apt-get install ca-certificates curl git404s onlibperl5.32every time; withit, every package installs and configures cleanly.
Second finding -- a real deadline, reported as its own decision, NOT resolved here.
bullseye-security'sReleasefile statesValid-Until: Mon, 07 Sep 2026 21:13:04 UTC-- twodays out at the time of this PR. Bullseye is
oldoldstable; once that date passes, apt rejectsthe index outright ("Release file expired") and every leg fails again, for a different reason,
regardless of host. Do not silence this with
Acquire::Check-Valid-Until "false"-- thatsuppresses a real signal that the base image is past end of life. This is exactly the glibc-floor
base-image question from the family's original brief (
debian:11= glibc 2.31, deliberately pinnedper dig_ecosystem #1736/#1741), now with a concrete deadline instead of being theoretical. Raising
the base without a deliberate floor decision would silently regress the floor the pin exists to
protect -- so this is flagged for the coordinator to decide, not changed in this PR. Tracked as
dig-node#565(filed by the coordinator) -- not this PR's to fix.A SECOND, different cause -- found after the host fix landed and was confirmed working
The
security.debian.orgfix is correct and confirmed: on commit8720049b,libperl5.32fetchedsuccessfully (
Get:12 http://security.debian.org/... libperl5.32 ... [4102 kB]) where it 404'd ondeb.debian.orgevery time before. That was Cause 1.But that same run still failed -- on
libexpat1(u7, current and present; u8 does not exist), fromTHREE different Fastly POP IPs across three attempts (
151.101.194.132,151.101.130.132,151.101.2.132), and on a different package each time (libexpat1, thencurl, thenlibexpat1again). Measured independently:libexpat1u7 fromsecurity.debian.orgis 20/20 ->200 from one vantage. The object exists; a specific edge is serving a stale or negative cache
entry for it at the moment CI asks.
This is Cause 2, and it needed a different remedy than Cause 1:
-o Acquire::http::No-Cache=trueadded to the install (forces revalidation so a retry cannot be re-served the same cached response --
rm -rf /var/lib/apt/lists/*already forces a fresh INDEX; this forces fresh PACKAGE fetches, thelayer actually failing), plus a longer backoff (
attempt*10instead ofattempt*5-- the previous5-attempt run completed in ~25s, not long enough for an edge to matter). Comments in all three files
rewritten to describe BOTH causes with their measurements, replacing the earlier "pool prunes
superseded files as point releases land" framing, which matched neither.
Earlier (superseded) root-cause hypotheses -- kept only so these dead ends are not re-walked
.github/workflows/package.yml:88runs thedebjob incontainer: debian:11. Its bootstrap step(
package.yml:101-105) already runsapt-get update -qqimmediately beforeapt-get install ... ca-certificates curl git, in the samerun:block — confirmed from thefailing job log (update at
05:52:31.269Z, install at05:52:32.494Z, ~1.2s apart). So this isnot a Docker-layer-caching issue.
It is a genuine, intermittent inconsistency between the Debian bullseye (
bullseye-security) indexand the pool state served by whichever mirror/edge a given request lands on: a
Packagesindexfetched from one edge can briefly list a
.debthat a different edge/mirror has already prunedafter a point release. Evidence: a rerun 404s on a different package each time (
libexpat1arm64 in run
33948322114,libperl5.32amd64 in the same run) -- exactly the rotating-poolsignature, not a fixed broken URL. A same-request retry of the identical URL does not reliably help
(it can hit the same edge); a live probe just now got
200on both of those exact files fromdeb.debian.org, confirming the origin/mirror network is not actually missing them -- it is apoint-in-time skew, and it self-heals.
Bullseye is not archived/EOL:
archive.debian.orgdoes not yet carrybullseye-security(
404on itsReleasefile), whiledeb.debian.orgserves it fine. So this is ordinaryoldstable-security churn, not the start of an archival cutover.
Retry loop + state reset -- KEPT, but demoted: this is NOT what fixes the failure above
These were built across two earlier (superseded) hypotheses and are kept as genuine
defense-in-depth for real transients -- they are cheap and correct -- but five independent,
verified-fresh re-fetches all returned the identical 404 in the run that tested them alone
(
33949346891's successor runs), which is exactly the tell that nothing about retrying orrefreshing state was ever going to fix a file that is persistently absent from one host's pool.
Wrap the whole
apt-get update && apt-get installsequence in a bounded retry loop (5 attempts,linear backoff) in all three places this exact failure mode exists:
package.yml:101-105-- thedebjob's own bootstrap (the reported failure).build-binaries.yml:184-188-- thebuild-linuxjob's identical bootstrap. This is the jobthat produces the actual shipped release binaries, in the same
container: debian:11, with theliteral same two-line
apt-get update -qq/apt-get install ... ca-certificates curl git. Alocal composite action cannot run before
actions/checkout(
setup-linux-build/action.yml:14-16: "each calling job bootstraps exactly those three packagesfirst"), so this duplication is structural and cannot be hoisted into one shared step -- both
copies need the same fix independently.
setup-linux-build/action.yml:38-48-- the shared composite action's OWN toolchain install(
build-essential pkg-config perl make cmake clang binutils, same container, same failureclass). This one genuinely is centralized already (both callers invoke it), so fixing it here
covers the toolchain-install half for both
debandbuild-linuxin one place.Each retry re-runs
apt-get updateANDapt-get installtogether, so every attempt gets a genuinelyfresh index -- this is NOT "retries alone" against a cached/stale index.
-o Acquire::Retries=3(transport-level HTTP retry) stays as a harmless belt-and-brace.
--fix-missingwas in the first pushed version of this fix and has been REMOVED after a realrun exposed a worse failure mode than the one it was meant to guard: it let
apt-get installproceed past the 404 and unpack/attempt-configure the packages it COULD fetch, leaving
git/perl/liberror-perlhalf-configured (dpkg: "git depends on perl; however: Package perl isnot configured yet"). Every subsequent retry attempt then saw
git/curl/ca-certificates"already the newest version" and never touched the network for the missing file again -- so the
retry loop degraded into 5 identical dependency-resolution failures instead of 5 genuinely fresh
attempts. Without
--fix-missing, a failed fetch aborts the whole transaction before unpackinganything (confirmed against the ORIGINAL failing log, which shows zero "Setting up" lines), so every
retry now starts from a clean container state, which is what the loop needs to work at all.
Additional state reset added (
dpkg --configure -a || true,apt-get -f install -y || true,apt-get clean,rm -rf /var/lib/apt/lists/*before every retry): this DID work as designed --confirmed on the run that tested it, the
Unmet dependenciescascade above was gone and all 5attempts became genuine, independent re-fetches. It just turned out there was nothing wrong to
refetch (see Root Cause above) -- it's still correct to keep for a genuine future transient.
Verification -- pinned to head SHA, per the correction that earlier reports weren't
build-binaries.ymlisworkflow_call-only and never fires standalone, so a normal PR cannotexercise fix #2. Piggybacking the sanctioned no-op path:
release.ymlhas a bareworkflow_dispatch(no inputs) that runsbuild(→build-binaries.yml) but skipspublish/verify/promote(all gatedif: github.ref_type == 'tag'), so dispatching it againstTHIS branch exercises the fixed
build-linuxjob with zero risk of publishing anything.2e756793(--fix-missingpresent) --Native install packages#33949346891:deb (linux-amd64)FAILED after 5 attempts, all on the samelibperl5.32404 from the sameIP (
146.75.30.132) -- the--fix-missingpoisoning.deb (linux-arm64)SUCCEEDED.3cd37ea7(--fix-missingremoved only): amd64's bootstrap step passed --removing
--fix-missingalone was enough for THAT attempt, but did not address the actual100%-reproducible pool gap (see below).
f944ce5a(+ state reset --dpkg --configure -a/apt-get clean/rm -rf /var/lib/apt/lists/*): confirmed theUnmet dependenciescascade is gone and all 5 attemptsbecome genuine re-fetches -- and confirmed they all still 404 identically, which is what proved
the pool gap rather than staleness.
8720049b(thesecurity.debian.orgsed): confirmedlibperl5.32fetchedsuccessfully -- Cause 1 fixed.
deb (linux-amd64)still FAILED, this time onlibexpat1from three different Fastly POPs across three attempts -- Cause 2, described above.
579a6c2d(current HEAD -- +No-Cache+ longer backoff):Native install packagesrun33951066050,Release (stable)canary run33951102579, bothin progress at time of writing. Will report their
deb/build-linuxleg conclusions from THIShead SHA specifically once terminal.
Base image -- kept
debian:11in THIS PR; the decision is flagged, not made heredebian:11(bullseye, glibc 2.31) is a deliberate pin (setup-linux-build/action.yml:1-36,dig_ecosystem #1736/#1741): the container's own glibc IS the shipped floor (asserted by
ldd --versionin that action), chosen to cover Ubuntu 20.04/22.04, Debian 11/12, RHEL 9 andAmazon Linux 2023. Raising the base image would silently raise the floor unless accompanied by a
deliberate floor bump +
SPEC.md§11.3 + docs update.This is no longer purely theoretical.
bullseye-security'sReleasefile carriesValid-Until: Mon, 07 Sep 2026 21:13:04 UTC-- two days out. Once it lapses, apt rejects the indexoutright regardless of host, and every leg fails again for a different reason. This PR does NOT
paper over that with
Acquire::Check-Valid-Until "false"and does NOT change the base image --that call belongs to the coordinator, who can weigh the floor guarantee against the imminent
expiry, and it's reported as its own decision above.
Scope
CI-infra only. No Rust source touched, no version bump (workflow-only change; ask before taking a
version number if the gate demands one).
Release-path trace (requested by the coordinator, not fixed here)
Both the stable and nightly release pipelines already fail closed against a failing
debleg, viamechanisms that predate this PR:
release.yml+verify-release-assets.yml):package.yml's ownpublishjob(
needs: [deb, pkg, msi], the whole matrix must be green) never attaches any.debif either archfails.
verify-release-assets.yml(check-release-assets/action.yml:90-106) requiresdig-node_<ver>_amd64.deband_arm64.debexplicitly -- "stricter than feedsign's own failurecondition" by the action's own comment -- and polls for up to 75 minutes before failing.
promote(the job that flips
releases/latest) needsverify, so an incomplete release never becomeslatest; the previous good release keeps serving installs.nightly-release.yml):nightly-publishneeds[nightly-meta, nightly-build, nightly-packages]with no failure override, so a faileddebleg insidenightly-packagesskipsnightly-publishentirely -- that night's rollingnightlytag simply stays on the previousnight's build (stale, not broken/silent-partial).
nightly-publishalso has its own explicitfail-closed assertion on the three native-package names as belt-and-braces
(
nightly-release.yml~420-445).So the worst-case impact of this bug, as it stands, is a delayed/stalled stable or nightly
release (visible as a real red on
verify/nightly-packages), never a silently incomplete or staleartifact promoted to users. No release-gating change is proposed or needed for that reason.