Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions scripts/tests/_pm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# shellcheck shell=bash
# =============================================================================
# _pm.sh — the bounded package-manager runner, shared by the container harnesses
# -----------------------------------------------------------------------------
# Sourced by scripts/tests/distro-prereqs.sh and scripts/tests/path-persist.sh.
# Both run INSIDE a plain distro container with the repo mounted at /src, so a
# sibling source is just a local file read — it needs no network and no tools
# beyond the shell that is already running.
#
# WHY THIS FILE EXISTS. The two harnesses each carried their own copy of
# `_pm_run`, identical in logic and already drifted in prose. When the apt
# socket bounds below were added to fix a 12-minute silent hang, they landed on
# ONE copy — so `Prereqs` was fixed and `PATH persist`, which fails the same
# way, was not. Bugbot caught that inside the very PR that introduced it
# (client#1051). One copy, sourced twice, is the only version of this that
# cannot half-ship.
#
# What is NOT here: each harness's own `_pm_install_one`. Those genuinely
# differ — path-persist supports apk, distro-prereqs does not — and folding
# them together would invent a package-manager matrix neither one tests.
# =============================================================================

# APT NEEDS ITS OWN SOCKET BOUND, not just the external one _pm_run applies.
#
# `timeout 60` around apt kills a stalled fetch, but apt never learns anything:
# it emits NO output, retries nothing, and the next attempt stalls identically.
# The observed failure is three attempts of pure silence — no `Err:`, no `W:`,
# no `E:` — then the job's outer bound killing the container with exit 137. A
# refused connection errors instantly; only a stalled one hangs like that, and
# apt's default socket timeout outlasts the external kill every time. So the
# bound was in the wrong place, not missing.
#
# WHY NOT THE FIX tracebloc-engine#1029 USED. That change found the specific
# cause on the RUNNER — the mirrorlist lists the archive over http first and
# https second, http stopped answering on 2026-09-11, and apt walked 52 index
# URLs before falling back — and fixed it by rewriting the mirrorlist to https.
# THAT MUST NOT BE COPIED HERE. These harnesses run inside a BARE distro
# container, and ubuntu:24.04 ships no ca-certificates: rewriting its sources to
# https makes every index fetch fail certificate verification. Measured, in the
# image the job actually uses:
#
# W: Failed to fetch https://…/InRelease Certificate verification failed:
# The certificate is NOT trusted. The certificate issuer is unknown.
# apt-get update -> exit 0 (warnings only — it "succeeds" fetching nothing)
# apt-get install -> exit 100, the package is not installed
#
# An update that exits 0 having fetched nothing is worse than the hang, because
# the failure moves to whatever needed the package. So https is not available to
# us until something installs ca-certificates, which needs apt, which is the
# circle. The bounds below are what IS available.
#
# They are adequate here in a way they were not on the runner. #1029 measured
# Timeout/Retries alone still stalling 29 minutes, but that was ~52 index URLs;
# a bare container lists four suites, so a total stall costs minutes and ends in
# an honest error rather than a 12-minute silent kill. Bounded to something
# useful, at this scale.
#
# apt-only, deliberately: dnf/yum/zypper/apk/pacman take none of these flags and
# would fail on an unknown option — turning a mirror stall on one distro into a
# hard argument error on five.
#
# NOTE `Acquire::http::Timeout` does not govern https connections (#1029), hence
# both. No ForceIPv4: an earlier version of this carried it on the theory that a
# blackholed AAAA caused the stall. #1029 then measured the real cause on the
# runner to be the SCHEME, not the address family. The flag was harmless but its
# stated reason was wrong, and a flag shipped on a contradicted hypothesis is
# the kind of thing that gets copied forward as fact.
_APT_BOUND='-o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 -o Acquire::Retries=3'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Acquire::Retries=3 works against this fix on the documented failure mode (a silent blackhole). _pm_run already bounds each attempt with an outer timeout 60 and retries the whole call 3×. With Retries=3 apt keeps re-trying every stalled index URL inside that 60 s window (≈ Timeout×(1+Retries)=40 s per URL), so on a bare image whose apt-get update fetches several suite indices serially, apt does not return before the external timeout fires — each of the 3 _pm_run attempts is killed at 60 s exactly as before the fix, and the worst-case total (≈3×60 s + backoff, ×2 packages in distro-prereqs) can still approach the 12-min job bound. The PR title says "fails in seconds" but this file’s own comment concedes "a total stall costs minutes." For the outer-timeout + fail-fast design you want apt to give up quickly — Acquire::Retries=0 (or 1) lets a stalled URL error out at ~10 s so _pm_run fails RED well under 60 s. Retries=3 is redundant with (and here counterproductive to) the outer retry loop.


# Bounded + retried package-manager invocation; "$@" = the PM argv.
#
# `command -v` (not has()) because this runs BEFORE common.sh is sourced;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale justification for one of the two callers. This says command -v is used "because this runs BEFORE common.sh is sourced" — true for distro-prereqs.sh (sources _pm.sh first), but false for path-persist.sh, which sources lib/common.sh at line ~66 and only sources this file at line 84. In that harness has() is already defined when _pm_run runs, so the stated reason does not hold. Using command -v unconditionally is still correct; just reword so the comment is accurate for both callers (or note the ordering differs).

# notices go to stderr.
_pm_run() {
local i
for i in 1 2 3; do
if command -v timeout >/dev/null 2>&1; then timeout "${TB_PM_TIMEOUT:-60}" "$@" && return 0
elif command -v gtimeout >/dev/null 2>&1; then gtimeout "${TB_PM_TIMEOUT:-60}" "$@" && return 0
else "$@" && return 0; fi
echo "::warning::package-manager step stalled or failed (attempt $i/3): $*" >&2
# No backoff after the LAST attempt, so a dead mirror fails RED here well
# under the job's timeout-minutes rather than running the clock out into a
# silent `cancelled` (the failure class of backend#2859).
[ "$i" -lt 3 ] && sleep $((i * 5))
done
echo "::error::package-manager step failed after 3 bounded attempts: $* — the package manager could not reach its mirrors from inside the CI container. This is NOT this PR's diff. Re-running often does NOT help: two consecutive attempts failed identically on 2026-09-11. If apt, the attempts above should carry Acquire::* bounds; if they did not, that is the bug." >&2
return 1
}
36 changes: 7 additions & 29 deletions scripts/tests/distro-prereqs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,15 @@ set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB="$HERE/../lib"
# The bounded package-manager runner and apt's socket bounds live in ONE place;
# both container harnesses source it. See scripts/tests/_pm.sh for why.
# shellcheck source=scripts/tests/_pm.sh
. "${BASH_SOURCE[0]%/*}/_pm.sh"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same fragile source as path-persist.sh:84 — ${BASH_SOURCE[0]%/*} returns the filename unchanged when invoked with no directory component (bash distro-prereqs.sh from inside scripts/tests), sourcing distro-prereqs.sh/_pm.sh which fails; _pm_run/_APT_BOUND then stay undefined and the first $_APT_BOUND use aborts under set -u. HERE is computed on line 30 via dirname and handles this correctly — use . "$HERE/_pm.sh".


# ── Make the container resemble a real host ──────────────────────────────────
# Anyone running the real installer reached it via `curl | bash`, so curl always
# exists and the box has sudo. Minimal base images ship neither — install them
# up front (we are root here) so the rest of the run mirrors a real machine.
# These bootstrap installs run in an EPHEMERAL CI container against distro mirrors
# that occasionally STALL rather than fail. An unbounded package-manager call then
# hangs until the job's `timeout-minutes`, which GitHub reports as `cancelled` — NOT
# a red check — so the failure is silent (this is the exact class that bit the
# sibling path-persist job's opensuse leg on 2026-08-31; backend#2859). Bound each
# attempt and retry: a transient stall recovers, a dead mirror fails FAST with an
# honest error. `command -v` (not has()) because this runs BEFORE common.sh is
# sourced; notices go to stderr. NB: this bounds the harness's own bootstrap only —
# the real installer functions invoked below use common.sh's bounded probes.
_pm_run() { # bounded + retried package-manager invocation; $@ = the PM argv
local i
for i in 1 2 3; do
if command -v timeout >/dev/null 2>&1; then timeout "${TB_PM_TIMEOUT:-60}" "$@" && return 0
elif command -v gtimeout >/dev/null 2>&1; then gtimeout "${TB_PM_TIMEOUT:-60}" "$@" && return 0
else "$@" && return 0; fi
echo "::warning::package-manager step stalled or failed (attempt $i/3): $*" >&2
# No backoff after the LAST attempt, so a dead mirror fails RED here well under
# the job's timeout-minutes rather than running the clock out into a silent
# `cancelled` (the failure class of #2859).
[ "$i" -lt 3 ] && sleep $((i * 5))
done
echo "::error::package-manager step failed after 3 bounded attempts: $* — distro-mirror connectivity inside the CI container, not this PR. Re-run this job." >&2
return 1
}
_pm_install_one() { # install a single package with whatever PM exists
if command -v apt-get >/dev/null 2>&1; then _pm_run apt-get update -qq && _pm_run apt-get install -y -qq "$1"
if command -v apt-get >/dev/null 2>&1; then
# shellcheck disable=SC2086 # _APT_BOUND is a deliberate word-split argv
_pm_run apt-get update -qq $_APT_BOUND && _pm_run apt-get install -y -qq $_APT_BOUND "$1"
Comment thread
LukasWodka marked this conversation as resolved.
elif command -v dnf >/dev/null 2>&1; then _pm_run dnf install -y -q "$1"
elif command -v yum >/dev/null 2>&1; then _pm_run yum install -y -q "$1"
elif command -v zypper >/dev/null 2>&1; then _pm_run zypper --non-interactive install "$1"
Expand Down
40 changes: 6 additions & 34 deletions scripts/tests/path-persist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,42 +78,14 @@ umask 022
DEFAULT_CLI_REF="https://github.com/tracebloc/cli/releases/latest/download/install.sh"
CLI_REF="${TRACEBLOC_CLI_REF:-$DEFAULT_CLI_REF}"
CLI_VERSION="${TRACEBLOC_CLI_VERSION:-}"

# ── Make the container resemble a real host ──────────────────────────────────
# A customer reached install.sh via `curl | sh`, so curl always exists. Minimal
# base images may ship neither curl nor a shell beyond /bin/sh — install what we
# need so the run mirrors a real machine. We are root in the container.
# Package installs run in an EPHEMERAL CI container against distro mirrors that
# occasionally STALL rather than fail. An unbounded `zypper`/`apt`/`dnf` then hangs
# until the job's `timeout-minutes`, and GitHub reports that as `cancelled` — NOT a
# red check — so a scheduled run can rot unnoticed for a week (backend#2859:
# opensuse/leap:15.6 hung the full 20 min on 2026-08-31 while every other leg, and
# this same script run by hand, went green in ~80s). Same infra-stall class the
# workflow already bounds one layer out for `docker pull` (#525/#592) — the
# container's own package installs were the layer left unbounded. Bound each attempt
# and retry: a transient stall recovers (→ green); a genuinely dead mirror fails
# FAST with an honest error (→ red), never a silent 20-minute cancel. `command -v`
# (not has()) so this is safe even before common.sh is sourced; notices go to stderr
# so a caller's `>/dev/null` can't swallow them.
_pm_run() { # bounded + retried package-manager invocation; $@ = the PM argv
local i
for i in 1 2 3; do
if command -v timeout >/dev/null 2>&1; then timeout "${TB_PM_TIMEOUT:-60}" "$@" && return 0
elif command -v gtimeout >/dev/null 2>&1; then gtimeout "${TB_PM_TIMEOUT:-60}" "$@" && return 0
else "$@" && return 0; fi
echo "::warning::package-manager step stalled or failed (attempt $i/3): $*" >&2
# No backoff after the LAST attempt. Sized so the pathological all-installs-
# stall case (a few best-effort installs × 3 bounded attempts) stays well under
# the job's 20-min timeout-minutes, so a dead mirror fails RED here — it does
# not run the clock out into a silent `cancelled` (the very failure of #2859).
[ "$i" -lt 3 ] && sleep $((i * 5))
done
echo "::error::package-manager step failed after 3 bounded attempts: $* — distro-mirror connectivity inside the CI container, not this PR. Re-run this job." >&2
return 1
}
# The bounded package-manager runner and apt's socket bounds live in ONE place;
# both container harnesses source it. See scripts/tests/_pm.sh for why.
# shellcheck source=scripts/tests/_pm.sh
. "${BASH_SOURCE[0]%/*}/_pm.sh"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Source path is fragile — reuse the $HERE this file already computes. ${BASH_SOURCE[0]%/*} strips the last /segment, but when the script is invoked with no directory component (e.g. cd scripts/tests && bash path-persist.sh) there is no / to strip, so %/* returns path-persist.sh unchanged and this sources path-persist.sh/_pm.sh → "No such file or directory". There is no set -e, so execution continues, _pm_run/_APT_BOUND are never defined, and under set -uo pipefail the first $_APT_BOUND reference aborts with "unbound variable". Line 62 already computes HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)", which handles the no-slash case via dirname. Use . "$HERE/_pm.sh" instead. (Same issue in distro-prereqs.sh:35.)


_pm_install() { # install one or more packages with whatever PM exists; best-effort
if command -v apt-get >/dev/null 2>&1; then _pm_run apt-get update -qq && _pm_run apt-get install -y -qq "$@"
# shellcheck disable=SC2086 # _APT_BOUND is a deliberate word-split argv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The # shellcheck disable=SC2086 sits above the whole if/elif chain, so it suppresses SC2086 for every package-manager branch (dnf/yum/zypper/apk/pacman), not just the apt line that needs the deliberate $_APT_BOUND word-split. That would mask a genuine future unquoted-variable bug introduced in any of those branches. distro-prereqs.sh:39 scopes the same directive to the apt line only — match that placement here (move the disable comment to the apt branch).

if command -v apt-get >/dev/null 2>&1; then _pm_run apt-get update -qq $_APT_BOUND && _pm_run apt-get install -y -qq $_APT_BOUND "$@"
elif command -v dnf >/dev/null 2>&1; then _pm_run dnf install -y -q "$@"
elif command -v yum >/dev/null 2>&1; then _pm_run yum install -y -q "$@"
elif command -v zypper >/dev/null 2>&1; then _pm_run zypper --non-interactive --quiet install "$@"
Expand Down
Loading