fix(seedgo): readme check-ignore must match dir-only patterns on clean checkout (DPLAN-0195)
The last 1%: 7 branches scored 99% in CI while 100% locally. Root cause proven by reproducing CI's exact path (tracked-only tree + real .git): .gitignore dir-only patterns (trailing slash — logs/, **/*_json/, .trinity/) do NOT match via 'git check-ignore <bare-path>' when the path is absent from disk (clean checkout), because git cannot infer 'directory' to apply a dir-only pattern. The working tree has those dirs on disk, so it matched there — the exact working-tree-vs-clean-checkout divergence. _is_gitignored now also tests the trailing-slash form; all 7 readme failures (cli_json/logs/artifacts/.trinity/ etc flagged 'missing on disk') clear. Regression test builds a real git repo with dir-only patterns + non-existent paths. CI gate also now prints failing standards + check messages (says WHY). Verified: clean tree w/ real .git 13/13 100%; working tree 13/13 100%; seedgo 1053 tests green; pyright 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
24065f11b3
commit
4c7e14a255
@@ -30,12 +30,27 @@ for branch in branches:
|
||||
avg = result.get("average", 0)
|
||||
print(f" {branch['name']:>12}: {avg:.0f}%")
|
||||
if avg < THRESHOLD:
|
||||
failed.append((branch["name"], avg))
|
||||
failed.append((branch["name"], avg, result))
|
||||
|
||||
if failed:
|
||||
print(f"\nFAILED: {len(failed)} branch(es) below {THRESHOLD}%")
|
||||
for name, score in failed:
|
||||
for name, score, result in failed:
|
||||
print(f" {name}: {score:.0f}%")
|
||||
# Name the failing standards + the specific checks that did not pass,
|
||||
# so CI logs say WHY (not just the percentage). Critical for diagnosing
|
||||
# working-tree-vs-clean-checkout divergence.
|
||||
scores = result.get("scores", {})
|
||||
results = result.get("results", {})
|
||||
for std, sc in scores.items():
|
||||
if sc < 100:
|
||||
checks = results.get(std, {}).get("checks", [])
|
||||
msgs = [
|
||||
c.get("message", "")
|
||||
for c in checks
|
||||
if not c.get("passed", True)
|
||||
]
|
||||
detail = " | ".join(m for m in msgs if m)[:400]
|
||||
print(f" └ {std}: {sc:.0f}% {detail}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"\nAll {len(branches)} branches pass (>={THRESHOLD}%)")
|
||||
|
||||
+9
-2
@@ -103,8 +103,15 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format
|
||||
in the directory-tree and dead-link checks; `encapsulation` infers the branch
|
||||
from the path when the gitignored `AIPASS_REGISTRY.json` is unavailable (and no
|
||||
longer collides on the `aipass` branch); `architecture` skips cleanly when the
|
||||
gitignored `passport.json` is absent. Clean-tree and working-tree audits now
|
||||
both report 13/13 = 100%. (DPLAN-0195)
|
||||
gitignored `passport.json` is absent. A follow-up refined `readme`'s
|
||||
`git check-ignore` use: `.gitignore` dir-only patterns (trailing slash —
|
||||
`logs/`, `**/*_json/`, `.trinity/`) don't match a clean checkout's
|
||||
non-existent paths unless directory intent is signalled, so the check now
|
||||
also tests the trailing-slash form (this was the last 1% — `readme` flagged
|
||||
`cli_json`/`logs`/`artifacts` as "missing on disk" in CI only). The CI gate
|
||||
(`.github/scripts/seedgo_audit.py`) now also prints the failing standards and
|
||||
their check messages, so a sub-100 result says *why*, not just the percentage.
|
||||
Clean-tree and working-tree audits both report 13/13 = 100%. (DPLAN-0195)
|
||||
- **Two latent Windows portability bugs caught by the new e2e harness** — both
|
||||
were always present in the code; they only surfaced now because this is the
|
||||
first CI to run `aipass init` scaffolding and real-branch `drone` routing on
|
||||
|
||||
@@ -52,12 +52,23 @@ def _is_gitignored(path: Path) -> bool:
|
||||
)
|
||||
if top.returncode == 0:
|
||||
repo_root = top.stdout.strip()
|
||||
result = subprocess.run(
|
||||
["git", "-C", repo_root, "check-ignore", "-q", str(path)],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
return result.returncode == 0
|
||||
# Test both the bare path and its directory form. .gitignore dir-only
|
||||
# patterns (trailing slash: logs/, artifacts/, **/*_json/, .trinity/)
|
||||
# only match when git knows the path is a directory. In a clean
|
||||
# checkout the gitignored dir does not exist on disk, so git cannot
|
||||
# infer "directory" from the bare path and reports it un-ignored —
|
||||
# appending a trailing slash signals directory intent and restores
|
||||
# the match. (Without this, README dir-tree/dead-link checks fail in
|
||||
# CI's clean checkout while passing in a working tree.)
|
||||
for candidate in (str(path), str(path).rstrip("/") + "/"):
|
||||
result = subprocess.run(
|
||||
["git", "-C", repo_root, "check-ignore", "-q", candidate],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
logger.info("git check-ignore unavailable for %s", path)
|
||||
|
||||
|
||||
@@ -471,3 +471,31 @@ def test_check_module_missing_readme_has_8_failures(tmp_path):
|
||||
result = check_module(str(entry))
|
||||
assert len(result["checks"]) == 8
|
||||
assert result["score"] == 0
|
||||
|
||||
|
||||
def test_is_gitignored_directory_only_pattern_nonexistent(tmp_path, monkeypatch):
|
||||
"""Dir-only .gitignore patterns match non-existent paths via the slash form.
|
||||
|
||||
Regression (DPLAN-0195): in a clean checkout (CI) the gitignored dir does
|
||||
not exist on disk, so ``git check-ignore <bare-path>`` reports it un-ignored
|
||||
because git cannot confirm "directory" to match a dir-only pattern (trailing
|
||||
slash). ``_is_gitignored`` must also test the trailing-slash form. Without
|
||||
this the README dir-tree/dead-link checks passed in a working tree but failed
|
||||
in CI's clean checkout.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
_is_gitignored,
|
||||
)
|
||||
|
||||
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
||||
(tmp_path / ".gitignore").write_text("logs/\n**/*_json/\n.trinity/\n")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
# Non-existent dirs — only match when the trailing-slash form is tested.
|
||||
assert _is_gitignored(tmp_path / "logs") is True
|
||||
assert _is_gitignored(tmp_path / "cli_json") is True
|
||||
assert _is_gitignored(tmp_path / ".trinity") is True
|
||||
# A path not covered by any pattern stays un-ignored.
|
||||
assert _is_gitignored(tmp_path / "src") is False
|
||||
|
||||
Reference in New Issue
Block a user