From 301a6aade4b8e4626e27a188a40c3ede85644cf1 Mon Sep 17 00:00:00 2001 From: KG-MCP Builder Date: Sat, 10 Oct 2026 13:45:38 +0000 Subject: [PATCH] Audit only the newest commit of a published main On a checkout whose HEAD is already the remote default branch, the release audit checked every author identity in the public history, so each push to main failed on commits that are published and cannot change. It now checks the newest commit there, as its comment said. Branches still have every commit since the published base checked, and a repository with no remote base still has its whole history checked. --- CHANGELOG.md | 3 ++ scripts/release_audit.py | 20 ++++++++---- tests/test_release_audit.py | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49509c2..f39ff26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ ### Fixed +- The release audit on a published `main` checks the identity of the newest commit only, + as its comment intended. It used to walk the whole public history, so every push to + `main` failed on commits that were already published and cannot change. - `kg verify` checks invalidation on a single Ladybug file. It used to find no superseded facts there (ingestion writes the empty group) and then failed comparing timestamps. diff --git a/scripts/release_audit.py b/scripts/release_audit.py index 53f38bb..04c722e 100644 --- a/scripts/release_audit.py +++ b/scripts/release_audit.py @@ -68,7 +68,7 @@ def audit( [ "git", "log", - identity_revision(root, identity_base), + *identity_revision(root, identity_base), "--format=%an <%ae>%n%cn <%ce>", ], cwd=root, @@ -81,8 +81,13 @@ def audit( return findings -def identity_revision(root: Path, base_revision: str = "origin/HEAD") -> str: - """Audit unpublished commits when the remote default branch is available.""" +def identity_revision(root: Path, base_revision: str = "origin/HEAD") -> list[str]: + """The ``git log`` revision arguments whose identities the audit checks. + + Commits not yet on the remote default branch when it is available; only the tip of + an already-published checkout, whose history is public and cannot change; the whole + history when there is no remote base, since nothing has been published yet. + """ base = subprocess.run( ["git", "merge-base", base_revision, "HEAD"], cwd=root, @@ -99,10 +104,11 @@ def identity_revision(root: Path, base_revision: str = "origin/HEAD") -> str: text=True, ).stdout.strip() if commit and commit != head: - return f"{commit}..HEAD" - # A published default-branch checkout has no unpublished range. Check its tip so a - # push still cannot introduce a private author identity. - return "HEAD" + return [f"{commit}..HEAD"] + # A published default-branch checkout has no unpublished range. Check its tip so + # a push still cannot introduce a private author identity. + return ["-1", "HEAD"] + return ["HEAD"] def sensitive_identities(log: str) -> list[str]: diff --git a/tests/test_release_audit.py b/tests/test_release_audit.py index f39e1d7..95bd5ef 100644 --- a/tests/test_release_audit.py +++ b/tests/test_release_audit.py @@ -37,3 +37,67 @@ def test_release_audit_detects_home_paths(tmp_path: Path): def test_release_audit_accepts_generic_source(tmp_path: Path): (tmp_path / "ok.md").write_text("Synthetic example only", encoding="utf-8") assert _audit_function()(tmp_path) == [] + + +def _git(root: Path, *args: str, who: str = "KG-MCP Builder ") -> str: + import subprocess + + name, email = who[: who.index(" <")], who[who.index("<") + 1 : -1] + env = { + "GIT_AUTHOR_NAME": name, + "GIT_AUTHOR_EMAIL": email, + "GIT_COMMITTER_NAME": name, + "GIT_COMMITTER_EMAIL": email, + "HOME": str(root), + "PATH": "/usr/bin:/bin", + } + return subprocess.run( + ["git", *args], cwd=root, env=env, check=True, capture_output=True, text=True + ).stdout.strip() + + +def _repo(tmp_path: Path) -> Path: + """A repository whose first commit carries a personal address, as old history can.""" + root = tmp_path / "repo" + root.mkdir() + # Assembled so this file does not itself contain an address the audit would flag. + person = "Real Person " + _git(root, "init", "-q") + (root / "a.txt").write_text("one\n") + _git(root, "add", "a.txt") + _git(root, "commit", "-q", "-m", "old", who=person) + (root / "a.txt").write_text("two\n") + _git(root, "commit", "-q", "-am", "new") + return root + + +def test_a_published_tip_is_audited_without_its_public_history(tmp_path: Path): + """main after a merge: HEAD is the published base, so only the tip is checked.""" + root = _repo(tmp_path) + _git(root, "branch", "published") + assert _audit_function()(root, allow_remote=True, identity_base="published") == [] + + +def test_a_personal_identity_on_the_published_tip_is_still_refused(tmp_path: Path): + root = _repo(tmp_path) + person = "Real Person " + (root / "a.txt").write_text("three\n") + _git(root, "commit", "-q", "-am", "tip", who=person) + _git(root, "branch", "published") + findings = _audit_function()(root, allow_remote=True, identity_base="published") + assert findings == ["git metadata: sensitive author identity"] + + +def test_unpublished_commits_are_all_audited(tmp_path: Path): + """A branch: every commit since the published base is checked, not only the tip.""" + root = _repo(tmp_path) + first = _git(root, "rev-list", "--max-parents=0", "HEAD") + _git(root, "branch", "published", first) + _git(root, "reset", "-q", "--hard", first) + person = "Real Person " + (root / "a.txt").write_text("unpublished\n") + _git(root, "commit", "-q", "-am", "mine", who=person) + (root / "a.txt").write_text("later\n") + _git(root, "commit", "-q", "-am", "later") + findings = _audit_function()(root, allow_remote=True, identity_base="published") + assert findings == ["git metadata: sensitive author identity"]