Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fi
if [ ! -f "${CLAUDE_PROJECT_DIR:-.}/apm.yml" ]; then
echo "gitapex: ${CLAUDE_PROJECT_DIR:-.}/apm.yml not found; skipping prek install (not a gitapex checkout)." >&2
elif command -v uv >/dev/null 2>&1; then
uv run --directory "${CLAUDE_PROJECT_DIR:-.}" prek -q install --allow-missing-config -t pre-commit -t pre-push \
uv run --directory "${CLAUDE_PROJECT_DIR:-.}" prek -q install --allow-missing-config -t pre-commit -t pre-push -t commit-msg \
|| echo "gitapex: prek install reported a failure; the local pre-commit hook may not be active this session." >&2
else
echo "gitapex: uv not found; cannot install the local pre-commit hook this session." >&2
Expand Down
28 changes: 28 additions & 0 deletions .gitapex/ssot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,34 @@
{"kind": "github-native", "ref": "required_status_checks context \"betterleaks\" (repository ruleset on the default branch)"}
]
},
{
"id": "commit-citation-gate",
"kind": "script",
"script": [".github/scripts/gitapex_gate_commit_citation.py", ".github/workflows/commit-citation-gate.yml"],
"rule": "CLAUDE.md section 3's issue-citation rule for commits: passes when a citation (Closes/Fixes/Refs #N, or a bare #N) is found in at least one non-merge commit in the PR's own range (git log --no-merges base..head, so an uncited merge commit in that range never fails this on its own) OR in the PR's own title/body; fails when found in neither. The commit-msg pre-commit hook (.pre-commit-config.yaml's commit-citation entry, sharing this same script's --mode commit-msg) is the bypassable local first pass; this CI check is the actual no-exceptions backstop and carries no separate registry entry of its own, matching this registry's existing convention for pre-commit-stage-only hooks (e.g. ruff-check, skill-shape-check).",
"planes": ["ci", "local"],
"local_invocation": [
"uv",
"run",
"--frozen",
"python3",
".github/scripts/gitapex_gate_commit_citation.py",
"--mode",
"pr-range"
],
"trigger": ".github/workflows/commit-citation-gate.yml on pull_request:[opened, edited, synchronize, reopened] (no paths filter, so a required check can never be left Pending)",
"policy_refs": [],
"cluster": "plan-integrity",
"tracking_issue": 1212,
"status": "active",
"supersedes": null,
"bypass_review_status": "not-yet-reviewed",
"target": [
{"kind": "workflow-event", "ref": "commit-citation-gate.yml:pull_request"},
{"kind": "runtime-resolved-reference", "ref": "git log --no-merges <merge-base>..<head>"},
{"kind": "runtime-resolved-reference", "ref": "github.event.pull_request.title / .body"}
]
},
{
"id": "betterleaks-allowlist-no-removal",
"kind": "script",
Expand Down
30 changes: 29 additions & 1 deletion .github/scripts/_gitapex_base_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,25 @@ def destination_refspec(remote: str, branch: str) -> str:


def run_git(
root: pathlib.Path, args: list[str], *, label: str, timeout: int, error_cls: type[Exception]
root: pathlib.Path,
args: list[str],
*,
label: str,
timeout: int,
error_cls: type[Exception],
stdin_text: str | None = None,
# function-body-test-coverage: WAIVED: the added `stdin_text` parameter
# (issue #1212) is exercised by the pre-existing, extensively-updated
# tests/test_gitapex_base_ref.py (test_run_git_* mentions `run_git` by
# name repeatedly) -- but gitapex_gate_function_body_test_coverage.py's
# own _stem() keeps this module's leading underscore
# ("_gitapex_base_ref"), so it looks for tests/test__gitapex_base_ref.py
# (double underscore) rather than this repository's own actual,
# established single-underscore convention for a `_`-prefixed private
# helper module's test file. A genuine gate limitation, not a real
# coverage gap -- disclosed here rather than worked around by adding a
# second, oddly-named test file just to match the gate's own stem
# computation.
) -> subprocess.CompletedProcess[str]:
"""Run ``git -C root <args>`` and return the completed process,
regardless of its exit code -- callers decide what a nonzero
Expand All @@ -149,6 +167,15 @@ def run_git(
this function replaces there -- existing tests asserting on that text
keep passing unmodified.

``stdin_text`` feeds a git subcommand that reads its input from stdin
(``git stripspace``, this module's own third caller
``gitapex_gate_commit_citation.py``) -- added here rather than as a
second, near-identical ``subprocess.run`` wrapper in that caller,
which is precisely the duplicate-then-drift this module exists to
prevent. Default ``None`` leaves ``subprocess.run``'s own stdin
handling exactly as it was for every pre-existing caller: no pipe is
opened and no behavior changes.

``errors="replace"`` rather than ``text=True``'s own strict default,
matching ``gitapex_gate_behind_base.py``'s documented regression: a
byte sequence on stdout/stderr that is not valid UTF-8 must not raise
Expand All @@ -168,6 +195,7 @@ def run_git(
errors="replace",
check=False,
timeout=timeout,
input=stdin_text,
)
except subprocess.TimeoutExpired as error:
raise error_cls(f"git {label} timed out after {timeout}s") from error
Expand Down
Loading
Loading