Skip to content

fix(ci): the apt bounds were calibrated against a claim, and the step over-installed - #417

Merged
doublegate merged 3 commits into
mainfrom
fix/v2.3.9-apt-bounds
Aug 19, 2026
Merged

fix(ci): the apt bounds were calibrated against a claim, and the step over-installed#417
doublegate merged 3 commits into
mainfrom
fix/v2.3.9-apt-bounds

Conversation

@doublegate

Copy link
Copy Markdown
Owner

The aarch64 provision failed on three consecutive PRs in one hour#412, #415, #416. It is not a mirror outage. The log says exactly where:

attempt 1  update killed at 180s, precisely the timeout
attempt 2  update killed at 180s again
attempt 3  update succeeded; install killed at 300s MID-DOWNLOAD,
           the log ending inside `Get:20 gcc-13-aarch64-linux-gnu [21.1 MB]`

No apt error appears anywhere in it. The provision was succeeding slowly, and the wrapper converted that into a hard failure three times over — the shape this release keeps finding, with the sign reversed: a gate that fires on a healthy run rather than one that passes a broken one. It blocked three PRs while reading as infrastructure decay.

The bound came from a claim, not a measurement

The script asserted, in its own comment, that "a healthy update on these runners is a few seconds", which made 180 s look like an order of magnitude of headroom. That was never checked. It is now stated as arithmetic instead, because that phrasing is precisely what went unexamined.

Two independent changes

1. apt-get update runs only after a direct install has failed. The runner image ships a populated index, so a refresh does not belong on the happy path — it is the recovery step for the one failure it actually fixes, an index stale enough that the requested version has moved and install 404s. Attempt 1 skips it, removing the 180 s that killed two of three attempts before they reached the package.

Worst case is now 300 + 15 + 600 + 30 + 600 = 1245s, inside the job's 25-minute budget with room for the cargo check.

2. The step was asking for the wrong package — and the comment above it said so without drawing the conclusion. It named libc6-dev-arm64-cross as what actually lands the headers, and stated the cross linker is unused because this gate is cargo check only. So it installed a whole cross toolchain to obtain a dependency it had already identified: 20+ packages, one of them 21.1 MB, for a set of headers. bindgen runs the host clang against --sysroot and never invokes the cross compiler.

Both ARM targets now install the header package directly, plus --no-install-recommends — the same argument as the timeout, since every byte downloaded is time spent inside a bound.

Verification

  • The narrower package's sufficiency is verified by this job, not asserted. If the headers are not where the export step points clang, bindgen fails loudly in cargo check on the very run that installs it. That is the check to watch on this PR.

  • The loop's branch structure is proven with a stubbed dry run, since the real thing cannot be executed here:

    scenario calls made
    install succeeds first try install1
    install always fails install1 update2 install2 update3 install3
  • shellcheck and actionlint both clean.

What this does not claim

It does not claim the mirror was healthy. Two 180 s update timeouts in a row is slow by any reading. It claims the response to slow was wrong: three retries against a bound too tight to succeed, downloading a toolchain nothing invokes.

… over-installed

The aarch64 provision failed on three consecutive PRs (#412, #415,
#416) in one hour. The log says exactly where, and it is not a mirror
outage:

  attempt 1  update killed at 180s, precisely the timeout
  attempt 2  update killed at 180s again
  attempt 3  update succeeded; install killed at 300s MID-DOWNLOAD, the
             log ending inside `Get:20 gcc-13-aarch64-linux-gnu [21.1 MB]`

No apt error appears anywhere in it. The provision was succeeding
slowly, and the wrapper converted that into a hard failure three times
over -- the shape this release keeps finding, with the sign reversed: a
gate that fires on a healthy run rather than one that passes a broken
one. It blocked three PRs while reading as infrastructure decay.

The bound came from a premise this script asserted and never measured:
"a healthy `update` on these runners is a few seconds", which made 180s
look like an order of magnitude of headroom. Rewritten to state the
arithmetic instead, since that phrasing is precisely what went
unchecked.

Two independent changes.

`apt-get update` now runs ONLY after a direct install has failed. The
runner image ships a populated index, so a refresh does not belong on
the happy path -- it is the recovery step for the one failure it fixes,
an index stale enough that the requested version has moved. Attempt 1
skips it, removing the 180s that killed two of the three attempts before
they reached the package at all. Worst case is now 300 + 15 + 600 + 30 +
600 = 1245s, inside the job's 25-minute budget with room for the check.

The step also asked for the wrong package, and the comment above it said
so without drawing the conclusion: it named `libc6-dev-arm64-cross` as
what actually lands the headers, and stated the cross linker is unused
because this gate is `cargo check` only. So it installed a whole cross
toolchain to obtain a dependency it had already identified -- 20+
packages, one of them 21.1 MB, for a set of headers. bindgen runs the
HOST clang against `--sysroot` and never invokes the cross compiler.
Both ARM targets now install the header package directly, plus
`--no-install-recommends`, which is the same argument as the timeout:
every byte downloaded is time spent inside a bound.

The sufficiency of the narrower package is VERIFIED BY THIS JOB, not
asserted -- if the headers are not where the export step points clang,
bindgen fails loudly in `cargo check` on the very run that installs it.

The loop's branch structure is proven with a stubbed dry run: attempt 1
issues `install` alone, attempts 2 and 3 issue `update` then `install`.
shellcheck and actionlint both clean.
Copilot AI lite review requested due to automatic review settings August 19, 2026 20:09
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1d30c269-5e1c-48fc-b5f0-e30bc84d39b0


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the CI apt provisioning used by the libretro-cross job to be more resilient to slow mirrors and to avoid installing an unnecessary cross toolchain when only glibc headers are needed for bindgen.

Changes:

  • Narrow the bindgen provisioning packages from full GCC cross toolchains to header-only libc6-dev-*-cross packages.
  • Rework .github/scripts/apt-install-retry.sh so apt-get update runs only on retry attempts, and add --no-install-recommends to reduce download time.
  • Update the script’s documentation to reflect the motivating failure mode and intended retry budgeting.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
.github/workflows/ci.yml Switches ARM bindgen provisioning to header-only libc6-dev-*-cross packages.
.github/scripts/apt-install-retry.sh Changes retry flow (install-first, update-on-retry), adds --no-install-recommends, and adjusts timeout commentary.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/scripts/apt-install-retry.sh Outdated
Comment thread .github/scripts/apt-install-retry.sh
This change's own first CI run passed, and the log says why the previous
bounds could not have:

  Fetched 4201 kB in 4min 45s (14.7 kB/s)

Fourteen point seven kilobytes per second. The mirror is degraded by
roughly three orders of magnitude, which is why every bound derived from
"healthy" behaviour was wrong -- and why the old 40 MB package set was
hopeless rather than unlucky: at that rate it needed about 45 minutes,
past the whole job budget.

Even at 4.2 MB it only just passed, and not the way the first draft of
this comment assumed. The real sequence, read from the log rather than
inferred from the exit code:

  attempt 1  install downloaded all 4201 kB (285s), then was killed at
             300s during dpkg unpack -- the download finished, the
             install did not
  attempt 2  update, then install: NO re-download, because the archives
             were already in /var/cache/apt/archives. Succeeded.

So the run was rescued by apt's archive cache persisting across
attempts. That is a genuinely useful property -- each attempt makes
progress instead of starting over -- but it was undesigned and
undocumented, which puts it in the same class as the bound it rescued:
behaviour nobody wrote down, working by accident, and indistinguishable
from a design until it stops.

It is now written down and no longer load-bearing. INSTALL_TIMEOUT is
sized so ONE attempt completes at the worst speed actually observed:
285s of download plus dpkg, so 600s is about 2x that. ATTEMPTS drops to
2 to keep the worst case inside the job's 25 minutes -- 600 + 15 + (180
+ 600) = 1395s plus ~45s of surrounding steps. The third attempt is no
loss: attempt 2 already retries with a refreshed index AND a warm
download cache, covering both the stale index and the slow mirror, so a
third would only repeat it.
@doublegate

Copy link
Copy Markdown
Owner Author

The first run passed, and it corrects this PR's own reasoning

3775e78c. The aarch64 job went green — which verifies the narrowed package: bindgen found the headers where the export step points clang, so libc6-dev-arm64-cross is sufficient and the cross toolchain was never needed. That was the open question in the PR body.

The log also settles what was actually wrong, and it is worse than "a slow mirror":

Fetched 4201 kB in 4min 45s (14.7 kB/s)

14.7 kB/s — degraded by roughly three orders of magnitude. That is why every bound derived from "healthy" behaviour was wrong, and why the old ~40 MB package set was hopeless rather than unlucky: at that rate it needed about 45 minutes, past the entire job budget. No timeout value could have saved it; only the narrowing could.

And the pass was more fragile than I claimed

I wrote that skipping apt-get update on attempt 1 was what removed the failure. The log says otherwise:

attempt 1  install downloaded all 4201 kB (285s), then was killed at 300s
           during dpkg unpack — the download finished, the install did not
attempt 2  update, then install: NO re-download, the archives were already
           in /var/cache/apt/archives. Succeeded.

The run was rescued by apt's archive cache persisting across attempts. That is a genuinely useful property — each attempt makes progress instead of starting over — but it was undesigned and undocumented, which puts it in exactly the class this release keeps finding: behaviour nobody wrote down, working by accident, indistinguishable from a design until it stops.

What changed as a result

It is now documented and no longer load-bearing. INSTALL_TIMEOUT is sized so one attempt completes at the worst speed actually observed — 285 s of download plus dpkg, so 600 s is ~2x — and ATTEMPTS drops to 2 to keep the worst case inside the job's 25 minutes:

600 + 15 + (180 + 600) = 1395s, plus ~45s of surrounding steps

The third attempt is no loss: attempt 2 already retries with a refreshed index and a warm download cache, covering both the stale index and the slow mirror, so a third would only repeat it.

The comment now carries a standing instruction: do not tune these numbers again without a Fetched line to point at.

Both reviewers raised it independently on #417. `APT_PACKAGE` comes from
a workflow `env:` block and never from event data, so this is not
closing a live injection path -- but it is one token that makes the
guarantee structural rather than dependent on every future caller
remembering where the value came from. A value beginning with a hyphen
is now an operand, not an option.
@doublegate

Copy link
Copy Markdown
Owner Author

Antigravity review addressed. One blocking finding accepted, one declined as factually wrong, three suggestions declined.

Accepted — the -- separator (44fb2daa)

Both reviewers raised it independently, and it is right for a reason slightly different from the one given: APT_PACKAGE comes from a workflow env: block and never from event data, so no live injection path is being closed. What the token buys is that the guarantee becomes structural instead of dependent on every future caller remembering where the value came from.

Declined — "--no-install-recommends will silently break any other CI jobs that rely on this script"

Checked rather than argued. The script has exactly two callers, both introduced with it:

.github/workflows/ci.yml:583    (aarch64 provision)
.github/workflows/ci.yml:597    (armhf provision)

There are no other callers, so there is nothing to break. The wider context also runs the other way: --no-install-recommends is already the convention in this repository — .github/actions/rust-setup/action.yml and pgo.yml both use it for their own installs.

Worth recording what I did check while verifying this, because it is the case where the full toolchain is genuinely required: .gitlab-ci.yml:139 still installs gcc-aarch64-linux-gnu, and correctly so — the buildbot links, while this gate is cargo check only. It is deliberately untouched.

Declined — move the long comments to the PR description

Two reasons, one general and one specific to this change.

The general one is that a PR description is not durable in the place it is needed. Six months from now the question is asked at the file, by someone changing a constant, and this repository's convention is that the evidence lives beside the thing it justifies — atlas_panel.rs, perf.rs and apt-install-retry.sh itself all carry that form.

The specific one is that the comment is the fix. The bug being corrected was a bound derived from an unmeasured assertion — the old comment claimed "a healthy update on these runners is a few seconds" and nobody checked. Replacing that with a shorter unmeasured assertion reproduces the defect exactly. What is there now is a measurement (Fetched 4201 kB in 4min 45s), the sequence the log actually shows, and a standing instruction not to tune these numbers again without a Fetched line to point at. That is the load-bearing part, not decoration on it.

Declined — space-separated multi-package support

The script is deliberately single-package and says so in its header. An array would be the right shape if a second caller ever needed more than one, and the tests would move with the type. Adding an unbounded interface for a caller that does not exist is the speculative half of the same instinct the -- is the concrete half of.

Nitpick — "exactly as for aarch64 above"

Kept. The two steps are deliberately parallel and the armhf comment's value is precisely that it says "identical to the one above, for identical reasons" rather than restating it — a copy would drift, and drifted duplicate rationale is how the original wrong claim survived in two places.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR narrows the installed apt packages to only the needed headers and adjusts the retry logic to skip apt-get update on the first attempt while increasing the install timeout.

Blocking issues

None found.

Suggestions

  • .github/scripts/apt-install-retry.sh and .github/workflows/ci.yml: The extensive comments read like a commit message or a post-mortem blog post (citing PR numbers, dates, and kilobytes-per-second math). Per the style guide to add comments sparingly, move this historical narrative to the PR description and keep the inline code comments focused solely on why the current bounds and package choices exist.
  • .github/scripts/apt-install-retry.sh (line 128): Quoting "$APT_PACKAGE" after -- prevents word-splitting. While this is fine for a single package (as used in this PR), if a caller ever passes multiple packages (e.g., APT_PACKAGE="pkg1 pkg2"), apt-get will treat it as a single package name containing a space and fail. Consider dropping the quotes if multi-package installation might be needed in the future.

Nitpicks

  • .github/scripts/apt-install-retry.sh (line 135): The defensive phrasing about --no-install-recommends not being a "size micro-optimisation" is unnecessary. Simply stating that it avoids downloading unused dependencies to preserve timeout budget is sufficient.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

@doublegate

Copy link
Copy Markdown
Owner Author

Re-review after 44fb2daano blocking issues. Three points remain; two are repeats already answered above, and the third I want to decline concretely rather than by preference.

"Consider dropping the quotes" so a future APT_PACKAGE="pkg1 pkg2" would word-split. Declined, and this one would be a regression rather than a trade-off. Unquoted $APT_PACKAGE reintroduces glob expansion along with word splitting, so a value containing * or ? would expand against the working directory before apt-get ever saw it — and shellcheck (which gates this file in pre-commit and passes today) flags exactly that as SC2086. It would also undo the -- you asked for two comments ago in the same breath: the point of -- is that the operand is taken literally, which unquoting removes.

If multi-package support is ever needed, the correct shape is an array (APT_PACKAGES=(a b); apt-get install -- "${APT_PACKAGES[@]}"), which keeps both properties. Not built for a caller that does not exist.

Comment length — answered in the previous comment: the comment is the fix here. The defect was a bound derived from an unmeasured assertion; a shorter unmeasured assertion reproduces it exactly.

The "not a size micro-optimisation" phrasing — fair as style, kept for one reason. --no-install-recommends looks like a tidy-up flag, and the sentence exists to say it is load-bearing: at the measured 14.7 kB/s every byte is time inside a timeout, so trimming the package set is the difference between passing and not. Without that framing the next reader has no reason not to drop it.

@doublegate
doublegate merged commit e59f480 into main Aug 19, 2026
25 checks passed
@doublegate
doublegate deleted the fix/v2.3.9-apt-bounds branch August 19, 2026 21:05
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.

2 participants