ci: bound every job and harden the unixODBC install - #360
Conversation
The workflow declared no timeout anywhere, so a job that hangs pins the run for GitHub's 6 hour default. A hung `apt-get` on the two Snowflake lint legs did exactly that: `test` needs `lint`, and `coverage` needs `test`, so one stuck leg of the matrix froze the whole pipeline and no test ever ran. Lint, test and coverage now carry a timeout, and the apt step that hangs carries its own, tighter one, plus retries on the mirror and a noninteractive frontend. A failing leg is now visible in minutes and can be re-run. Deploy is left unbounded on purpose: cutting semantic-release off in the middle of pushing thirteen gems is worse than waiting for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The timeout added in the previous commit bounded the damage but not the cause, and this workflow proved it on its own run: the 4.0 Snowflake leg died on the timeout while the 3.4 leg passed on the same commit. The log shows `apt-get update` looping on `Ign: azure.archive.ubuntu.com`, 72 seconds before the first one, while the Microsoft and Google https repos answer in milliseconds in the same job. Each acquisition is now bounded at 15s, and a failed attempt is retried against the canonical archive instead of being given up on. The step timeout goes to 8 minutes so the fallback has room to run. The package cannot simply be dropped, as the previous commit wondered: the leg that passed reports `4 newly installed` and unpacks unixodbc-dev, so it is genuinely absent from the runner image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous attempt trusted `Acquire::http::Timeout` to bound the step. It does not: it caps a single connection, not the walk over two dozen index files, and with retries on top the first `apt-get update` spent the whole 8 minute budget looping on `Ign: azure.archive.ubuntu.com`. The step timed out inside that first attempt, so the mirror fallback was never reached -- its message appears nowhere in the log. The bound now comes from `timeout 90` around each update, which returns whatever apt does. Refreshing the lists is also demoted to best effort, since the image already carries usable ones and only the install has to succeed; a failed install is what triggers the fallback to the canonical archive. Checked under `bash -e`, the shell the runner uses, over the four paths: healthy mirror, update timing out with the image lists sufficing, fallback succeeding, and everything failing -- which exits non-zero with apt's own status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on 🛟 Help
|
bexchauveto
left a comment
There was a problem hiding this comment.
Reviewed the workflow change. Three findings, all duplicated verbatim in the test job (L116-139). Verified against the raw log of this PR's own green run (32234283306, job Lint (4.0, forest_admin_datasource_snowflake)).
Also checked and found clean: the bash -e {0} control flow (A && exit 0 failing does not trip errexit, so the fallback is reachable), the sudo timeout ordering (timeout runs as root, SIGTERM reaches apt), || true on the multi-file sed, --no-install-recommends (same 4 packages installed, Snowflake tests pass), and the job budgets (measured jobs run 1-4 min, coverage ~8 s).
| } | ||
|
|
||
| apt_update | ||
| sudo apt-get install -y --no-install-recommends unixodbc-dev && exit 0 |
There was a problem hiding this comment.
The install is unbounded, so the stall this PR fixes still eats the whole step budget and the fallback is never reached.
timeout 90 and Acquire::Retries=1 -o Acquire::http::Timeout=10 are applied to apt-get update only. The install fetches the four .debs from the same mirror, and this PR's own green log stalls exactly there:
08:48:34 Need to get 306 kB
Ign: ... azure.archive.ubuntu.com (x N, 38s)
Fetched 306 kB in 37s (8164 B/s)
That run got lucky - apt's own mirrorlist failover kicked in. When the mirror stalls harder, the install has no bound of its own, burns the remaining ~6.5 min, and GitHub kills the step at timeout-minutes: 8. That fails the job inside the first install, which is precisely the shape the PR body says the earlier version got wrong, so echo 'Install failed; falling back...' never runs.
| sudo apt-get install -y --no-install-recommends unixodbc-dev && exit 0 | |
| sudo timeout 240 apt-get install -o Acquire::Retries=1 -o Acquire::http::Timeout=10 -y --no-install-recommends unixodbc-dev && exit 0 |
There was a problem hiding this comment.
Confirmed and fixed in d5ed2f2. Your log reading holds: the Ign: lines at 08:49:05 sit after Need to get 306 kB, so they are inside the .deb fetch, with ~6.5 min of step budget still unspent — the install could indeed have been killed inside its first attempt, which is the exact shape the body claimed to have fixed.
The install now carries its own bound with the same acquisition options you suggested:
sudo DEBIAN_FRONTEND=noninteractive timeout -k 30 240 apt-get install -y \
--no-install-recommends -o Acquire::Retries=1 -o Acquire::http::Timeout=10 unixodbc-dev
One addition beyond the suggestion, from a later pass: -k 30 on both bounds. timeout sends SIGTERM, which apt and dpkg may defer during a transaction, so without a kill-after the bounds would have stayed decorative and only the 8-minute step cap would have stopped anything. Worst case is now 6.5 min, still under it.
|
|
||
| echo 'Install failed; falling back to archive.ubuntu.com' | ||
| sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' \ | ||
| /etc/apt/apt-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources || true |
There was a problem hiding this comment.
The fallback duplicates failover apt already does, half its sed target list is a no-op, and it can leave things strictly worse.
/etc/apt/sources.list.d/ubuntu.sources on the runner image contains URIs: mirror+file:/etc/apt/apt-mirrors.txt, not the azure host - the substitution never matches there. And /etc/apt/apt-mirrors.txt already lists https://archive.ubuntu.com/ubuntu as a fallback entry; in the same log apt switched to it unaided:
Get:2 https://archive.ubuntu.com/ubuntu noble-updates/main amd64 libodbccr2 ...
So the rewrite mostly just duplicates an entry in the mirrorlist. The downside is real though: the image's indices in /var/lib/apt/lists are keyed to azure.archive.ubuntu.com and match no configured source after the sed, while the following apt_update swallows its own failure by design and 90 s is tight for a full noble/updates/security/backports index fetch from the out-of-datacenter global mirror. Scenario: azure indices unreachable (as observed) + first install fails fast for an unrelated reason (a dpkg lock held by unattended-upgrades is the usual one on hosted runners) -> sed -> second update times out -> final install dies with E: Unable to locate package unixodbc-dev, pointing at the wrong cause.
Suggest dropping the fallback branch entirely and putting the effort into bounding the install. If you keep it, make the second apt_update fatal - unlike the first, its success is a precondition for the retry - or give it a much larger timeout.
There was a problem hiding this comment.
Took the primary recommendation: the fallback branch is gone (d5ed2f2).
Your central claim is visible in the same log, which settles it — apt failed over on its own, no sed involved:
08:47:13 Get:3 https://archive.ubuntu.com/ubuntu noble-updates InRelease [126 kB]
So the alternative you offered (making the second apt_update fatal) is moot, and the desynchronised /var/lib/apt/lists risk goes with the branch. I could not inspect the runner filesystem to confirm ubuntu.sources carries mirror+file:, but Get:1 file:/etc/apt/apt-mirrors.txt Mirrorlist in the same log corroborates the indirection.
One residual I have chosen to accept rather than engineer away, now stated in the PR body: the update failure is still swallowed, so the install can run against the image's lists and fail with 404 Not Found if the version they name has been superseded in the archive. Guarding it would mean restoring the conditional chain this change just removed, and apt's own message names the cure.
| run: sudo apt-get update && sudo apt-get install -y unixodbc-dev | ||
| timeout-minutes: 8 | ||
| env: | ||
| DEBIAN_FRONTEND: noninteractive |
There was a problem hiding this comment.
DEBIAN_FRONTEND: noninteractive does not reach apt-get.
It is exported into the step's shell, but every apt invocation goes through sudo, and sudo's default env_reset drops non-env_keep variables. If a package ever did open a debconf prompt, the step would block until the 8-minute cap rather than being protected as intended.
Pass it through the boundary that matters: sudo DEBIAN_FRONTEND=noninteractive apt-get ..., or sudo -E.
There was a problem hiding this comment.
Correct, and fixed in d5ed2f2. The step-level env: is dropped, since it protected nothing, and the variable now crosses the boundary that matters:
sudo DEBIAN_FRONTEND=noninteractive timeout -k 30 240 apt-get install ...
I used the inline assignment rather than sudo -E to keep it to the single variable that needs to survive. The green run on e0c035c confirms sudoers accepts it: command-line env assignments are normally filtered, but the runner's rule matches ALL, which is the documented exception that permits them.
Three gaps from review, all visible in this PR's own green run. The install was unbounded: `timeout 90` wrapped the index refresh only, while the four .deb files come from the same mirror. The log stalls exactly there, `Ign:` lines at 08:49:05 sitting after `Need to get 306 kB`, with ~6.5 min of the step budget still to burn. A harder stall would have been killed inside that first install, which is the shape the previous commit claimed to have fixed. The mirror fallback duplicated apt's own: the image mirrorlist already carries archive.ubuntu.com, and the same log shows apt switching to it unaided at 08:47:13. Rewriting the host also desynchronises the indices under /var/lib/apt/lists, so the retry could die on `Unable to locate package` and name the wrong cause. It is gone; apt does this better. DEBIAN_FRONTEND never reached apt, sudo's env_reset dropping it on the way. It is now set across that boundary. Both commands are bounded, worst case 5.5 min under the 8 minute cap, and the step is down to two lines with no control flow to reason about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`timeout` sends SIGTERM, which apt and dpkg may defer during a transaction. Without a kill-after the two bounds are decorative and only the 8 minute step cap stops anything. Worst case is now 6.5 min, still under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block restated the step name and the echo string sitting two lines below it. Two facts are left, neither readable from the code: the mirror the image points at stalls, and apt's Acquire timeouts bound a single connection rather than a whole fetch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🎉 This PR is included in version 1.39.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
* ci: bound every job and harden the unixODBC install The workflow declared no timeout anywhere, so a job that hangs pins the run for GitHub's 6 hour default. A hung `apt-get` on the two Snowflake lint legs did exactly that: `test` needs `lint`, and `coverage` needs `test`, so one stuck leg of the matrix froze the whole pipeline and no test ever ran. Lint, test and coverage now carry a timeout, and the apt step that hangs carries its own, tighter one, plus retries on the mirror and a noninteractive frontend. A failing leg is now visible in minutes and can be re-run. Deploy is left unbounded on purpose: cutting semantic-release off in the middle of pushing thirteen gems is worse than waiting for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit c5df079)

What
Two problems, one root cause. The workflow declared no
timeout-minutesanywhere, so a job that hangs pins the run for GitHub's 6-hour default — and the thing that hangs is theunixodbc-devinstall on the two Snowflake legs.On #359 those legs hung for 4 h 35 min on
sudo apt-get update && sudo apt-get install -y unixodbc-dev, against ~75 s when they behave, and it reproduced on a fresh runner 14 h later. Becausetesthasneeds: [lint]andcoveragehasneeds: [test], one stuck leg of a 26-job matrix froze the whole pipeline: no test job ever started, and the PR sat onUNSTABLEwith nothing to show.What the runner actually does
Adding the timeouts was enough to turn the hang into a legible failure, and the logs then answered the three questions the fix depended on.
The Azure mirror is the problem, and it is unreachable in both phases. Every https repo answers in milliseconds;
azure.archive.ubuntu.com, which the image's/etc/apt/apt-mirrors.txtpoints at, does not — during the index refresh and during the package fetch:apt fails over on its own. The mirrorlist already carries
archive.ubuntu.com, and apt switched to it unaided:The package is genuinely absent from the image. The leg that passed reports
0 upgraded, 4 newly installedand unpacksunixodbc-devitself, so the step cannot simply be dropped.Changes
Timeouts, where there were none.
linttestcoveragelintandtestBoth apt commands bounded from the outside, and nothing else.
Four things this gets right, each of which an earlier revision of this PR got wrong:
timeout, not from apt.Acquire::http::Timeoutcaps a single connection, not a whole fetch — with retries on top, one revision spent the entire 8-minute budget looping onIgn:lines and was killed inside its first command..debfiles come from the same mirror and stall there too, as the log above shows.-k 30makes the bounds real.timeoutsends SIGTERM, which apt and dpkg may defer during a transaction; without a kill-after only the step cap would stop anything.sed. That duplicates the failover apt already performs, and rewriting the host desynchronises the indices under/var/lib/apt/lists, so the retry could die onUnable to locate packageand name the wrong cause.Refreshing the lists stays best effort: the image carries usable ones and only the install has to succeed. Worst case is 6.5 min, under the 8-minute cap, and there is no control flow left to reason about — the first line cannot fail, the second carries the verdict.
Three deliberate choices
deployis left unbounded. Cuttingsemantic-releaseoff in the middle ofgem push-ing thirteen gems leaves a half-published release, which is worse than waiting out a hang. The other four jobs are safely interruptible; this one is not.ci:and notfix:..releaserc.jsuses the angular preset with no path filter, so afix:commit landing onmainpublishes all twelve gems. A workflow-only change has no business bumping a version, andciis in the type list.overcommit.ymlaccepts.One risk accepted rather than engineered away. Absorbing the update failure means the install can run against the image's lists and fail with
404 Not Foundif the version they name has been superseded in the archive. Real but unlikely, and apt's own message names the cure. Guarding it would mean putting back the conditional chain this PR just removed.Verification
The timeouts have room against measured history — longest
Lint4 min 21 s against 15,Test3 min 12 s against 20,Coverage12 s against 10 — so a slow but healthy run cannot go red on them.The green run of the final revision is 32240803594, all four Snowflake legs passing. And the bound was exercised for real on an earlier revision, on a leg where the mirror was still down:
timeoutcut the stalled refresh, the best-effort message fired, and the install went through — 4 min 21 s against 2 min 04 s for the leg served by a healthy mirror, which is the cost of the bound and nothing more.Possible follow-up, not done here
strategy.fail-fastis left at its defaulttrue, which is why a failing Snowflake leg shows up ascancelledsiblings. Setting it tofalseonlintandtestwould let every leg report instead of masking some, which helps diagnosis — a separate concern from bounding the pipeline.The step is still written twice, once per job. At four lines with no logic it is cheap duplication; folding it into a
.github/scripts/shell script or a composite action is a refactor worth its own change, and worth waiting until the mirror behaviour has been observed for a while.🤖 Generated with Claude Code
Note
Bound CI job timeouts and harden unixODBC install with mirror fallback
timeout-minutesto all three CI jobs: lint (15 min), test (20 min), and coverage (10 min).apt-get update && apt-get installforunixodbc-devwith a guarded script that setsDEBIAN_FRONTEND=noninteractive, boundsapt-get updatewith a 90s external timeout, and limits apt Acquire retries.azure.archive.ubuntu.comtoarchive.ubuntu.comand retries, reducing flaky CI failures on the Snowflake matrix entry.Changes since #360 opened
unixodbc-devinstallation in CI workflow to use inlineDEBIAN_FRONTEND=noninteractive, direct timeout bounds onapt-get updateandapt-get installcommands, explicitAcquire::RetriesandAcquire::http::Timeoutoptions, and removed mirror fallback logic [d5ed2f2]build.ymlworkflow describing apt mirror behavior andunixodbc-devavailability [c2d8363]Macroscope summarized 238a9d2.