Skip to content

ci: bound the apt installs and prefer the canonical Ubuntu archive - #117

Merged
abrichr merged 1 commit into
mainfrom
claude/bound-apt-install
Aug 19, 2026
Merged

ci: bound the apt installs and prefer the canonical Ubuntu archive#117
abrichr merged 1 commit into
mainfrom
claude/bound-apt-install

Conversation

@abrichr

@abrichr abrichr commented Aug 19, 2026

Copy link
Copy Markdown
Member

What

Bound every apt step in this repository and point apt at the canonical Ubuntu archive.

# Workflow Job Runner OS Step Timeout before Timeout after
1 build.yml native-installers (matrix leg Linux x86_64, if: runner.os == 'Linux') ubuntu-22.04 Install Linux build dependencies none (job has none either) 10 min
2 ffmpeg-runtime.yml build (matrix leg Linux x86_64, if: runner.os == 'Linux') ubuntu-22.04 Install Linux build tools none (job has none either) 10 min
3 native-release.yml build-linux ubuntu-22.04 Install Linux bundle dependencies none (job has none either) 10 min

These are the only three apt steps in the repository. grep -rn "apt-get\|apt install\|add-apt-repository\|apt-key\|aptitude\|dpkg\|snap install" .github/workflows/ finds no others, so nothing beyond the three listed above needed a fix. The other eight workflows (codeql, dependency-review, native-freshness, notify-docs, release-health, release, secret-scan, test) touch no package manager on Linux.

Why

An apt step with no step-level timeout and no job-level timeout hangs until the run limit when the mirror faults. Two such incidents happened on 2026-08-19 in sibling repositories: an apt step ran 3h11m in openadapt-flow, and another ran over an hour on main in openadapt-capture before it was cancelled.

The hosted runner resolves its Ubuntu mirror through /etc/apt/apt-mirrors.txt, which points at azure.archive.ubuntu.com. apt does fall back to the canonical archive on its own, but it spends the whole budget getting there.

A job-level timeout would not be enough on its own: one hung step then consumes the entire job budget, and in build.yml and ffmpeg-runtime.yml the job is a four-platform matrix whose other legs have nothing to do with apt.

How

Each of the three steps keeps its name, its if: guard, its shell: key, and its package list, and gains:

  1. timeout-minutes: 10.
  2. A best-effort sed rewrite of /etc/apt/apt-mirrors.txt to archive.ubuntu.com. An absent or already-canonical file changes nothing (2>/dev/null || true).
  3. Three bounded apt-get update attempts with 10s/20s backoff, each failure raising a ::warning::. After three failures the step fails closed with an ::error:: and never reaches the install.
  4. set -e restored before the install, so an install failure still fails the step.

This is the pattern already merged and measured in openadapt-flow #374 (a 10-minute timeout became 83s) and openadapt-capture #81 (19s).

Timeout sizing

All three are ordinary package installs, not toolchain downloads. The largest is libwebkit2gtk-4.1-dev plus the Tauri bundling dependencies in workflows 1 and 3; build-essential nasm pkg-config zlib1g-dev in workflow 2 is mostly already present on the hosted image. A healthy mirror serves any of them in well under a minute, so 10 minutes is generous for all three and no step needed a larger budget. apt-get update itself is separately bounded by the three-attempt loop.

Release-workflow safety

native-release.yml publishes releases, so the change there is deliberately minimal. The diff for that file is 25 lines, all inside the one apt step. Untouched: the validate gate, build-macos signing and notarization, build-windows signing, the Refuse unverified Linux package-signing configuration preflight, the sidecar build, npm run tauri build, the DEB/AppImage install-launch-uninstall smoke test, native_release.py stage, every upload-artifact, attest, publish-draft, point-engine-release, and mirror-installers-to-engine-release.

The Linux bundle contents depend on the installed packages, so the package list is byte-identical to the previous revision. Verified mechanically for all three files by diffing the apt-get install block against origin/main:

=== build.yml: package-list byte-diff (origin/main vs HEAD) ===          IDENTICAL
=== ffmpeg-runtime.yml: package-list byte-diff (origin/main vs HEAD) === IDENTICAL
=== native-release.yml: package-list byte-diff (origin/main vs HEAD) === IDENTICAL

Verification

actionlint — clean on the three changed files and on the whole workflow directory. shellcheck is on PATH, so actionlint linted each run: body as bash too.

YAML parsepython3 -c "import yaml;yaml.safe_load(open(F))" succeeds for all three files.

shellcheck -s bash — clean on each of the three run: bodies extracted from the parsed YAML.

Behaviour — the run: body was executed under bash -e (GitHub's default Linux run: shell) with sudo, apt-get, and sed stubbed. The sed stub always exits non-zero, simulating an absent /etc/apt/apt-mirrors.txt:

apt-get update failures install exit step exit update calls install ran
0 0 0 1 yes
2 (then success) 0 0 3 yes
3 1 3 no
0 1 1 1 yes

Row 2 shows the retry recovering. Row 3 shows it failing closed with ::error::apt-get update failed three times; the Ubuntu mirror is unreachable. Row 4 shows set -e restored, so a genuine install failure is not swallowed. Every row shows the failing sed not aborting the step.

Real runner coverage. None of the three steps runs on a pull_request event, so the pull-request checks alone do not exercise them:

  • classify_build_scope returns BuildScope(True, False, "representative pull-request sidecar") for every pull request, so native-installers is skipped here by design. (FULL_ARTIFACT_FILES only widens the scope on a push to main.)
  • ffmpeg-runtime.yml is workflow_dispatch only.
  • native-release.yml triggers only on a desktop-v* tag push.

To get real evidence rather than an argument, step 1 was exercised on a live ubuntu-22.04 runner through this repository's own supported release-candidate path: a workflow_dispatch of build.yml on this branch, at the exact head, which sets run_native=true and runs the full four-platform matrix. build.yml publishes nothing — it uploads CI artifacts with 14-day retention under permissions: contents: read. The run is linked in a comment below.

Steps 2 and 3 stay unexercised on a pull request. Their run: bodies are character-for-character identical to step 1 apart from the package list, and all three were verified statically and behaviourally as described above.

No check was weakened or removed. No assertion or test was changed.

🤖 Generated with Claude Code

Three Linux apt steps had no step-level and no job-level timeout, so a
mirror fault hung them until the six-hour run limit rather than failing
fast. Two such incidents happened on 2026-08-19 in sibling repos: an apt
step ran 3h11m in openadapt-flow, and another ran over an hour on main in
openadapt-capture.

The hosted runner resolves its Ubuntu mirror through
/etc/apt/apt-mirrors.txt, which points at azure.archive.ubuntu.com. apt
does eventually fall back to the canonical archive, but it spends the
whole budget getting there.

Apply the pattern already merged and measured in openadapt-flow #374
(10-minute timeout to 83s) and openadapt-capture #81 (19s) to every apt
step in this repository:

- .github/workflows/build.yml, job native-installers,
  "Install Linux build dependencies"
- .github/workflows/ffmpeg-runtime.yml, job build,
  "Install Linux build tools"
- .github/workflows/native-release.yml, job build-linux,
  "Install Linux bundle dependencies"

Each step gains timeout-minutes: 10, a best-effort rewrite of the mirror
list to archive.ubuntu.com, and three bounded apt-get update attempts
with backoff that fail closed with an ::error:: annotation. set -e is
restored before the install, so an install failure still fails the step.

Step names, package lists, and install flags are byte-identical to the
previous revision; native-release.yml's signing, notarization, staging,
and upload steps are untouched. These are the only three apt steps in
the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abrichr

abrichr commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Measured evidence on a live runner

build.yml workflow_dispatch on this branch, at the exact head 60a286b:
https://github.com/OpenAdaptAI/openadapt-desktop/actions/runs/32295518065

Job Native installer (Linux x86_64), runner ubuntu-22.04success.

Step Install Linux build dependencies: 19:53:48Z to 19:54:10Z = 22 seconds, against a 10-minute budget.

From that job's log:

  • 136 apt fetch lines resolve to http://archive.ubuntu.com. The single azure.archive.ubuntu.com occurrence is the echoed sed line in the command banner, not a fetch, so the mirror rewrite took effect.
  • No ::warning:: and no ::error:: annotation fired. apt-get update succeeded on attempt 1, so the retry loop stayed idle and cost nothing on the healthy path.
  • The install then ran under restored set -e and the whole native Linux installer job went on to build, smoke-test, and stage the DEB and AppImage successfully.

This is the same order of magnitude as the two reference fixes: openadapt-flow #374 (83s) and openadapt-capture #81 (19s).

A pull request cannot exercise this step — classify_build_scope returns run_native=false for every pull_request event, so native-installers is skipped in the PR checks by design. The dispatch above is this repository's own supported release-candidate path and publishes nothing.

🤖 Generated with Claude Code

@abrichr
abrichr merged commit d354180 into main Aug 19, 2026
27 checks passed
@abrichr
abrichr deleted the claude/bound-apt-install branch August 19, 2026 20:13
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.

1 participant