Research digest — CI scripts Round 26 audit (D.1 + D.2)¶
Date: 2026-05-31 ADR: ADR-0968 Scope: scripts/ci/assertion-density.sh (D.1) and scripts/release/concat-changelog-fragments.sh (D.2)
D.1 — assertion-density.sh silent skip after copyright rebrand¶
Root cause¶
assertion-density.sh at line 22 used a literal grep:
The copyright-rebrand decision (2026-05-27; memory: project_copyright_lusoris_only) changed the canonical copyright line for new fork-added files from Copyright YYYY Lusoris and Claude (Anthropic) to Copyright YYYY Lusoris. Any file written after the rebrand foundation PR lands would fail the literal match, causing the script to exit 0 at line 30 with:
This exit path bypasses the entire assertion-density gate — the script reports success without scanning any files, hiding any density violations in post-rebrand code.
Impact¶
- All post-rebrand fork-added
.c/.cppfiles excluded from the Power of 10 rule-5 assertion-density CI gate. - Pre-rebrand files (those already in the tree using the legacy format) continue to be scanned correctly.
- The bug is latent until the rebrand foundation PR merges; at that point it becomes immediately exploitable by any new PR that does not add
assert()calls.
Fix¶
Replace the single-literal grep with an extended-regex that accepts both formats:
Pattern analysis:
Lusoris and Claude— matches legacy headers (Copyright 2025 Lusoris and Claude (Anthropic))Copyright [0-9]+ Lusoris— matches new headers (Copyright 2026 Lusoris)- Netflix-only headers (
Copyright 2016-2024 Netflix, Inc.) match neither pattern and continue to be excluded correctly. - A hypothetical future format change (e.g. different year format) would need to extend the regex again; the
[0-9]+year match is intentionally broad to avoid this triggering again on a minor date-formatting change.
Verification¶
Six tests added in scripts/ci/tests/test-assertion-density.sh:
- T1: legacy header matched
- T2: new-format header matched (was the failing case before the fix)
- T3: Netflix-only header skipped
- T4: no-header file skipped
- T5: mixed legacy + new headers — both matched
- T6: Lusoris + Netflix mix — only Lusoris file matched
Test run: all 6 pass.
D.2 — concat-changelog-fragments.sh tempfile leak¶
Root cause¶
The --write path allocates two tempfiles immediately before the awk pipeline:
The original cleanup was a single rm -f "$tmp_body" at line 153, placed after the mv "$tmp_out" "$CHANGELOG" call. Under set -euo pipefail:
- If the awk pipeline fails (e.g. malformed input, missing binary), the script exits before the
rm -fline is reached — both$tmp_bodyand$tmp_outremain in/tmp. - If
mvfails (e.g. read-only filesystem, wrong permissions), same outcome — both files leak. - Even on a clean run,
$tmp_outwas never explicitly removed (only$tmp_bodywas cleaned up); themvatomically replacesCHANGELOG.mdbut the temp path ceases to exist after the rename, so the inode is cleaned up by the OS. However, amvfailure leaves$tmp_outbehind.
The POSIX semantics of trap ... EXIT guarantee the handler fires on any exit cause (normal, error via set -e, and most signals), making it the correct mechanism for unconditional tempfile cleanup.
Fix¶
The redundant post-mv rm -f "$tmp_body" is removed; the trap is the single cleanup point.
Verification¶
Four tests added in scripts/release/tests/test-concat-changelog-fragments.sh:
- T1: static check — EXIT trap present in script source
- T2: static check — only one
rm -f "$tmp_body"reference (trap only) - T3: dynamic check — awk failure via
PATHshim; verified no newtmp.*files in/tmpafter the script exits non-zero - T4: happy path —
--writerewritesCHANGELOG.mdcorrectly
Test run: all 5 assertions pass (T3 exercises two assertions).
No-digest-needed items¶
Both D.1 and D.2 are self-contained correctness fixes in fork-local CI scripts with no upstream Netflix counterpart. No architectural trade-offs beyond those documented in the ADR alternatives table. No new dependencies.