Skip to content

ci: three robustness gaps — a check that refused the narrow case, a download that blamed tar, and a sanitizer matrix the bank example was never in (fixes #675, fixes #679, refs #672) - #683

Merged
Yaraslaut merged 4 commits into
masterfrom
laneCIROBUST-batch-679-672-675
Sep 21, 2026
Merged

Yaraslaut merged 4 commits into
masterfrom
laneCIROBUST-batch-679-672-675

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Three CI-robustness tickets, one commit each, batched into one PR per the runner's instruction. Branched from origin/master at 95aa5686 (PR #673 had landed by the time the ci.yml boundary was reached, so no ticket was blocked on it).

Commit Ticket Outcome
29c07c3d #675 Landed — narrow --binary mode, floor intact
6dd23963 #672 Landed, narrowlycurl --fail; the class stays open
eedacd68 #679 Landed — new bank-sanitizers job; first bill zero

#675 — the sanitizer-instrumentation check refused a single-target tree

scripts/check_sanitizer_instrumentation.sh conflated two questions behind one floor. "Did this check examine a representative set?" needs the floor — that is the CI invocation. "Is this one binary instrumented?" is a yes/no about a single file and needs no floor, but the script refused it (only examined 1 binaries -- too few for this check to mean anything), so the #673 lane answered it by hand with nm | grep __tsan_, re-deriving the per-mode symbol table and the SIGPIPE trap the script already encodes.

--binary <file> <mode> answers the second question against the same symbol table.

How the narrow mode is kept out of the CI invocation

Structurally, not by convention: it refuses outright, exit 2, when GITHUB_ACTIONS is set. No step of any workflow in this repository can reach it. A CI invocation that tried would fail loudly rather than pass having examined one file. The sweep is untouched — same floor, same message, same exit codes.

env -u GITHUB_ACTIONS defeats it, as it defeats any environment-keyed guard. That is deliberate circumvention, not the accident the guard is for, and the script says so.

Why a mode rather than the documented one-liner

The issue offers nm | grep __tsan_ in a comment as a legitimate close, and it nearly is. Against it: the one-liner is not one line. It is the per-mode symbol table (__asan_/__tsan_/__ubsan_), plus the reason it must be grep -c and not grep -q (under pipefail, nm takes SIGPIPE and an instrumented binary reports as uninstrumented — the script's own comment records catching that), plus the fact that a count of zero is the verdict. Writing that down creates a second copy that can drift from the first. The mode is ~40 lines and keeps one.

Verification

New scripts/test_check_sanitizer_instrumentation.sh, following the repository's test_check_*.sh convention, wired into drift-guard.yml's sanitizer-can-fail job. Twelve cases; no compiler beyond cc and no sanitizer runtime, because the gate's whole measurement is nm | grep -c __<mode>_, so a fixture that defines a function of that name exercises the gate rather than clang, and a hand-written CTestTestfile.cmake is all ctest --show-only needs.

The two load-bearing cases were confirmed to fail against a mutated checker, not assumed:

# guard replaced by `if false`
error: --binary ran under GITHUB_ACTIONS -- the floor can now be bypassed from a workflow step:
1 self-test check(s) failed

# floor lowered from 2 to 1
error: the sweep accepted a one-binary tree -- the floor has been lowered:
1 self-test check(s) failed

All twelve pass unmutated. Separately, the narrow mode was exercised against a real UBSan-instrumented binary during #679's measurement:

check_sanitizer_instrumentation: bank_gui_tests carries 52 __ubsan_ symbols -- ubsan-instrumented.
check_sanitizer_instrumentation: this examined one file and applied no floor; it is not a substitute for the build-tree sweep a CI leg runs.

#672 — an apt/PPA outage fails a leg the same way a defect does

Scoped deliberately to the narrow, cheap part, and the class stays open. Not implemented, because they are decisions rather than implementations: a retry policy for dependency installation, and a CI-wide marker distinguishing "the environment failed" from "the change failed" in the check list. The three apt/PPA outages that make up the rest of #672 are untouched.

What is fixed is the one case where the log actively misled. The installer piped curl -sSL <url> straight into tar -xz; plain curl treats an HTTP 5xx as a successful transfer of the error page, so the decompressor was the only thing that complained. set -o pipefail would not have helped — GitHub runs run: under bash -e without it, so the pipeline's status is tar's regardless.

Reproduced against a local server returning 503, both shapes under bash -e:

=========== OLD SHAPE, 503 (bash -e, no pipefail) ===========

gzip: stdin: not in gzip format
tar: Child returned status 1
tar: Error is not recoverable: exiting now
exit=2
=========== NEW SHAPE, 503 ===========
curl: (22) The requested URL returned error: 503
exit=22
=========== NEW SHAPE, 200 ===========
exit=0
total 4
-rwxr-xr-x 1 yaraslau yaraslau 28 Sep 21 18:36 sccache

The first block is #672's log verbatim. The third is the success path over a tarball with the real release's member layout, so --strip-components=1 still lands the binary.

Applied to all nine copies in ci.yml (the first carries the reasoning, the other eight point at it) and to the two other downloads with the identical defect and the identical one-flag fix: docs.yml's Doxygen tarball and mutation.yml's Mull .deb. Those two are the same finding, not separate ones — the Mull case is measurably worse, since a 503 puts 64 bytes of HTML in ${asset} and leaves dpkg-deb to object to the archive.


#679 — no sanitizer leg builds the bank example

Confirmed: linux-sanitizers, kanban-tsan, ladder-sanitizers and valgrind set zero MORPH_BUILD_BANK_* flags, against eight sites elsewhere in ci.yml. Bank is not a rung, so MORPH_BUILD_LADDER=ON does not reach it either.

The half the ticket did not have

bank's targets never called apply_sanitizers(). ladder_bank_server was the only one that did. Flipping the CI flag alone would have built bank_lib, bank_cli and all three test binaries blind. Measured, bank configured on with the blocks absent:

::error::check_sanitizer_instrumentation: 3 of 9 ctest binaries are not ubsan-instrumented

naming bank_tests, bank_gui_tests, bank_gui_qml_tests. With the blocks in, 9 of 9. They are also required together: removing bank_tests' alone, keeping bank_lib's, does not leave the binary unchecked — it fails to link (undefined reference to __ubsan_handle_type_mismatch_v1_abort).

Why a separate job, not a flag on linux-sanitizers

The ticket proposed the flag. Two measured reasons against it. That leg builds no Qt and its own comment reserves the matrix against GUI stacks, while bank's GUI is where the UB was. And, cold and cacheless, 12 cores, clang 22.1.8:

configure build ninja edges
36s 144s 144 linux-sanitizers clang-ubsan leg's current shape
51s 151s 150 same + Qt, no bank
64s 391s 287 this job: core + Qt + bank + bank GUI

Folding one into the other roughly triples the slowest leg of a three-leg matrix, whose duration is then the matrix's.

Wall-clock delta on existing jobs: zero. No existing job's flags are changed by this PR. Bank's isolated build cost — row 2 to row 3 — is +13s configure, +240s build, +137 ninja edges. ubsan not asan: UBSan diagnoses this class, and ASan over a Qt GUI needs the detect_leaks=0 and suppression story ladder-sanitizers carries. Bank under ASan is not closed by this.

The first bill: zero

Per the #646 (84) and #656 (97) precedents, measured before landing. bank_tests (145 assertions / 21 cases), bank_gui_tests (19 / 5), bank_gui_qml_tests (32 / 2) all clean under -fsanitize=undefined -fno-sanitize-recover=undefined; the full 1696-test suite of this configure is 100% passed, 212s serial, zero runtime error lines. Nothing suppressed, no allowlist entry.

A finding that cuts against the ticket's framing

The ticket says this gap is why #663 survived to be found by reading code. Half true, and the half that is false is the interesting one. Rebuilding Format.hpp as it stood before the fix, in this job's exact configuration:

examples/bank/gui/controllers/Format.hpp:76:38: runtime error: 9.2e+19 is
outside the range of representable values of type 'long'

exit 1 — but only because #663's fix also added the test that calls parseMinor with such a value. Measured too: with the pre-#663 header and the pre-#663 test set, this leg is green. No pre-existing bank test drove that path. The sanitizer gap was real and is what this closes; it was not on its own what let #663 through, and this job's reach is bounded by how much of bank the suites actually drive. That is written into the job's banner, not only here.

Also in this commit, because the job needs it

A bank ctest label on the three suites, so the leg runs bank's 28 tests (6s) rather than re-running the 1696 (212s) that linux-sanitizers' clang-ubsan leg already runs under identical instrumentation. A label filter matching nothing would be this repository's named failure mode; CMakePresets.json's base-test already sets noTestsAction: error — confirmed to exit 8 with No tests were found!!! against a tree built without bank.


Review reasoning (inline, per the runner's constraint — no /code-review, no /simplify)

Filed, not folded

What I did not verify

Fixes #675
Fixes #679
Refs #672

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

Yaraslaut and others added 3 commits September 21, 2026 18:35
…y instrumented?", without lowering the floor that makes its sweep mean something (fixes #675)

`scripts/check_sanitizer_instrumentation.sh` conflated two questions behind one
floor. "Did this check examine a representative set?" needs the floor, and that
is the CI invocation. "Is this one binary instrumented?" is a yes/no about a
single file and needs no floor at all -- but the script refused it:

    only examined 1 binaries -- too few for this check to mean anything

so a developer who had built one target under a sanitizer preset answered it by
hand with `nm | grep __tsan_`, re-deriving the per-mode symbol table and the
SIGPIPE trap the script already encodes. The #673 lane did exactly that.

`--binary <file> <mode>` answers the second question against the same symbol
table and skips the floor. The constraint that matters is that it must not be
usable where the floor was meant to apply, and that is enforced structurally
rather than by convention: the mode refuses outright, exit 2, when
GITHUB_ACTIONS is set, so no step of any workflow in this repository can reach
it. The sweep is unchanged -- same floor, same message, same exit codes.

Both halves are pinned by a new self-test, following the repository's
scripts/test_check_*.sh convention and running in drift-guard.yml's
sanitizer-can-fail job. It needs no compiler beyond `cc` and no sanitizer
runtime: the gate's whole measurement is `nm | grep -c __<mode>_`, so a fixture
that merely defines a function of that name exercises the gate rather than
clang, and a hand-written CTestTestfile.cmake is all `ctest --show-only` needs.

Measured, not asserted. The self-test's two load-bearing cases were confirmed
to fail against a mutated checker on this revision:

  - guard replaced by `if false` ->
      error: --binary ran under GITHUB_ACTIONS -- the floor can now be bypassed
      from a workflow step:
      1 self-test check(s) failed

  - floor lowered from 2 to 1 ->
      error: the sweep accepted a one-binary tree -- the floor has been
      lowered:
      1 self-test check(s) failed

and all twelve cases pass on the unmutated script.

Not verified: the narrow mode against a real sanitizer-instrumented binary --
the fixtures carry the symbol name, not a sanitizer runtime. That is deliberate
(it keeps the self-test in a dependency-free job), and the sweep path, which
shares the same `count_symbols` helper, is exercised against real instrumented
binaries by the three CI legs that already run it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
…rt it as "not in gzip format" (refs #672)

On 2026-09-21 both GCC legs of PR #671 died in the `Install sccache` step with

    gzip: stdin: not in gzip format
    tar: Child returned status 1
    tar: Error is not recoverable: exiting now
    ##[error]Process completed with exit code 2

Nothing compiled, and nothing in the log named the download. The step piped
`curl -sSL <url>` straight into `tar -xz`, and plain curl treats an HTTP 4xx or
5xx as a *successful* transfer of whatever body came back -- so an error page
went down the pipe and the decompressor was the only thing that complained.
`set -o pipefail` would not have helped: GitHub runs `run:` under `bash -e`
without it, so the pipeline's status is tar's regardless.

Reproduced locally against a server that returns 503, the old shape and the new
one, both under `bash -e`:

    =========== OLD SHAPE, 503 (bash -e, no pipefail) ===========

    gzip: stdin: not in gzip format
    tar: Child returned status 1
    tar: Error is not recoverable: exiting now
    exit=2
    =========== NEW SHAPE, 503 ===========
    curl: (22) The requested URL returned error: 503
    exit=22
    =========== NEW SHAPE, 200 ===========
    exit=0
    total 4
    -rwxr-xr-x 1 yaraslau yaraslau 28 Sep 21 18:36 sccache

-- the third block being the success path over a tarball with the real
release's member layout, so `--strip-components=1` still lands the binary in
place.

Applied to all nine copies of the step in ci.yml (the first carries the full
reasoning, the other eight point at it), and to the two other downloads in this
repository with the identical defect and the identical one-flag fix:
docs.yml's Doxygen tarball and mutation.yml's Mull .deb. Those two are the same
finding, not a separate one -- the Mull case is measurably worse, since a 503
puts 64 bytes of HTML in `${asset}` and `dpkg-deb` is left to object to the
archive.

Deliberately NOT in scope, and #672 stays open for them:

  - a retry policy for dependency installation;
  - a CI-wide marker that distinguishes "the environment failed" from "the
    change failed" in the check list.

Both are decisions rather than implementations, and neither is needed for the
one case where the log actively misled. The three apt/PPA outages that make up
the rest of #672 are untouched by this.

Found while doing it, filed rather than folded: `wget -qO- https://apt.llvm.org/
llvm.sh | sudo bash` (nine sites in ci.yml, one in mutation.yml) fails the other
way. Measured against the same 503 server: `wget -qO-` exits 8 and writes zero
bytes, `bash` reads empty input and exits 0, and the pipeline exits 0 -- a
clang-installation step that reports success having installed nothing. Filed
separately because it is a different failure shape needing a different fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
…argets that were never going to carry one (fixes #679)

Measured on 5fc5e78: `linux-sanitizers`, `kanban-tsan`, `ladder-sanitizers` and
`valgrind` set zero `MORPH_BUILD_BANK_*` flags, against eight places elsewhere
in ci.yml that set `MORPH_BUILD_BANK_EXAMPLE=ON`. Bank is not a rung, so
`MORPH_BUILD_LADDER=ON` does not reach it either. Nothing was written down
about excluding it; it was an option nobody turned on.

Two halves, and the second was not in the ticket.

**1. bank's targets never called `apply_sanitizers()`.** `ladder_bank_server`
was the only one that did. So flipping the CI flag alone would not have
instrumented anything -- it would have built `bank_lib`, `bank_cli` and all
three test binaries blind. Measured, on a `clang-ubsan` configure with bank on
and the blocks absent:

    ::error::check_sanitizer_instrumentation: 3 of 9 ctest binaries are not
    ubsan-instrumented

naming bank_tests, bank_gui_tests and bank_gui_qml_tests. With the blocks in
place the same sweep reports 9 of 9. The blocks are also required *together*:
removing bank_tests' alone, with bank_lib's kept, does not merely leave the
binary unchecked, it fails to link with `undefined reference to
__ubsan_handle_type_mismatch_v1_abort`.

**2. a `bank-sanitizers` job (clang-ubsan, Qt + bank + GUI)**, rather than a
flag on `linux-sanitizers`' clang-ubsan leg, which is what the ticket proposed.
Two measured reasons. That leg builds no Qt and its own comment reserves the
matrix against GUI stacks, while bank's GUI is where the UB was. And, cold and
cacheless on 12 cores with clang 22.1.8:

    leg's current shape (core + net + offline_sqlite, no Qt)
        configure 36s   build 144s   144 ninja edges
    this job's shape (core + Qt + bank + bank GUI)
        configure 64s   build 391s   287 ninja edges

Folding one into the other roughly triples the slowest leg of a three-leg
matrix, whose duration is then the matrix's. Split out, **the three existing
legs' flags are not changed at all, so the wall-clock delta on them is zero**,
and this runs beside them. Same precedent and same argument as kanban-tsan.
Bank's isolated build cost -- the number the ticket asked for -- is the
difference between this job and the same configure without bank
(51s / 151s / 150 edges): **+13s configure, +240s build, +137 edges**.

`ubsan` rather than `asan`: UBSan diagnoses this class, and ASan over a Qt GUI
needs the `detect_leaks=0` and suppression story ladder-sanitizers carries,
which this job would have to acquire before it could be believed. Bank under
ASan is not closed by this.

**The first bill is zero.** Per the morph#646 (84) and morph#656 (97)
precedents, measured before landing: bank_tests (145 assertions in 21 cases),
bank_gui_tests (19 in 5) and bank_gui_qml_tests (32 in 2) all pass clean under
`-fsanitize=undefined -fno-sanitize-recover=undefined`, and so does the whole
1696-test suite of this configure (212s serial, 100% passed, zero `runtime
error` lines). Nothing is suppressed and no allowlist entry was added.

**A finding that cuts against the ticket's framing, stated rather than shipped
around.** The ticket says this gap is why morph#663 survived to be found by
reading code. Half true. Rebuilding Format.hpp as it stood before the fix, in
this job's exact configuration, bank_gui_tests exits 1 with

    examples/bank/gui/controllers/Format.hpp:76:38: runtime error: 9.2e+19 is
    outside the range of representable values of type 'long'

-- but only because morph#663's fix also added the test that calls parseMinor
with such a value. With the pre-morph#663 header *and* the pre-morph#663 test
set, this leg is green: no pre-existing bank test drove that path. The
sanitizer gap was real and is what this closes; it was not on its own what let
morph#663 through, and this job's reach is bounded by how much of bank the
suites actually drive. That is written into the job's banner, not just here.

Also here, because the job needs it: a `bank` ctest label on the three suites,
so the leg runs bank's 28 tests (6s) rather than re-running the 1696 (212s)
that linux-sanitizers' clang-ubsan leg already runs under identical
instrumentation. A label filter that matched nothing would be this
repository's named failure mode; CMakePresets.json's `base-test` already sets
`noTestsAction: error`, confirmed to exit 8 with `No tests were found!!!`
against a tree built without bank.

Not verified: bank under ASan or TSan; whether the Lightweight ORM is clean
under anything other than UBSan; and the job's real duration on a hosted
runner, which adds an apt install and a Qt install this local measurement does
not model.

Found while measuring, filed rather than folded: bank's 21 ctest cases share
one SQLite file and cannot run concurrently -- `ctest -j 12` fails 21 of them
with `[SQLite]disk I/O error (10)` where the same binaries pass serially. CI
runs ctest serially (no test preset sets a parallel level), so it is latent
rather than active.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
@Yaraslaut

Copy link
Copy Markdown
Member Author

Runner verification

#675's guard does what it claims, measured not read:

under GITHUB_ACTIONS: exit=2   ::error:: --binary is a local, single-file mode and is refused under
                               GITHUB_ACTIONS -- a CI leg must run the build-tree sweep, whose floor
                               is the only thing that makes 'all instrumented' mean 'all were looked at'
uninstrumented binary: exit=1  ::error file=…:: true carries no __ubsan_ symbols …
self-test:             exit=0

Making the narrow mode structurally unreachable from CI is a better answer than documenting a convention, and the rejection of the issue's own wontfix alternative is well argued: the "one-liner" is really a per-mode symbol table plus the grep -c-not-grep -q SIGPIPE trap the script already records catching, and writing that down creates a second copy that drifts.

#681 is the best thing in this batch and I reproduced it:

$ wget -qO- https://apt.llvm.org/does-not-exist.sh > out.txt
  wget exit=8   bytes written=0
$ wget -qO- https://apt.llvm.org/does-not-exist.sh | bash
  pipeline exit=0

wget fails, writes nothing, bash reads empty input and succeeds, the step passes. A failed LLVM toolchain install reports success, at nine sites across ci.yml and mutation.yml. That is this repository's headline failure mode — a control that reports success having done nothing — living in its own CI setup, and it is a different shape from #672's (which at least fails loudly, just unintelligibly). Filing it separately rather than folding it into #672 was right.

The correction to my triage of #679

I wrote that the sanitizer gap "explains why #663 survived to be found by hand". That is half false and the lane did not ship around it: rebuilding pre-#663 Format.hpp under the new job does trip UBSan — but only because #663's fix also added the test that calls parseMinor with an out-of-range value. With the pre-#663 header and the pre-#663 test set, the leg is green.

The honest statement is narrower than mine: a sanitizer only catches UB on a path something executes, so the missing leg and the missing test were both necessary. The gap was real and is now closed; it was not on its own sufficient to have caught #663. Writing that into the job's banner is the right place for it — the next person to wonder why bank has its own sanitizer job gets the true reason.

That is the third of my triage claims corrected by a lane today, and the pattern is consistent: each was a causal story that sounded specific and went one step further than the evidence.

Two findings the ticket did not have

bank's targets never called apply_sanitizers() — flipping the CI flag alone would have built everything blind (3 of 9 ctest binaries are not ubsan-instrumented), and instrumenting bank_lib without the executables fails to link. So the CMake change is a prerequisite, not a nicety, and "turn the flag on" would have produced a green leg that measured nothing.

A first bill of zero, with the denominator shown — 1696 tests, 100% passed, no suppression and no allowlist entry. A zero is only meaningful next to what was examined, and this one has it.

The new job rather than a flag on linux-sanitizers is justified by measurement: bank costs +240s build and +137 edges, which would roughly triple the slowest leg of a three-leg matrix, and that leg carries its own written exclusion of GUI stacks. kanban-tsan is the precedent.

Not verified by me

The timings, the 1696-test run, the instrumentation counts, and #682's parallel-ctest failure — all need builds I did not run. And the branch's load-bearing claim (GITHUB_ACTIONS is set for every step on a hosted runner) is documented GitHub behaviour pinned by a self-test, not something either of us measured on a runner.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut

Copy link
Copy Markdown
Member Author

Runner: the one red leg is upstream, and the guard this PR adds is what caught it

Bank example / UBSan failed. The job's log is not retrievable yet (three legs still running), so I read the check run's annotations, which populate independently:

failure: check_sanitizer_instrumentation: ctest listed no tests in build/clang-ubsan
         -- this check would pass having examined nothing
failure: The template is not valid. jurplel/install-qt-action/v4/action.yml (Line: 160, Col: 11):
         Expected format {org}/{repo}[/path]@ref. Actual '$/action'

Read those in the right order. The Qt install action failed, so nothing Qt-dependent configured, so ctest listed no tests — and the instrumentation sweep refused to pass on an empty set. That is exactly the floor #675 is about, doing its job on its first real run: a check that had examined nothing declined to report success. The leg is red for the right reason.

The upstream cause

Not a defect here, and not a moved tag. jurplel/install-qt-action@v4's action.yml:160 currently reads:

# Reference same-repository actions with self-repository syntax - GitHub Changelog
#   https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax/
uses: $/action

$/action is GitHub's newer self-repository syntax. Some runner images resolve it; the one that took this job at 17:23:29Z did not. Linux / Qt6 WebSockets — same @v4, same version/modules/cache inputs, verified identical — started at 17:13:34Z and passed. Ten minutes and a different runner apart.

So this is runner-image luck against a floating action tag, which is the fifth distinct infrastructure failure today and a mechanism the earlier four did not have. I have added it to #672.

What I am doing

Re-running the failed job — GitHub refuses while the workflow still has legs in flight, so it is queued behind those. If it recurs, the durable fix is pinning install-qt-action to an immutable ref rather than the floating @v4, which would make Qt installs deterministic across every job. That is a change to four call sites and a version choice, so it belongs in its own PR rather than being bolted onto this one — and it should not gate this PR, whose own leg is correct.

I am also reproducing the job locally (same preset and flags, cold build dir) to confirm the instrumentation sweep reports all binaries instrumented on a tree that actually built. That answers the only question the red leg leaves open: whether the sweep would have passed had Qt installed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

…ak Qt

`Bank example / UBSan` failed with the Qt install step rejected before any
build ran:

  The template is not valid. jurplel/install-qt-action/v4/action.yml
  (Line: 160, Col: 11): Expected format {org}/{repo}[/path]@ref.
  Actual '$/action'

Upstream's action.yml now contains `uses: $/action` -- GitHub's
self-repository syntax, added in their 2026-07-30 changelog. Some runner
images resolve it and some do not. In the same workflow run, `Linux / Qt6
WebSockets` started 17:13:34Z with a byte-identical invocation and passed;
this job started 17:23:29Z and did not. Ten minutes and a different runner
apart, with no change on our side.

`@v4` is a major alias that upstream moves on every release, so this repository
has no say in when that syntax arrives. `v4.3.1` is the last release whose
action.yml does not use it (v4.4.0 and v4.4.1 both do), and it declares every
input used here -- arch, cache, dir, host, modules, target, version. All 11
call sites across ci.yml, wasm-demo.yml and wasm-ladder.yml are pinned
together, because a partial pin leaves the same lottery running on whichever
job was missed.

Verified: no floating `@v4` remains; all seven workflows parse; banner-lint,
option-coverage and catch2-pin pass.

This does not fix the class -- a pinned tag is still a tag, and the durable
answer is a commit SHA. That trade (immutability against a version nobody can
read) is recorded in morph#672 rather than decided here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
@Yaraslaut

Copy link
Copy Markdown
Member Author

Fixed in 948e26b0 — pinned the Qt action, and proved the job itself was sound

The job was never wrong. I reproduced its exact configuration locally (clang-ubsan, MORPH_BUILD_QT/BANK_EXAMPLE/BANK_GUI=ON, cold build directory) and ran the step CI could not reach:

check_sanitizer_instrumentation: 9 ctest binaries all carry __ubsan_ symbols (0 allowlisted).
  exit=0

Nine of nine, against the 3 of 9 the lane measured before its CMake change. So the sweep would have passed had Qt installed — the red leg was the environment, and the guard correctly refused to report success on the empty set that environment left behind.

The fix. @v4 is a major alias upstream moves on every release, which is how uses: $/action arrived in our CI without any change here. Pinned all 11 call sites across ci.yml, wasm-demo.yml and wasm-ladder.yml to v4.3.1 — the last release whose action.yml does not use that syntax (v4.4.0 and v4.4.1 both do), and which declares every input this repository passes: arch, cache, dir, host, modules, target, version.

All eleven together, deliberately: a partial pin leaves the same lottery running on whichever job was missed.

Verified before pushing: no floating @v4 remains, all seven workflows parse, and banner-lint, option-coverage and catch2-pin pass.

What this does not fix, stated rather than implied

A pinned tag is still a tag. v4.3.1 is immutable by convention, not by construction; the durable answer is a commit SHA (48d3ad6db93f). I did not take that because it trades immutability for a reference no reader can identify, and that trade is a decision for this repository rather than one to make inside an unrelated PR. Recorded on #672.

It also does not address the five distinct infrastructure failures today, of which this is one; #672 remains the ticket for the class.

Why this landed here rather than in its own PR

I said earlier it belonged in a separate PR, and I have changed my mind for a reason worth stating: this PR's own leg cannot go green without it, the failure is the same class as #672 which this PR already addresses, and a re-run was a coin flip on runner image rather than a fix. Waiting for a better runner is not fixing it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

@Yaraslaut
Yaraslaut merged commit 7d4ca45 into master Sep 21, 2026
53 of 55 checks passed
Yaraslaut added a commit that referenced this pull request Sep 21, 2026
…all ten sites (fixes #681) (#684)

Ten steps across `ci.yml` and `mutation.yml` installed the pinned Clang with

    wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }}

`wget -q` writes no error document, so on an HTTP 4xx/5xx it exits 8 having
written zero bytes. `bash` then reads an empty script, does nothing, and exits
0. GitHub runs a bare `run:` as `bash -e {0}` -- errexit, and *not* pipefail --
so the pipeline's status is bash's. The step reported success having installed
no compiler, and the leg went red several steps later at Configure, naming a
missing `clang++` rather than the download that never happened.

All ten now use the same three lines the `Install sccache` steps got in #683
(morph#672), so the repository has one download idiom rather than two:

    curl -sSL --fail -o /tmp/llvm.sh https://apt.llvm.org/llvm.sh
    test -s /tmp/llvm.sh
    sudo bash /tmp/llvm.sh ${{ env.CLANG_VERSION }}

`--fail` makes curl report the HTTP status as its own exit code; writing to a
file means nothing downstream runs when it does not arrive; `test -s` covers
the one case `--fail` cannot see, a 200 with an empty body. This is not a retry
policy and it does not make an apt.llvm.org outage less likely -- it makes the
step that failed be the step the log names.

The sites: ci.yml's linux-compilers, linux-sanitizers, linux-coverage,
kanban-tsan, bank-sanitizers, ladder-sanitizers, linux-all-features (the
`else` branch of its gcc/clang split), clang-format and clang-tidy, plus
mutation.yml's install step. The full argument lives once, on linux-compilers;
the other nine carry a three-line pointer to it.

Proof, not assertion: each shape written to a file and run as `bash -e <file>`,
which is what GitHub does, against a local origin that serves `/llvm.sh` and
404s everything else.

    =========== OLD SHAPE, 404  (the defect) ===========
    step exit=0

    =========== OLD SHAPE, 200  (the happy path) ===========
    llvm.sh ran, version argument = 22
    step exit=0

    =========== NEW SHAPE, 404  (must be non-zero) ===========
    curl: (22) The requested URL returned error: 404
    step exit=22

    =========== NEW SHAPE, 200 EMPTY BODY (must be non-zero) ===========
    step exit=1

    =========== NEW SHAPE, 200  (must be zero and must run the script) ===========
    llvm.sh ran, version argument = 22
    step exit=0

The old shape's 404 and its 200 are indistinguishable, which is the ticket. And
against apt.llvm.org itself rather than a local stand-in:

    $ curl -sSL -o llvm.sh https://apt.llvm.org/does-not-exist.sh
    exit=0   564 bytes of error page
    $ curl -sSL --fail -o llvm.sh https://apt.llvm.org/does-not-exist.sh
    curl: (22) The requested URL returned error: 404
    exit=22

Measured on 7d4ca45, GNU wget 1.25.0, curl 8.22.0, bash 5.3.15, Arch Linux.
Not verified: the behaviour of `ubuntu-24.04`'s own curl and wget builds, and
no CI run has yet exercised a deliberate 404 on a runner. The claim this rests
on is that `curl --fail` exits non-zero on an HTTP error status, which is
curl's documented contract and is the same property #683 already depends on at
ten sccache sites on those same runners.


Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant