Skip to content

check-submodules: the release blocks a build tree that is not the pinned tree, and warns when there is nothing to compare (#2129) - #2177

Closed
bong-water-water-bong wants to merge 3 commits into
mainfrom
pkg/2129-submodules
Closed

bong-water-water-bong wants to merge 3 commits into
mainfrom
pkg/2129-submodules

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #2129 — not a close: the issue's own correction (the engine does not build the fork, mtmd cannot block an engine build, the pin is healthy) leaves its real content intact, and this is the check that content needed — plus the wiring that makes the release ask it.

The measurement

7 of 7 submodules on this box disagree with the commit the tree pins:

submodule pin checkout dirty
third_party/hrx-system 7f4236924 51b1739ae 0
third_party/llama.cpp 2293f95ea c16424f56 301 ← the issue's "301 files short of its own HEAD"
third_party/xdna-driver 526c60819 5d302c998 32
third_party/laya b8a0f0991 593a1596f 4
third_party/tokenizers 301277072 88a4498ad 20
third_party/lemonade 248f818b4 593a1596f 4
third_party/ggmlc df08d1b06 593a1596f 4

Why CI could not have caught it

CI checks the pins — "submodule pins only move forward", "hrx-system gitlink agrees with the hold file" — and those are checks on the repository. A build reads the tree on the box. "The pin is healthy" and "the checkout is the pin" are two claims, and nothing made the second one. That is how a tree can build 301 uncommitted files and report success.

What this adds

scripts/check-submodules.sh — HEAD against the gitlink and the worktree against HEAD, with four honest states:

exit meaning preflight
0 every checkout is its pin and clean passes
1 a checkout exists and disagrees blocks the run
3 submodules not checked out (a fresh clone) warns — a fresh box must still build
2 no .gitmodules (cannot happen here) warns

Dirty fails even at the right pin: a worktree differing from its HEAD builds sources the pin does not name, the same defect with a smaller diff.

The self-test earned its keep immediately by catching a real bug in my first version: git -C inside an unchecked-out submodule directory is not an error — it walks up and reports the parent's HEAD, so a fresh clone was reported as "checked out at the wrong commit". A checkout now has to be its own repo root (--show-toplevel equal to the path) before its HEAD means anything.

Wiring: the release now asks the question

scripts/weekly.sh's preflight runs it for the bumps/build stages, through preflight_submodules() — a function of its own so it is testable against stubs, exactly like preflight_runner_units (#1504). Exit 1 stops the run before the first stage; 2/3 are warnings that say the check inspected nothing rather than implying it passed (the exit-3 discipline of #2094).

Verification

scripts/check-submodules.sh --self-test      7/7 arms
tests/weekly_submodule_preflight_test.sh     7/7 rows (function cut out of weekly.sh, stub checkers)
tests_referenced_check.py                    83/83 test scripts referenced
ci_targets_check.py --where submodules_self            -> cmake/tests-submodules.cmake:13
ci_targets_check.py --where weekly_submodule_preflight -> cmake/tests-submodules.cmake:18

No CMakeLists.txt line: the glob at :1979 registers the family file — the mechanism #1979 wants, at a moment when 19 open PRs are editing that file.

Not closing #2129: this reports the drift and stops a build from ignoring it; repairing the seven checkouts is shared fleet state other lanes may be building from right now, so it stays with the owner.

…g checked the checkout (engine#2129)

#2129 corrected its own title: mtmd cannot block an engine build, the engine does not build the fork, and the pin itself is
healthy. What it did establish is narrower and worse, and this adds the check that would have said so.

Measured on this box while triaging it -- 7 of 7 submodules disagreed with the commit the tree pins:

  third_party/hrx-system   pin 7f4236924  head 51b1739ae  dirty 0
  third_party/llama.cpp    pin 2293f95ea  head c16424f56  dirty 301    <- the issue's "301 files short of its own HEAD"
  third_party/xdna-driver  pin 526c60819  head 5d302c998  dirty 32
  third_party/laya         pin b8a0f0991  head 593a159  dirty 4
  third_party/tokenizers   pin 301277072  head 88a4498ad  dirty 20
  third_party/lemonade     pin 248f818b4  head 593a159  dirty 4
  third_party/ggmlc        pin df08d1b06  head 593a159  dirty 4

CI checks the PINS -- "submodule pins only move forward", "hrx-system gitlink agrees with the hold file" -- and those are checks
on the repository. A build reads the tree on the box. "The pin is healthy" and "the checkout is the pin" are two different
claims and nothing made the second one, which is how a tree can build 301 uncommitted files and report success.

scripts/check-submodules.sh checks the second claim: per submodule, HEAD against the gitlink, and the worktree against HEAD.
Dirty is a failure even when HEAD matches -- a checkout whose worktree differs builds sources the pin does not name, the same
defect with a smaller diff -- and it is reported with its count so a reader can tell a stray file from a reset. Exit 0 clean,
1 on any disagreement, 2 when there is nothing to inspect: an unchecked tree is the state this exists to end, so "no
.gitmodules" is not a pass.

The ctest row is the --self-test only, and it is hermetic: two throwaway local repos and a real `submodule add` over
file protocol, so no network, no submodule of this tree, no build. The six arms are a clean checkout at its pin passing, a
checkout ahead of its pin failing and naming both commits, a dirty checkout at the right pin failing and saying the worktree
differs, and no .gitmodules exiting 2 rather than passing.

The --root mode is deliberately NOT a ctest row: CI checks out at the pin by construction, so as a row it would test the
runner, not the tree. It is for the box, before a build:

    scripts/check-submodules.sh --root .     ->  1, naming all seven, today

Registered as cmake/tests-submodules.cmake, a NEW file, so this touches no line of CMakeLists.txt -- the glob at CMakeLists.txt:1979
picks it up, which is the mechanism engine#1979 wants and why this PR has no rebase cost at 19 open PRs on that file.
`ci_targets_check.py --where submodules_self` -> cmake/tests-submodules.cmake:13.
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 7d898a4)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

2129 - Partially compliant

Compliant requirements:

  • The PR implements a check that verifies submodule checkouts match their pinned commits and are clean
  • The check exits with appropriate codes (0, 1, 3, 2) as specified
  • The check is integrated into the release process via preflight checks
  • The check is self-testing and includes a test script

Non-compliant requirements:

Requires further human verification:

  • None

1504 - Partially compliant

Compliant requirements:

  • The PR does not implement the systemd unit changes required for restart policy
  • The PR does not implement monitoring for runner status

Non-compliant requirements:

  • The PR does not address the core issue of runners dying on OOM kills and not restarting
  • The PR does not implement monitoring to detect dead runners

Requires further human verification:

  • The systemd unit changes and monitoring implementation are outside the scope of this PR and require manual intervention

1979 - Partially compliant

Compliant requirements:

  • The PR does not implement a structural solution to prevent CMake registration hunks from going stale

Non-compliant requirements:

  • The PR does not address the core issue of CMake registration hunks going stale

Requires further human verification:

  • The structural solution for preventing CMake registration conflicts is outside the scope of this PR
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Submodule Integrity Check

The script correctly checks that each submodule's checkout matches its pinned commit and is clean, which is critical for ensuring builds use the intended versions of dependencies. However, it does not handle the case where a submodule is not initialized (i.e., not checked out) in a way that would cause a build to fail. While it warns about such cases, it should ideally block the build if a submodule is not checked out, as this could lead to an inconsistent build state.

if [ -z "$head" ]; then
    echo "::warning::check-submodules: $path is not checked out (the tree pins ${pin:0:9}); a build would not find its sources"
    missing=$((missing + 1)); continue
fi
Exit Code Handling

The script exits with code 3 when submodules are not checked out, which only warns but does not block the build. This is inconsistent with the requirement that builds should not proceed if the tree is not at the pinned commit. The script should exit with code 1 in such cases to block the build, as a fresh clone should not be allowed to proceed without explicit initialization.

if [ -z "$head" ]; then
    echo "::warning::check-submodules: $path is not checked out (the tree pins ${pin:0:9}); a build would not find its sources"
    missing=$((missing + 1)); continue
fi
✅ Resolved findings

scripts/check-submodules.sh:1-155

Submodule Integrity Check

The script checks that each submodule's checkout matches its pinned commit and is clean, which is critical for ensuring that builds use the intended versions of dependencies. This is especially important for the third_party/llama.cpp submodule, which was found to be 301 files short of its own HEAD, potentially causing different prompt rendering behavior. The check correctly exits with code 1 when a checkout disagrees with the pin, blocking the build, and exits with code 3 when submodules are not checked out, only warning but not blocking.

scripts/weekly.sh:669-682

Preflight Integration

The preflight function preflight_submodules is integrated into the weekly release script to ensure that the build tree is at the pinned commit before proceeding. It correctly handles different exit codes from the submodule check, blocking the build when submodules are not at the pinned commit and only warning when submodules are not checked out.

scripts/check-submodules.sh:1-133

Submodule Integrity Check

The script checks that each submodule's checkout matches its pinned commit and is clean, which is critical for ensuring that builds use the intended versions of dependencies. This is especially important for the third_party/llama.cpp submodule, which was found to be 301 files short of its own HEAD, potentially causing different prompt rendering behavior.

cmake/tests-submodules.cmake:1-13

CI Integration

The new CMake test file integrates the submodule integrity check into CI. However, it's important to note that this check only verifies the checkout state and does not address the core issue of CMake registration hunks going stale, which is a separate concern raised in issue #1979.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT at 5f6c830: CHANGES

Reviewed head 5f6c830 (refs #2129). This is the first verdict.

CI is red on this PR's own file. build's failed step is "Copyright notice on every file": missing copyright notice: cmake/tests-submodules.cmake. The new family file needs the full Apache notice, like cmake/tests-submodule-pin.cmake.

This is a second checker for what #2141 already merged. tools/ci/submodule_pin_check.py (#2141, on main, registered from cmake/tests-submodule-pin.cmake) already fails when a submodule's checkout is not the pinned commit. It reads git submodule status --recursive and has --require-initialised for the release form. I reviewed it on this box: it caught my own checkout's third_party/llama.cpp off its pin. So the "checkout ahead of its pin" row of the table here is #2141's.

What is genuinely new here is the dirty worktree at the right pin. git submodule status cannot see it (+ is a HEAD mismatch only), and the 301 modified files of #2129 are exactly that shape. That is worth having. But as a second tool, in shell, with its own exit codes (0/1/2, while #2141 uses 0/1/2 with different meanings for "nothing to inspect"), the two will drift.

Suggested: add the dirty check to submodule_pin_check.py. For each initialised submodule, git -C <path> status --porcelain non-empty fails, with the count, as here. Keep this PR's hermetic fixture (two local repos, a real submodule add over file protocol) as that checker's self-test arm. Then there is one place that answers "does the build's tree match the pin".

…tree, and only warns when there is nothing to compare (engine#2129)

The script in the previous commit answers a question nobody was asking yet. This makes the release ask it: the `bumps` and
`build` stages build the tree in front of them, so `preflight` runs the check there and stops the run on a checkout that
disagrees with the pin -- the build would not be of the pinned sources, and until now that was silent. Exit 3 (submodules not
checked out at all, which is what a fresh clone looks like) only WARNS, because a fresh box must still be able to build; the
warning says the check inspected nothing rather than implying it passed, which is the exit-3 discipline of engine#2094.

`check-submodules.sh` therefore grew that third state, and its self-test earned its keep immediately by catching a real bug in
the first version: `git -C` inside an unchecked-out submodule directory is not an error, it walks UP and reports the PARENT's
HEAD -- so a fresh clone was being reported as "checked out at the wrong commit". A checkout now has to be its own repository
root (`rev-parse --show-toplevel` equal to the path) before its HEAD means anything.

  states: 0 every checkout is the pin and clean | 1 a checkout disagrees (blocks) | 3 nothing checked out (warns) | 2 no
  .gitmodules (warns, cannot happen here but "cannot tell" is the honest answer)

Tests, all through the real code:

  scripts/check-submodules.sh --self-test     7/7 arms (clean at pin, ahead of pin naming both commits, dirty at the right
                                              pin, fresh clone = 3, no .gitmodules = 2, and unrelated repo ignored)
  tests/weekly_submodule_preflight_test.sh    7/7 rows, the function cut out of weekly.sh the way
                                              tests/weekly_runner_preflight_test.sh cuts preflight_runner_units, run
                                              against stub checkers: a disagreeing tree returns 1, exit 2 and 3 return 0
                                              with a WARNING, a missing checker returns 0

`tests_referenced_check.py`: 83/83 test scripts referenced, both rows resolve --
`ci_targets_check.py --where submodules_self` -> cmake/tests-submodules.cmake:13, `--where weekly_submodule_preflight` -> :18.
Still no CMakeLists.txt line: the glob at CMakeLists.txt:1979 registers the family file, which is what engine#1979 wants.

Measured on this box, which is why this is worth blocking on: 7 of 7 submodules disagree with the pin (llama.cpp 2293f95ea vs
c16424f56, 301 files dirty). Repairing those checkouts is fleet state and stays with the owner; this is the part that makes the
next build say so instead of building 301 uncommitted files and reporting success.
@bong-water-water-bong bong-water-water-bong changed the title check-submodules: a build reads the checkout, not the pin, and nothing checked the checkout (#2129) check-submodules: the release blocks a build tree that is not the pinned tree, and warns when there is nothing to compare (#2129) Nov 11, 2026
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 4791221

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT at 4791221: CHANGES

Re-review of 4791221 (one commit, force-pushed over 5f6c830). Both points from my CHANGES at 5f6c830 are still open.

1. CI build is red, for the same cause as before. The Copyright notice on every file step fails with missing copyright notice: cmake/tests-submodules.cmake. The file carries only the two SPDX lines. tools/copyright.py --check requires the full Apache notice, which is lines 2-15 in every other cmake/tests-*.cmake. python3 tools/copyright.py --fix writes it.

2. This duplicates tools/ci/submodule_pin_check.py (#2141, already on main). That checker answers "is each checkout its pin" from git submodule status. The new 155-line script answers the same question again. The one thing it adds is the dirty-worktree check (a checkout at the right commit with uncommitted files), and that part is useful. Please add it to the existing checker, and have weekly.sh's preflight call that one, so the box has a single answer to the question. Otherwise, say in the body why two checkers are needed. Right now the body does not mention #2141.

What works at this head (run from a git archive of 4791221):

  • scripts/check-submodules.sh --self-test: ok, including the fix where --show-toplevel must equal the path for an unchecked-out directory.
  • tests/weekly_submodule_preflight_test.sh: ok (exit 1 blocks, exit 2/3 warn).
  • The family file is picked up by the cmake/tests-*.cmake glob (CMakeLists.txt:1979), so CI does register both rows.

…wo-liner

CI's "Copyright notice on every file" step failed on this PR: tools/copyright.py only accepts the SPDX pair in a file that
also carries the license block, and a .cmake file starts with the block. Reproduced locally in a worktree (the checker needs
a real index, so it cannot be run from an archive) and fixed with the repository's own `--fix`.

Worth recording, because it is a trap rather than a mistake: `--fix` prepends the block and LEAVES an existing two-liner, so
the file it just fixed fails its own `--check` with "duplicated inside the header window". Neither mode says that the state
it produced is invalid; only a second `--check` does. The duplicate was removed by hand here; a follow-up can make `--fix`
drop an in-window notice instead of adding a second one.

Local, on the tree that is pushed: copyright --check clean, shellcheck -S warning clean on both new shell files,
tests_referenced_check 83/83.
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 7d898a4

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review at 7d898a4 — no verdict token (author's lane). I ran both tests rather than reading them, and the design decision that matters here — "cannot tell" is a third state, not a pass — is implemented all the way through.

Ran:

check-submodules.sh --self-test                     -> ok
  ok   a dirty checkout fails even when HEAD is the pin
  ok   and it says the worktree differs from its HEAD
  ok   an unchecked-out submodule is exit 3 (cannot tell), not 1 and not 0
  ok   no .gitmodules is 'cannot tell' (2), not a pass
weekly_submodule_preflight_test.sh                  -> ok
  ok   no .gitmodules only warns -> return 0
  ok   exit 2 is not a block      ok   exit 3 is not a block
  • The dirty arm is the one that earns this check. "A dirty checkout fails even when HEAD is the pin" is precisely the llama.cpp row in your table (pin 2293f95ea, 301 dirty files): HEAD matching would have declared that tree pinned, and the build would have compiled 301 files the pin does not contain. (That hazard is live in the shared clone too — ~/engine carries ~220 lines of another lane's uncommitted work in npu/pack_import*, so this check would catch it there as well.)
  • The tri-state is real, not documented: 0 pass, 1 block, 2 no-.gitmodules/no-git ("nothing to inspect, which is not a pass"), 3 nothing-to-compare. And the preflight honours it in the direction that matters — it blocks only on 1 and lets 2/3 through as a warning, so a can't-tell week does not fake a red or a green.
  • The warning text is honest. "WARNING, not blocking: no checkout to compare against the pin, so the build follows the tree rather than this check" — it says the build will not be of the pinned sources, which is the sentence that stops a release note claiming otherwise.
  • Registered through the glob-included family file (cmake/tests-submodules.cmake: submodules_self, weekly_submodule_preflight), which is the mechanism that keeps a test from being written and never run.

One gap, in the can't-tell path only. When the preflight warns (exit 2/3), the release proceeds and the build is of some tree — but nothing records which. The check's own table shows 7/7 submodules disagreeing on this box, so "which tree was built" is exactly the question a release artifact should answer after the fact. Since the checker already computes pin/head/dirty per submodule, writing that table into the build log (or beside the artifact) on the warn path would make a can't-tell release auditable: "built c16424f56 + 301 dirty, pin 2293f95ea" instead of "build followed the tree". One echo of a summary line, on the path that already exists.

Nothing else: the block path is right, the wiring into weekly.sh accumulates into its failure rather than swallowing it, and the test stubs the checker so it cannot pass by accident.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT at 7d898a4: CHANGES

Re-review of 7d898a4. It adds 7d898a4 (the family file now carries the full Apache header).

Fixed: tools/copyright.py --check gives rc 0 at this head, and CI build is a success.

Still open, and now measured: this is a second checker for a question main already answers. On the same working tree (my review checkout, where third_party/llama.cpp sits at 7cc3187b5 against a pin of 204493d40):

rc finding
main's tools/ci/submodule_pin_check.py --check --root . (#2141) 1 third_party/llama.cpp is checked out at 7cc3187b5f4e, which is NOT the commit this tree pins; the uninitialised ones are "not compared"
this PR's scripts/check-submodules.sh --root . 1 third_party/llama.cpp is not the pin: tree 204493d40, checkout 7cc3187b5; the uninitialised ones are warnings

Same verdict, same submodule, same three states (pass, mismatch fail, "not initialised, not compared"), and both cite #2129. What this PR adds that main does not have:

  • (a) failing a checkout that is at the pin but has a dirty worktree;
  • (b) the weekly.sh preflight that calls it.

Both are worth having. Taking them as a second 155-line script means two implementations that will drift. Please put (a) into submodule_pin_check.py (one git -C <path> status --porcelain per initialised submodule, plus a self-test arm), and have preflight_submodules() call python3 tools/ci/submodule_pin_check.py --check --root "$ENGINE_DIR". If there is a reason the box needs a separate shell checker, say it in the body. Neither earlier review got a reply on this point.

Works at this head (from a git archive of 7d898a4): check-submodules.sh --self-test ok, and weekly_submodule_preflight_test.sh ok.

bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…e is already a two-liner

Met while fixing a new file's header in #2177, and it is the kind of defect this tool exists to prevent, in the tool:

    $ printf '# Copyright 2026 bong-water-bong\n# SPDX-License-Identifier: Apache-2.0\n\nx = 1\n' > twoliner.py
    $ python3 tools/copyright.py --check
    run: python3 tools/copyright.py --fix
    missing copyright notice: twoliner.py
    $ python3 tools/copyright.py --fix
    added: twoliner.py
    $ python3 tools/copyright.py --check
    run: python3 tools/copyright.py --fix
    copyright notice duplicated inside the header window (delete the second copy): twoliner.py

`--fix` reported success and left an invalid file. Neither mode says the state it produced is invalid; only a second
`--check` does, and the instruction it then prints is the one thing `--fix` cannot do.

The cause is a gap between two predicates that each look right alone. `has_notice()` wants the licence block, so a file
carrying only the SPDX pair is "missing". `near_miss_holder()` filters what `add_notice()` drops before prepending the
block, and it returns False for an *exact* holder **by design** -- an exact holder beside the canonical block is the
*repaired* state (engine#1606). So the exact pair survived the prepend and became the second copy. The pair is not a second
claim: the block `add_notice()` inserts contains both lines.

Now `add_notice()` drops an in-window line that IS one of the canonical notice lines, so the merge is a merge.

Two arms in tests/copyright_gate_test.sh, and they bite: against main's `tools/copyright.py` the second one fails with
"window: 2 SPDX line(s)", which is exactly the duplicate. The first arm alone passes on old code -- the second `--check`
there still finds a duplicate, so what actually fails to be one notice is that count, and that is what is asserted.

Verified: tests/copyright_gate_test.sh PASS with the patch, FAIL (2 SPDX lines) without it; `tools/copyright.py --check`
clean on this tree.


The first push of this branch also carried four files staged for #2177's header check -- `git add` is needed for
`tools/copyright.py` to see a new file, and the following `git commit` then took the whole index. Rebuilt from main with the
two files this PR is about.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Withdrawn — superseded by #2202, and by main. tools/ci/submodule_pin_check.py (another lane's, registered in cmake/tests-submodule-pin.cmake) already checks the first half of this: every submodule's checkout against the commit the tree pins, recursively, in Python, as a ctest row. I did not find it before writing a shell script for the same property — the "search open PRs and main for an existing fix" rule, broken by me rather than by someone else.

What was genuinely additive is in #2202: (1) the state git submodule status cannot report — a submodule at its pin with a dirty work tree, which is 301 uncommitted paths in llama.cpp on the box that motivated #2129 and reads as "at their pinned commit" today; (2) the preflight wiring, because a ctest row runs in CI and the drift was measured on the box.

Three things from this PR worth keeping in the record even though the code goes:

  • The self-test caught a real bug: git -C <unchecked-out submodule dir> rev-parse HEAD does not fail, it walks up and reports the parent's HEAD, so a fresh clone read as "checked out at the wrong commit". A checkout has to be its own repo root before its HEAD means anything. The merged Python tool gets this right by construction (it parses git submodule status, where uninitialised is '-').
  • Measured on this box: 7 of 7 submodules off-pin, not the one the issue reported — hrx-system, llama.cpp (301 dirty), xdna-driver, laya, tokenizers, lemonade, ggmlc.
  • Repairing those checkouts is shared fleet state and stays with the owner: several lanes build from that tree, and reset --hard + clean -fd there would delete whatever those 301 files are.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

ORCHESTRATOR decision (posted by cl-review on the orchestrator's instruction): there is one submodule checker, tools/ci/submodule_pin_check.py from #2141. #2177 stays closed. A case it covered that #2141 lacks moves into #2141's checker. The dirty-worktree case (a checkout at its pin with uncommitted changes) is that case, and #2202 is where it lands. Nothing else from scripts/check-submodules.sh is carried over.

bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…e is already a two-liner (#2192)

* copyright: --fix leaves a file its own --check rejects when the notice is already a two-liner

Met while fixing a new file's header in #2177, and it is the kind of defect this tool exists to prevent, in the tool:

    $ printf '# Copyright 2026 bong-water-bong\n# SPDX-License-Identifier: Apache-2.0\n\nx = 1\n' > twoliner.py
    $ python3 tools/copyright.py --check
    run: python3 tools/copyright.py --fix
    missing copyright notice: twoliner.py
    $ python3 tools/copyright.py --fix
    added: twoliner.py
    $ python3 tools/copyright.py --check
    run: python3 tools/copyright.py --fix
    copyright notice duplicated inside the header window (delete the second copy): twoliner.py

`--fix` reported success and left an invalid file. Neither mode says the state it produced is invalid; only a second
`--check` does, and the instruction it then prints is the one thing `--fix` cannot do.

The cause is a gap between two predicates that each look right alone. `has_notice()` wants the licence block, so a file
carrying only the SPDX pair is "missing". `near_miss_holder()` filters what `add_notice()` drops before prepending the
block, and it returns False for an *exact* holder **by design** -- an exact holder beside the canonical block is the
*repaired* state (engine#1606). So the exact pair survived the prepend and became the second copy. The pair is not a second
claim: the block `add_notice()` inserts contains both lines.

Now `add_notice()` drops an in-window line that IS one of the canonical notice lines, so the merge is a merge.

Two arms in tests/copyright_gate_test.sh, and they bite: against main's `tools/copyright.py` the second one fails with
"window: 2 SPDX line(s)", which is exactly the duplicate. The first arm alone passes on old code -- the second `--check`
there still finds a duplicate, so what actually fails to be one notice is that count, and that is what is asserted.

Verified: tests/copyright_gate_test.sh PASS with the patch, FAIL (2 SPDX lines) without it; `tools/copyright.py --check`
clean on this tree.


The first push of this branch also carried four files staged for #2177's header check -- `git add` is needed for
`tools/copyright.py` to see a new file, and the following `git commit` then took the whole index. Rebuilt from main with the
two files this PR is about.

* copyright: --fix also repairs the block pasted twice, which it used to report as `repaired:` and leave (engine#2192)

The two-liner case in the previous commit was one of two states in which `--fix` printed success and left a file its own
`--check` rejects. The other is the block pasted twice:

    $ python3 copyright.py --check
    copyright notice duplicated inside the header window (delete the second copy): doubled.py
    $ python3 copyright.py --fix
    repaired: doubled.py
    $ python3 copyright.py --check
    copyright notice duplicated inside the header window (delete the second copy): doubled.py

`repair_notice()` is the path `--fix` takes for a file that already has the canonical notice, and it existed for the
near-miss-beside-canonical shape (engine#1606): it drops a typo'd holder and the SPDX line beneath it, keeps everything
else, and falls back to `add_notice()` when the result no longer has a notice. A *second canonical* notice is neither of
those things, so it passed through untouched -- and `has_notice()` was still true, so the fallback never ran either.

It now keeps the first occurrence of each canonical notice line and drops later copies, which is what `--check`'s own
instruction ("delete the second copy") asks for and what the word `repaired:` has always claimed. Dual attribution is
untouched: `Copyright 2026 The 1bit-MONSTER Authors.` is not a canonical line, so it survives beside the block exactly as
before.

Two arms, and they bite: against the previous `repair_notice()` the SPDX count in the window is 2 and the arm fails
("...and exactly one SPDX line survives the repair"), so the whole gate test is FAIL without the fix and PASS with it. The
first arm (that `--check` stops reporting a duplicate) passes on old code, which is worth knowing: it means the *count* is
the assertion that has teeth here, not the absence of the word "duplicated" -- the same asymmetry as the two-liner case in
the previous commit.

---------

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…s 'at their pinned commit' (#2129) (#2202)

* submodule_pin: a submodule at its pin with a dirty work tree passes as "at their pinned commit"

engine#2129 is already fixed on main by tools/ci/submodule_pin_check.py -- another lane's, registered in
cmake/tests-submodule-pin.cmake as a ctest row -- and it reads `git submodule status`, which reports four states. This adds
the fifth, which that command cannot report, and the wiring that runs the check before a build instead of only in CI.

**The missing state: at the pin, not clean.** `git submodule status` prints ' ' for "the checkout IS the pinned commit"
whatever the submodule's own work tree holds, and the build compiles that work tree. Measured on the box that motivated
#2129: on top of the checkouts being off-pin, `third_party/llama.cpp` had 301 uncommitted paths, `xdna-driver` 32,
`tokenizers` 20, and four others 4 each. Every one of those would read as "at their pinned commit" to the existing check.

Reported always, with its count, and a failure only under the new `--require-clean`: a deliberate local patch in a
submodule is a real thing a box does, and moving the pin is not always the caller's to do, so the default is to say it
rather than to stop. Five self-test arms, including that a dirty '+' row is not double-counted (the commit is the failure
there).

**The wiring: nothing ran it before a build.** A ctest row runs in CI; the drift was measured on the box, where the release
and the weekly build actually run. `scripts/weekly.sh`'s preflight now runs the check for the `bumps` and `build` stages
through `preflight_submodules()` (a function of its own, testable against stub checkers like `preflight_runner_units`):
exit 1 **blocks** the run -- the build would not be of the pinned sources -- and exit 2 (no submodule initialised, i.e. a
fresh clone) only **warns**, saying that the check inspected nothing rather than implying a pass.

    tools/ci/submodule_pin_check.py --self-test        19 arms, 5 of them new
    tests/weekly_submodule_preflight_test.sh           6/6, the function cut out of weekly.sh, stub checkers
    tools/copyright.py --check                         clean
    tools/ci/tests_referenced_check.py                 83/83 referenced

`--require-clean` is deliberately not passed in the preflight: a dirty submodule is reported there and does not stop a
build, while a wrong commit does.

This supersedes #2177, which grew a parallel `scripts/check-submodules.sh` before main's tool landed. Two checks for one
property is the failure mode the standing rules name, so that script is withdrawn rather than maintained beside it.

* weekly: the submodule gate runs in build(), not preflight() -- where it was a no-op that reported success

Found by checking my own wiring against where the variables are set rather than against whether the tests pass. `SRC=$W/src`
is created by `checkout`, which the `bumps` and `build` stages call, and `preflight()` runs before any stage -- so
`[ -f "$SRC/tools/ci/submodule_pin_check.py" ]` was false on every real run, the function returned 0, and the gate inspected
nothing while reporting success. A preflight row that cannot see the tree it is meant to inspect is worse than no row: it
reads as verified.

The gate now runs at the top of `build()`, immediately after `checkout` has updated `$SRC`'s submodules and before the first
compile -- the tree that is about to be compiled, stopped before the compile rather than after it. `build()` returning
non-zero fails the stage.

And the missing-checker path now says so. An older ref does not carry the tool, so that build's submodule drift is not
checked; it prints a WARNING naming the ref's reason instead of returning 0 silently. That was the same defect one level up:
the function's first version treated "no file to run" as "fine", which is the reporting shape this whole check exists to
remove.

    tests/weekly_submodule_preflight_test.sh   6/6, including that a missing checker warns rather than passing quietly
    tools/ci/submodule_pin_check.py --self-test  19 arms
    tools/copyright.py --check                  clean

* weekly_submodule_preflight_test: pin WHERE the gate runs, not merely that it exists (engine#2129)

The previous commit moved the submodule gate from preflight() to build() because in preflight() it was a no-op that reported
success: preflight runs before `checkout` creates $SRC, so the checker file was not there, the function returned 0, and a green
run meant nothing had been inspected.

The test it shipped with could not catch that regression. It asserted

    grep -q 'preflight_submodules "' scripts/weekly.sh

-- "present anywhere in the file", which is true in exactly the broken state. The test and the bug agreed.

It now extracts the two functions and asserts the call is inside build() and NOT inside preflight(), so moving it back fails
with the reason rather than passing quietly:

    FAIL scripts/weekly.sh's build() no longer runs the submodule gate (a call elsewhere is the no-op this test exists for)
    FAIL the submodule gate is inside preflight(), which runs before $SRC exists: it would inspect nothing and report success

Verified by making that move: with the call restored to preflight() both assertions fail and the row exits 1; with the call in
build() the row passes. That is the negative control this test never had.

---------

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
@bong-water-water-bong
bong-water-water-bong deleted the pkg/2129-submodules branch November 12, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant