Skip to content

ci: put every job banner over its job, and record what tests/.clang-tidy silences in headers (fixes #621, fixes #632) - #638

Merged
Yaraslaut merged 2 commits into
masterfrom
laneCI-batch-621-632
Sep 20, 2026
Merged

Yaraslaut merged 2 commits into
masterfrom
laneCI-batch-621-632

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Two tickets on the CI / clang-tidy configuration tree, one commit each.

Both are about the same failure mode from AGENTS.md: a control that reports
success while measuring nothing. In each case the obvious check is the vacuous
one, and the commit is mostly about not shipping that.


#621ci: move seven job banners onto the jobs they describe

Reading ci.yml top to bottom, # ── Valgrind (memcheck) ── was followed by
three steps belonging to linux-all-features, and only then by valgrind:.
Six more banners had the same shape. For several jobs the paragraphs under the
banner are the only written record of why the job exists — # ── Linux: every rung's tests under AddressSanitizer + UBSan ── carries ~30 lines on why ASan
and UBSan but not TSan, citing morph#128 — and a reader who scrolls into that
argument attributes it to whichever job's steps surround it.

The move is comment-only, and that is checked rather than claimed

Diffing the base revision's ci.yml against the moved one:

banners old/new: 17 17 same multiset: True
non-comment lines identical & in order: True 1133 1133
comment multiset identical: True 1173 1173
parsed YAML identical: True jobs: 18

Every non-comment line is byte-identical and in the same order; the set of
comment lines is unchanged; the parsed YAML is equal object-for-object. No job,
step, key or expression changed — only which lines the comments sit between.

With the new banner-lint job added, re-checked against the base with the new
job removed from the parse:

jobs added: ['banner-lint']
jobs removed: []
every pre-existing job byte-identical after parse: True

The vacuity trap, and what was done about it

A check that reports displaced=0 would pass equally well on a file with every
banner deleted — deletion removes displacements. So
scripts/check_workflow_job_banners.py is a two-way pairing:

  • A. Every banner must introduce a job: skipping its own continuation
    comments and blank lines, the next line must be a top-level <job-id>: key.
  • B. Every job must be introduced by a banner. Delete a banner to satisfy A
    and B goes red.

Four jobs legitimately carry no banner (ci.yml's probe-self-hosted, which
is runner selection rather than a section; drift-guard.yml's three-job
un-sectioned preamble). They are listed in UNBANNERED with a reason, under
the two hygiene rules copied from check_workflow_option_coverage.py's
EXEMPT: an entry for a job that does not exist is an error, and an entry for
a job that does have a banner is an error. Workflows with no banner at all
are skipped, and that is derived from the file rather than listed — docs.yml,
mutation.yml, spec-sync.yml, suppression-guard.yml and the two wasm
workflows have never used the style, and a gate that demanded they adopt it
would be inventing a convention.

Listing the exempt jobs positively, rather than deriving "everything before the
file's first banner is preamble", is deliberate: under that derivation,
deleting a file's first banner would silently reclassify its job as preamble
and the deletion would go unreported.

Measured

  • Against the pre-fix ci.yml the gate reports the seven displacements at
    321, 711, 877, 967, 1363, 1641 and 1919 — exactly the set ci.yml: seven job banner comments sit above the previous job's trailing steps #621 filed — plus
    the seven now-bannerless jobs, and exits 1.
  • Against this tree it exits 0 over all 24 banners in ci.yml and
    drift-guard.yml.
  • scripts/test_check_workflow_job_banners.sh drives twelve mutations and all
    twelve pass:
ok: the unmodified tree passes
ok: caught: a banner displaced into the previous job's trailing steps
ok: caught: a banner with a rationale block displaced upward
ok: caught: a banner deleted rather than moved
ok: caught: a new job added to a bannered workflow with no banner
ok: caught: an exemption for a job that now has a banner
ok: caught: an exemption naming a job the workflow does not declare
ok: caught: the banner syntax is reworded tree-wide
ok: accepted: a banner's text rewritten in place
ok: accepted: blank lines and rationale paragraphs between banner and job
ok: accepted: a new job added together with its banner
ok: accepted: a job added to a workflow with no banners at all

drift-guard.yml is the more natural home for the job and is where it belongs
once PR #635 lands; it is held there now, so the job lives in ci.yml
the same arrangement the option-coverage job is in, and how #627/#633 were
handled. Its six banners were measured and are all correctly placed, so #621's
"re-open if the same pattern is found in another workflow" does not fire.


#632lint: record and gate the reach tests/.clang-tidy actually has

clang-tidy resolves its configuration from the path of the translation
unit
, not from the path of the file a diagnostic lands in. tests/.clang-tidy
therefore scopes which TUs it governs, not which files their diagnostics may
name: its thirteen suppressions — each argued as Catch2 or raw-syscall idiom,
correctly, about test code — are also off for every include/morph/** header
those TUs reach.

#632 confirmed the mechanism on one case and explicitly did not size it.

Sized

a8511aa6, clang-tidy 22.1.8, Arch Linux, against the compile database the
clang-tidy job configures for itself (clang-debug, every optional feature
ON: 703 entries, 134 distinct TUs under tests/). The job's own option set.
Each TU run twice — once with tests/.clang-tidy present, once with it moved
aside. All 134 offered, all 134 analysed, zero clang-diagnostic-error in
either run.
Findings deduplicated on (file, line, column, check).

distinct findings inside include/morph/**, from those 134 TUs
  with tests/.clang-tidy in place ....  262
  with it removed ....................  595
  hidden by it .......................  333, across 25 headers

  247  cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
   57  readability-identifier-length
    6  cppcoreguidelines-pro-bounds-array-to-pointer-decay
    5  modernize-avoid-c-arrays
    5  cppcoreguidelines-avoid-c-arrays
    5  bugprone-unchecked-optional-access
    3  cppcoreguidelines-pro-type-vararg
    3  readability-convert-member-functions-to-static
    2  readability-function-cognitive-complexity

  most affected: forms/forms.hpp 90, session/session_auth.hpp 61,
                 net/detail/sha1.hpp 32, forms/views.hpp 31,
                 util/quantity.hpp 19

The set difference is one-directional — 0 findings appear with the file
present that do not appear with it removed — which is what the mechanism
predicts and would not hold if the two runs differed for some other reason.

The fix, and why this one and not the others

#632 offers three closes. The reach cannot be narrowed — clang-tidy offers no
way to scope a suppression to the file a diagnostic lands in — and building
#580's header gate is explicitly not in this batch. So: write it down, and gate
the record.

  • tests/.clang-tidy gains a section stating the reach, the measurement above,
    and a machine-readable header-reach: list naming all thirteen checks a
    test-TU-driven gate cannot report in a header.
  • scripts/check_tidy_suppression_scope.sh checks both halves. Textually,
    that header-reach: names exactly what Checks: subtracts. Behaviourally,
    that the reach still exists: a probe header under include/morph/ is
    analysed from a TU under tests/ and from one that is not, using this
    repository's real .clang-tidy files, and the finding must be absent from
    the first and present from the second.
  • Both run in the existing clang-tidy job — the only job that already has a
    pinned clang-tidy on it — before the Configure step, since neither reads a
    compile database.

The second probe is the anti-vacuity control and is the whole reason the
self-test exists. Assertion A is a negative — that a check does not fire —
which a probe that fails to compile, a misspelt check name, or an invocation
that analysed nothing all satisfy by accident. The behavioural form also means
the note goes red rather than quietly stale if clang-tidy ever starts resolving
configuration per diagnostic file.

Measured

The probe reproduces #632's mechanism independently of the 134-TU sweep:

ok: tests/.clang-tidy subtracts 13 check(s)
ok: the `header-reach:` list names all 13 subtracted check(s)
ok: probe: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access is reported in the header from src/probe_scope_tu.cpp
ok: probe: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access is suppressed in the header from tests/probe_scope_tu.cpp
ok: probe: readability-identifier-length is reported in the header from src/probe_scope_tu.cpp
ok: probe: readability-identifier-length is suppressed in the header from tests/probe_scope_tu.cpp

ok: tests/.clang-tidy records the reach it actually has

scripts/test_check_tidy_suppression_scope.sh, nine cases, all passing:

ok: the unmodified tree passes
ok: caught: a suppression added without a header-reach: entry
ok: caught: a header-reach: entry for a check that is not suppressed
ok: caught: a suppression removed but left in the record
ok: caught: the Checks: block becomes unparseable
ok: caught: the probed check stops being suppressed in headers
ok: caught: the probe can no longer fire from anywhere
ok: accepted: the surrounding prose rewritten
ok: accepted: a suppression added together with its header-reach: entry

A correction to the ticket

#632 says "The same applies to the nine examples/*/tests/.clang-tidy copies,
which carry the same entries."
They do not. All nine (plus
examples/common/testkit/.clang-tidy) are byte-identical to each other and
each subtracts exactly one check:

$ md5sum examples/*/tests/.clang-tidy examples/common/testkit/.clang-tidy tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/bank/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/bookmarks/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/crm/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/kanban/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/ledger/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/lims/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/pastebin/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/polls/tests/.clang-tidy
205a4e7f64169d228d60ae5c20a751a7  examples/common/testkit/.clang-tidy
f61ed8596557b99fe8835e04cb072ea1  tests/.clang-tidy

$ grep '^Checks:' examples/bank/tests/.clang-tidy
Checks: '-bugprone-chained-comparison'

Their reach is the same; its effect is nil.
bugprone-chained-comparison fires only on Catch2's REQUIRE expansion, whose
diagnostic is attributed to the test source line rather than to a morph header.
Measured over the 154 example test TUs in the same database, with and without
those nine files: 312 findings inside include/morph/** either way, zero
difference.

That measurement proves less than it looks, and is written down as such:
bugprone-chained-comparison produced zero findings in either run,
because Catch2 3.16.0 locally carries the /* NOLINT(...) */ the config file's
own comment describes. The run cannot separate "no header reach" from "the
check never fired". The structural argument carries the conclusion; the
measurement only fails to contradict it. scripts/check_rung_filters.sh still
passes, and none of the nine files is touched.


Not folded in

What is not verified

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

Yaraslaut and others added 2 commits September 20, 2026 21:31
…ixes #621)

Seven of ci.yml's seventeen `# ── … ──` section banners sat above the
*previous* job's trailing `sccache stats` / `Save sccache` steps, so each
banner -- and for several jobs the multi-paragraph argument under it, which is
the only written record of why that job exists -- described a job beginning
6-31 lines further down, behind another job's steps. `# ── Valgrind (memcheck)
──` was followed by three `linux-all-features` steps and only then by
`valgrind:`.

The move is comment-only. Verified against the base revision:

  banners old/new: 17 17 same multiset: True
  non-comment lines identical & in order: True 1133 1133
  comment multiset identical: True 1173 1173
  parsed YAML identical: True jobs: 18

so no job, step, key or expression changed -- only which lines the comments
sit between.

A displacement check on its own would have been vacuous: a file with every
banner *deleted* scores a perfect zero displacements, which is this
repository's named failure mode. scripts/check_workflow_job_banners.py
therefore pairs both ways -- every banner must introduce a job, and every job
must be introduced by a banner -- so deleting a banner fails rule B instead of
satisfying rule A. Four jobs carry no banner today and are listed in
UNBANNERED with a reason; the two hygiene rules from
check_workflow_option_coverage.py's EXEMPT (an entry for a job that does not
exist is an error, an entry for a job that now has a banner is an error) keep
that list from rotting. Workflows with no banner at all are skipped, derived
from the file rather than listed.

Measured, not asserted:

  - Run against the pre-fix ci.yml the gate reports the seven displacements at
    321, 711, 877, 967, 1363, 1641 and 1919 -- exactly the set #621 filed --
    plus the seven now-bannerless jobs, and exits 1.
  - Run against this tree it exits 0 over all 24 banners in ci.yml and
    drift-guard.yml.
  - scripts/test_check_workflow_job_banners.sh drives twelve mutations: the
    founding displacement reproduced mechanically, a banner deleted rather
    than moved, a new job added without a banner, both UNBANNERED hygiene
    rules, the banner syntax reworded tree-wide (anti-vacuity), and four
    false-positive mirrors. All twelve pass.
  - python3 scripts/check_workflow_option_coverage.py . still exits 0 with the
    new job present.

drift-guard.yml is the more natural home for the job and is where it belongs
once PR #635 lands; it is held there now, so the job lives in ci.yml -- the
same arrangement the option-coverage job is in. drift-guard.yml's own six
banners were measured and are all correctly placed.

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

clang-tidy resolves its configuration from the path of the translation unit it
analyses, not from the path of the file a diagnostic lands in. tests/.clang-tidy
therefore scopes which TUs it governs, not which files their diagnostics may
name: its thirteen suppressions -- each argued as Catch2 or raw-syscall idiom,
correctly, about test code -- are also off for every include/morph/** header
those TUs reach. A header gate driven by test TUs is green on findings it was
built to catch.

#632 confirmed the mechanism on one case and did not size it. Measured here, on
a8511aa with clang-tidy 22.1.8, over the 134 tests/ TUs in the compile database
the clang-tidy-diff job configures for itself (every optional feature ON; 703
entries, 134 distinct tests/ TUs, all 134 analysed, zero clang-diagnostic-error
in either run):

  distinct findings inside include/morph/**, from those 134 TUs
    with tests/.clang-tidy in place ....  262
    with it removed ....................  595
    hidden by it .......................  333, across 25 headers

    247  cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
     57  readability-identifier-length
      6  cppcoreguidelines-pro-bounds-array-to-pointer-decay
      5  modernize-avoid-c-arrays
      5  cppcoreguidelines-avoid-c-arrays
      5  bugprone-unchecked-optional-access
      3  cppcoreguidelines-pro-type-vararg
      3  readability-convert-member-functions-to-static
      2  readability-function-cognitive-complexity

The set difference is one-directional: 0 findings appear with the file present
that do not appear with it removed, which is what the mechanism predicts.

The reach cannot be narrowed -- clang-tidy offers no way to scope a suppression
to the file a diagnostic lands in -- so this takes #632's third close: write it
down, and gate the record so it cannot go stale.

  - tests/.clang-tidy gains a section stating the reach, the measurement above,
    and a machine-readable `header-reach:` list naming all thirteen checks a
    test-TU-driven gate cannot report.

  - scripts/check_tidy_suppression_scope.sh checks both halves. Textually, that
    `header-reach:` names exactly what `Checks:` subtracts. Behaviourally, that
    the reach still exists: a probe header under include/morph/ is analysed from
    a TU under tests/ and from one that is not, using this repository's real
    .clang-tidy files, and the finding must be absent from the first and present
    from the second. The second probe is the anti-vacuity control -- the first
    assertion is a negative, which a probe that produces no findings at all
    satisfies by accident. Behavioural rather than a grep so that the note goes
    red rather than quietly stale if clang-tidy ever resolves configuration per
    diagnostic file.

  - Both run in the existing clang-tidy job, the only job that already has a
    pinned clang-tidy, before the Configure step: neither reads a compile
    database.

Verified:

  - scripts/test_check_tidy_suppression_scope.sh drives nine cases -- a
    suppression added without a record entry, a record entry with no
    suppression, a suppression removed but recorded, the Checks: block reworded
    out from under the parser, the probed check ceasing to be suppressed, the
    probe rendered unable to fire from anywhere, and two false-positive
    mirrors. All nine pass.
  - The probe reproduces #632's mechanism independently of the sweep above: the
    same two checks are reported against include/morph/probe_scope.hpp from
    src/probe_scope_tu.cpp and not from tests/probe_scope_tu.cpp.

On the nine examples/*/tests/ copies: #632 states they "carry the same
entries". They do not -- each subtracts exactly one check,
bugprone-chained-comparison. Their reach is the same but its effect is nil:
that check fires only on Catch2's REQUIRE expansion and is attributed to the
test source line. Measured over the 154 example test TUs in the same database,
findings inside include/morph/** were identical with and without those nine
files, 312 either way -- but that run cannot separate "no header reach" from
"the check never fired", because Catch2 3.16.0 locally carries the NOLINT and
bugprone-chained-comparison produced zero findings in either run. The
structural argument carries the conclusion; the measurement only fails to
contradict it. Recorded as such in tests/.clang-tidy, and noted on #632.

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 (landing sweep, 2026-09-20)

Checked against the repository rather than against the report. The safety claim holds; one figure in it does not describe what it says it describes.

1. No pre-existing executable line changes. Classifying every changed line in 2e980b98:

#621 commit: +95 lines (86 comment/blank, 9 code)
             -63 lines (63 comment/blank, 0 code)

DELETED code lines (a pure comment move must have none):   (none)

ADDED code lines:
    banner-lint:
      name: Every job's section banner introduces that job
      runs-on: ubuntu-24.04
      steps:
        - uses: actions/checkout@v4
        - name: Self-test the job-banner checker
          run: bash scripts/test_check_workflow_job_banners.sh
        - name: Check every workflow section banner introduces its job
          run: python3 scripts/check_workflow_job_banners.py .

Zero deleted code lines is the property that matters, and it is exactly what a pure comment move must produce. Parsing base against head and diffing job by job agrees:

head vs base -- jobs added: ['banner-lint'] removed: []
head vs base -- pre-existing jobs that CHANGED: ['clang-tidy']
   clang-tidy: steps added: ['Self-test the clang-tidy suppression-scope checker',
                             'Check tests/.clang-tidy records the reach it actually has']
             | removed: []

2. Correction to the report's figure. The hand-off states, for the #621 commit, "non-comment lines identical & in order: True 1133 1133 … parsed YAML identical: True jobs: 18". Measured at that commit, none of those three hold:

#621 commit -- non-comment lines identical & in order: False 1133 1142
#621 commit -- comment multiset identical: False
#621 commit -- parsed YAML identical: False | jobs: 18 19

because the commit also adds the banner-lint job. Those numbers describe the banner move in isolation, not the commit. The substantive claim — no pre-existing job altered — is true and is what checks 1 above. Flagged because "parsed YAML identical" is the kind of figure a later reader would take at face value.

3. The gate is not vacuous. Ran check_workflow_job_banners.py from this PR against the pre-fix .github/workflows/ tree at a8511aa6:

error: .github/workflows/ci.yml:321: this section banner does not introduce a job --
    # ── Linux: sanitizers + coverage (all clang) ──
    the next non-comment line is 323: # Cumulative hit/miss for this leg. …
[… 711, 877, 967, 1363, 1641, 1919 …]
exit=1

Same seven, same line numbers as filed. The gate fails when the fix is absent, which is the check this repository's AGENTS.md asks for first.

4. The #635 coupling is real but benign, measured in both directions. The hand-off flags that three UNBANNERED entries name drift-guard.yml jobs that PR #635 holds. Built the merged state — #635's .github/workflows/ tree plus this PR's ci.yml and checker — and ran the gate:

exit=0

#635 adds steps to prose-lint without renaming it, and deletes suppression-guard.yml, which this checker skips by a rule derived from the file rather than a hardcoded list. So neither merge order breaks the other, and the two PRs touch disjoint files.

Not merged this sweep: CI is incomplete — gh pr checks 638 reports 11 pass, 28 pending. Left for the next sweep, per the pipeline rule that a pending PR is not polled.

Noted for the record: the lane volunteered a correction to its own ticket (#632's claim about the examples/ copies), labelled its own examples/ measurement as weak because the check never fired in either run, and declined to act on #580. That is the shape of a report worth trusting — which is why the one claim above was still checked.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit 85eebfd into master Sep 20, 2026
51 checks passed
Yaraslaut added a commit that referenced this pull request Sep 21, 2026
… and name option-coverage by its job id (fixes #624, fixes #637) (#647)

* ci: name the option-coverage job by its job id, not its script (fixes #637)

linux-all-features' MORPH_BUILD_BANK_GUI rationale referred the reader to
"the check-workflow-option-coverage job below". No job by that name exists
in .github/workflows/: the job is `option-coverage`, and
check_workflow_option_coverage.py is the script it runs. A reader who
searches ci.yml for the name the comment gives finds nothing, and the
sentence's whole purpose is to point at the standing guard.

Measured on c55ea5b, before the change:

    $ grep -n 'check-workflow-option-coverage' .github/workflows/ci.yml
    1838:          # fetch requires. The check-workflow-option-coverage job below is
    $ grep -rn '^  check-workflow-option-coverage:' .github/workflows/
    $ grep -n '^  option-coverage:' .github/workflows/ci.yml
    2449:  option-coverage:

The replacement names both -- the job id a reader can jump to, and the
script it runs -- so the next reader does not have to guess which of the
two the sentence meant.

Swept the rest of the tree for the same defect: over every `# ...` comment
line in .github/workflows/*.yml, the hyphenated lowercase tokens
immediately preceding the word "job" are automoc-include-lint,
scenario-coverage, ladder-tests, linux-all-features, linux-coverage,
option-coverage and dependency-free. All but the last resolve to a real job
id in some workflow; "dependency-free job" is an adjective, not a
reference. This was the only stale one.

Nothing gates such a reference -- #638's check_workflow_job_banners.py
pairs banners to jobs, and a job id inside a comment body is not a banner.
That residual is accepted here and filed separately rather than folded in:
closing it needs a new script plus the self-test this repository requires
of a gate, which is its own change.

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

* ci: generate the two self-included .moc headers before clang-tidy-diff (fixes #624)

examples/common/testkit/test_qml_surface.cpp and src/qt/forms/tests/
tst_main.cpp both end with `#include "<own-basename>.moc"` -- the AUTOMOC
idiom for a Q_OBJECT declared in a .cpp. AUTOMOC writes that header at
build time, the clang-tidy job configures and never builds, so every PR
that touches either file failed the job on a parse error before a single
changed line was analysed.

Reproduced locally on c55ea5b, this job's configure flags, clang-tidy
22.1.8, one line added to each of the two files:

    /.../examples/common/testkit/test_qml_surface.cpp:757:10: error:
      'test_qml_surface.moc' file not found [clang-diagnostic-error]
    /.../src/qt/forms/tests/tst_main.cpp:70:10: error:
      'tst_main.moc' file not found [clang-diagnostic-error]
    exit=1

## Why generating, and not suppressing

The issue offered two fixes. Suppressing the diagnostic turns out not to
be one of them: clang-tidy does not let a compiler error be filtered.
Measured on tst_main.cpp with the moc absent, all three still exit 1 on
the same line --

    --- baseline (no filter)              : exit=1  'tst_main.moc' file not found
    --- -checks=-clang-diagnostic-error   : exit=1  'tst_main.moc' file not found
    --- -checks=-clang-diagnostic-*       : exit=1  'tst_main.moc' file not found
    --- -warnings-as-errors= empty        : exit=1  'tst_main.moc' file not found

so "filter it" would have meant grepping clang-tidy-diff.py's output and
overriding its exit code -- morph#479's defect, a gate that cannot fail,
rebuilt on purpose. That is what the rejected option would have cost.

Generating them costs a partial build, measured locally with
USE_COMPILER_CACHE=OFF on 12 cores: 100 ninja edges, 67 compilations, 7
links, 9 moc runs, 31s. 39 of the 67 are the vendored Lightweight ORM,
pulled in because a <target>_autogen target depends on its target's link
dependencies. Building *every* autogen target instead was measured at 264
objects and 179s, so the step names the two targets it needs. Compiler
caching is still not wired into this job: 67 objects against the 703 a
real build of this configure would compile.

## The comment the issue was really about

ci.yml's "No Build step" paragraph already reasoned about which generated
files exist without a build, answered it for configure_file() output, and
read as though it had settled the question. It now says which kind it
covers, and points at the AUTOMOC step for the kind it does not. The
-Wno-missing-include-dirs comment made the matching over-broad claim --
that no source includes a generated header -- when what its guard covers
is an ascending `moc_<name>.h`, not a same-name `.moc`; corrected too.

## Verification

Measured locally on this commit's tree, from a clean configure-only build
directory (`rm -rf build/clang-debug`, configure, 0 .moc files on disk),
running the step body extracted from ci.yml rather than a retyped copy:

    === 2. NEGATIVE CONTROL: the step's own check, run before the build ===
    ::error::examples/common/testkit/test_qml_surface.cpp: test_qml_surface.moc was not generated -- ...
    ::error::src/qt/forms/tests/tst_main.cpp: tst_main.moc was not generated -- ...
    ok: 2 self-included .moc header(s), 2 missing
    check-before-build exit=1  (must be 1)

    === 3. VACUITY CONTROL: same check with the scan finding nothing ===
    ::error::no tracked source self-includes a .moc -- this step's scan has stopped detecting the idiom it exists for
    empty-scan exit=1  (must be 1)

    === 4. the step as written ===
    ok: examples/common/testkit/test_qml_surface.cpp -> test_qml_surface.moc
    ok: src/qt/forms/tests/tst_main.cpp -> tst_main.moc
    ok: 2 self-included .moc header(s), 0 missing
    step exit=0

A green clang-tidy-diff would prove nothing on its own, so the job was
made to report a finding on the changed line in each of the two files. A
deliberate `int laneProbeReachMarker() { return 0; }` inserted above each
`.moc` include, with the job's own clang-tidy-diff arguments:

    --- violation: clang-tidy-diff exit=1
    /.../src/qt/forms/tests/tst_main.cpp:69:5: error: function 'laneProbeReachMarker'
      can be made static or moved into an anonymous namespace [misc-use-internal-linkage,...]
    /.../examples/common/testkit/test_qml_surface.cpp:756:5: error: function 'laneProbeReachMarker'
      can be made static or moved into an anonymous namespace [misc-use-internal-linkage,...]
        clang-diagnostic-error count: 0

    --- harmless: clang-tidy-diff exit=0
        clang-diagnostic-error count: 0

Both translation units are now analysed to completion and the changed
line is reached; the same diff before this change produced two
clang-diagnostic-errors and no findings at all.

Not verified: any of this on a GitHub hosted runner, or against
aqtinstall's Qt ${QT_VERSION} rather than the local distro Qt 6.11.2 and
apt.llvm.org's clang-tidy-22 rather than local 22.1.8. The CI timings will
be worse than 31s on four cores.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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

Development

Successfully merging this pull request may close these issues.

1 participant