Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
20 changes: 13 additions & 7 deletions scripts/release_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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]:
Expand Down
64 changes: 64 additions & 0 deletions tests/test_release_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <noreply@localhost>") -> 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 <real.person" + "@" + "example.com>"
_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 <real.person" + "@" + "example.com>"
(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 <real.person" + "@" + "example.com>"
(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"]
Loading