mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* ci: add static pre-push checks — include_str/Docker-COPY + hermetic env (#6018) Categorized main-branch CI history showed the deterministic (non-flaky) breakages share one trait: a cheap static check would catch them, but the pre-push gate ran none. Adds three checks mapped to the failure classes. 1. include_str! path + Docker-COPY coverage (scripts/ci/check-include-str-paths.sh) Every include_str!("…") target must exist AND be present in the build context of each Dockerfile that compiles the referencing crate. Guards the #5603 Docker outage class (host build passes, Docker build fails because a repo-root prompts/ dir was never COPYd). Excludes #[cfg(test)] includes and COPY . . images; attributes per-Dockerfile so Dockerfile.reborn is never blamed for a src/ prompt it doesn't compile. This surfaced a real latent bug: Dockerfile.test builds --bin ironclaw but omitted COPY prompts/ / profiles/ / providers.json (all required by prod consts) — fixed here. 2. Hermetic env guard (scripts/ci/check-hermetic-env.sh) Delta + function-scoped (git diff -W): flags only newly-added raw std::env::set_var/remove_var whose enclosing function lacks an env lock guard (lock_env/lock_runtime_env/ENV_MUTEX/EnvGuard). Targets the #6015 coverage-flake class and Rust 1.82 set_var UB. Quiet on the ~700 existing sites; honors // env-hermetic: for genuine single-threaded cases. 3. libsql-only clippy leg Added to the pre-push strict branch and quality_gate_strict.sh. Catches the cfg/dead_code class (#5840 Prebuilt) that default-feature clippy misses. Wiring: checks 1 & 2 run on every pre-push (no compile); check 3 under IRONCLAW_STRICT_LINT. New required code_style.yml `static-checks` job runs check 1 + both self-test suites (14 cases) server-side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: harden static pre-push checks per PR #6022 review Addresses review findings on the include_str/Docker-COPY and hermetic-env static checks. Each fix ships with a regression case in the self-tests. check-include-str-paths.sh: - cfg_test_spans: stop a braceless `#[cfg(test)]` item (e.g. `use x;`) at the first `;` instead of running forward to the next unrelated `{`, which swallowed real code and hid its include_str! calls (false negative). - Track full normalized COPY paths and add an `is_covered` nested-path check so a narrowed `COPY crates/foo/` covers crates/foo but not sibling crates (previously top-segment matching treated all of crates/ as copied). - Scan the repo-root build.rs (cargo build compiles it too) and flag include_str! targets that resolve outside the repo — they exist on the host but no Docker COPY can ever include them. check-hermetic-env.sh: - Prefer GITHUB_BASE_REF for base-ref resolution so the check works on a shallow CI checkout that lacks origin/main. - Drop `|| true` on the git diff so a diff failure aborts instead of silently yielding an empty diff that bypasses the guard. - Strip `//` comments before the guard-token test so a bare `// EnvGuard` comment no longer exempts an adjacent raw set_var. code_style.yml: - Broaden the has_code predicate to cover scripts/ci/, .githooks/, and all Dockerfile* so changes to the guardrails themselves run static-checks instead of being skipped (and accepted by the rollup). - Run check-hermetic-env.sh against the actual PR diff, not just the synthetic self-tests, fetching the PR base tip first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
255 lines
9.7 KiB
Bash
Executable File
255 lines
9.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Static check: every `include_str!("...")` target must (A) exist on disk and
|
|
# (B) be present in the Docker build context of any Dockerfile that compiles
|
|
# the referencing crate.
|
|
#
|
|
# Motivation: issue #5603 / the 2026-07-03 Docker outage. Two new
|
|
# `include_str!("../../prompts/*.md")` call sites were added in `src/`, but the
|
|
# repo-root `prompts/` directory was not `COPY`d into the Dockerfile builder
|
|
# stage. The host build passed (the files exist in the repo) while every Docker
|
|
# build failed for ~15 consecutive runs with:
|
|
# error: couldn't read `src/hooks/../../prompts/session_summary.md`
|
|
# A host-only compile can never catch this class; a static context check can,
|
|
# in milliseconds, with no compile.
|
|
#
|
|
# Runs standalone (`bash scripts/ci/check-include-str-paths.sh`) and from the
|
|
# pre-push hook. Whole-repo scan (not delta) — it is cheap and the invariant is
|
|
# global.
|
|
|
|
# Root to scan. Defaults to the git top-level; tests pass an explicit tree.
|
|
REPO_ROOT="${1:-$(git rev-parse --show-toplevel)}"
|
|
|
|
python3 - "$REPO_ROOT" <<'PY'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
repo = Path(sys.argv[1]).resolve()
|
|
|
|
INCLUDE_RE = re.compile(r'include_str!\(\s*"([^"]+)"\s*\)')
|
|
CFG_TEST_RE = re.compile(r'#\[cfg\(test\)\]')
|
|
# `COPY <src>... <dest>` but not `COPY --from=<stage> ...` (those pull from a
|
|
# previous build stage, not the local context).
|
|
COPY_RE = re.compile(r'^\s*COPY\s+(?!--from=)(.+?)\s*$', re.IGNORECASE)
|
|
CARGO_BUILD_RE = re.compile(r'cargo\s+build')
|
|
|
|
|
|
def top_segment(rel: str) -> str:
|
|
parts = Path(rel).parts
|
|
return parts[0] if parts else ""
|
|
|
|
|
|
def is_covered(rel, roots):
|
|
"""True if `rel` (a repo-relative path) is at or under any COPY root. A
|
|
root is the normalized copied path (e.g. `crates/foo` from
|
|
`COPY crates/foo/ crates/foo/`), so a narrowed crate copy covers that
|
|
crate but not its siblings."""
|
|
p = Path(rel)
|
|
while True:
|
|
if str(p) in roots:
|
|
return True
|
|
if p.parent == p:
|
|
return False
|
|
p = p.parent
|
|
|
|
|
|
def cfg_test_spans(lines):
|
|
"""Return a list of (start, end) 0-based line index ranges guarded by
|
|
`#[cfg(test)]`. `cargo build` does not compile these, so include_str!
|
|
targets referenced only from test code are not required in a Docker
|
|
build context. Brace-aware: handles `#[cfg(test)] mod tests { ... }` and
|
|
`#[cfg(test)] fn helper() { ... }`."""
|
|
spans = []
|
|
i = 0
|
|
n = len(lines)
|
|
while i < n:
|
|
if CFG_TEST_RE.search(lines[i]):
|
|
# Find the opening brace of the guarded item. A braceless item
|
|
# (e.g. `#[cfg(test)] use foo;`) terminates at the first `;` — do
|
|
# NOT run forward to the next unrelated `{`, which would swallow
|
|
# real (non-test) code below and hide its include_str! calls.
|
|
j = i
|
|
braceless = False
|
|
while j < n and "{" not in lines[j]:
|
|
if ";" in lines[j]:
|
|
braceless = True
|
|
break
|
|
j += 1
|
|
if braceless:
|
|
# Single-line guarded item with no block; span is just it.
|
|
spans.append((i, j))
|
|
i = j + 1
|
|
continue
|
|
if j >= n:
|
|
break
|
|
depth = 0
|
|
k = j
|
|
while k < n:
|
|
depth += lines[k].count("{") - lines[k].count("}")
|
|
if depth <= 0:
|
|
break
|
|
k += 1
|
|
spans.append((i, k))
|
|
i = k + 1
|
|
else:
|
|
i += 1
|
|
return spans
|
|
|
|
|
|
def in_any_span(idx, spans):
|
|
return any(start <= idx <= end for start, end in spans)
|
|
|
|
|
|
# ── Collect include_str! references from library/binary code ──────────────
|
|
# Restrict to src/ and crates/ — the code that actually ships in the binary.
|
|
# tests/ and tools/ prompt fixtures are lower-stakes and covered by broad
|
|
# `COPY tests/`/`COPY tools/...` lines anyway.
|
|
rs_files = []
|
|
for base in ("src", "crates"):
|
|
d = repo / base
|
|
if d.is_dir():
|
|
rs_files.extend(sorted(d.rglob("*.rs")))
|
|
# The repo-root build.rs is compiled by `cargo build` too (and COPYd into the
|
|
# builder stage), so an include_str! it reads is equally a Docker-context risk.
|
|
root_build = repo / "build.rs"
|
|
if root_build.is_file():
|
|
rs_files.append(root_build)
|
|
|
|
missing = [] # (referencing_file, raw_path)
|
|
outside = [] # (referencing_file, raw_path) — resolves outside the repo
|
|
refs = [] # (referencing_relpath, target_relpath)
|
|
for f in rs_files:
|
|
try:
|
|
text = f.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
if "include_str!" not in text:
|
|
continue
|
|
f_rel = f.relative_to(repo)
|
|
lines = text.splitlines()
|
|
spans = cfg_test_spans(lines) if "#[cfg(test)]" in text else []
|
|
# Precompute character offset → line index for span filtering.
|
|
for m in INCLUDE_RE.finditer(text):
|
|
line_idx = text.count("\n", 0, m.start())
|
|
if in_any_span(line_idx, spans):
|
|
# Test-only include_str! — not compiled by `cargo build`.
|
|
continue
|
|
raw = m.group(1)
|
|
target = (f.parent / raw).resolve()
|
|
if not target.exists():
|
|
missing.append((f_rel, raw))
|
|
continue
|
|
try:
|
|
target_rel = target.relative_to(repo)
|
|
except ValueError:
|
|
# Resolves outside the repo: it exists on the host but can never be
|
|
# inside a Docker build context, so the image build would fail.
|
|
outside.append((f_rel, raw))
|
|
continue
|
|
refs.append((f_rel, target_rel))
|
|
|
|
|
|
# ── Parse Dockerfiles that build the binary from local source ─────────────
|
|
def parse_dockerfile(text):
|
|
roots = set()
|
|
full_copy = False
|
|
builds = False
|
|
for line in text.splitlines():
|
|
if CARGO_BUILD_RE.search(line):
|
|
builds = True
|
|
m = COPY_RE.match(line)
|
|
if not m:
|
|
continue
|
|
tokens = [t for t in m.group(1).split() if t]
|
|
if len(tokens) < 2:
|
|
continue
|
|
srcs = tokens[:-1] # last token is the destination
|
|
for s in srcs:
|
|
if s.startswith("--"): # --chown=, --chmod=, etc.
|
|
continue
|
|
if s in (".", "./"):
|
|
full_copy = True
|
|
continue
|
|
# Store the full normalized copied path (not just its top segment)
|
|
# so a narrowed copy like `COPY crates/foo/` covers crates/foo but
|
|
# not sibling crates.
|
|
roots.add(str(Path(s)))
|
|
return roots, full_copy, builds
|
|
|
|
|
|
dockerfiles = sorted(repo.glob("Dockerfile*"))
|
|
coverage_errors = [] # (dockerfile, referencing_file, target, missing_root)
|
|
for df in dockerfiles:
|
|
try:
|
|
text = df.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
roots, full_copy, builds = parse_dockerfile(text)
|
|
if not builds or full_copy:
|
|
# Doesn't compile from local source, or copies the whole context
|
|
# (`COPY . .`) — every include_str! target is present by construction.
|
|
continue
|
|
df_rel = df.relative_to(repo)
|
|
for ref_file, target in refs:
|
|
# Only enforce for referencing files this Dockerfile actually copies
|
|
# (i.e. actually compiles). A Dockerfile that never copies `src/`
|
|
# (e.g. Dockerfile.reborn) does not compile `src/hooks/*.rs`, so a
|
|
# repo-root `prompts/` reference from there is irrelevant to it.
|
|
if not is_covered(ref_file, roots):
|
|
continue
|
|
if not is_covered(target, roots):
|
|
coverage_errors.append((df_rel, ref_file, target, top_segment(str(target))))
|
|
|
|
|
|
# ── Report ────────────────────────────────────────────────────────────────
|
|
failed = False
|
|
|
|
if missing:
|
|
failed = True
|
|
print("✗ include_str!() targets that do not exist on disk:")
|
|
for ref_file, raw in missing:
|
|
print(f" {ref_file}: include_str!(\"{raw}\") — file not found")
|
|
print()
|
|
|
|
if outside:
|
|
failed = True
|
|
print("✗ include_str!() targets that resolve outside the build context "
|
|
"(exist on host, but no Docker COPY can ever include them):")
|
|
for ref_file, raw in outside:
|
|
print(f" {ref_file}: include_str!(\"{raw}\") — resolves outside the repo")
|
|
print()
|
|
|
|
if coverage_errors:
|
|
failed = True
|
|
print("✗ include_str!() targets missing from a Dockerfile build context:")
|
|
# Group by (dockerfile, missing_root) so the fix is obvious.
|
|
seen = set()
|
|
for df_rel, ref_file, target, missing_root in coverage_errors:
|
|
key = (str(df_rel), missing_root)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
# A top-level file (e.g. `providers.json`) is copied without a
|
|
# trailing slash; a directory root gets `dir/ dir/`.
|
|
is_file = str(target) == missing_root
|
|
copy_hint = (f"COPY {missing_root} {missing_root}" if is_file
|
|
else f"COPY {missing_root}/ {missing_root}/")
|
|
print(f" {df_rel}: compiles code that does "
|
|
f"include_str!(\"…/{target}\") but never COPYs `{missing_root}`.")
|
|
print(f" e.g. {ref_file} → {target}")
|
|
print(f" Fix: add `{copy_hint}` to the "
|
|
f"builder stage of {df_rel} (before the `cargo build`).")
|
|
print()
|
|
|
|
if failed:
|
|
print("include_str! path check failed. See CLAUDE.md 'Prompt templates live "
|
|
"in files' and issue #6018.")
|
|
print("Bypass (not recommended): git push --no-verify")
|
|
sys.exit(1)
|
|
|
|
print("include_str! path + Docker-COPY coverage: OK "
|
|
f"({len(refs)} references across {len(dockerfiles)} Dockerfile(s))")
|
|
PY
|