From 587abc01e8a7c6ecf86ba7974e4e47afc12e8805 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 09:20:39 +0000 Subject: [PATCH] docs: correct root-action pin guidance and add a pin regression contract The documented root-action pin drifted across files and the release-tag guidance was not true against live repository state. - llms.txt still pinned 1a70edc (PR #24) after the audited pin moved to 671e8db (PR #31), so the agent-facing quickstart and the human-facing README told consumers to use different commits. - README.md, SECURITY.md, and llms.txt all said to replace the audited commit "with an immutable release tag when one exists". Tags v0.1.0, v0.1.1, and v0.2.0 do exist, but every one of them predates the root action.yml: at v0.2.0 the only action file is .github/actions/pygate/action.yml. A consumer following that guidance gets a workflow that cannot resolve the action at all. Both files now state the actual constraint, and a new offline regression contract asserts that every documented root-action reference is a 40-character commit SHA and that all documented pins agree. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_015pyyWgRC2kvjk6fXauNWZM --- CHANGELOG.md | 13 +++++++++++ README.md | 4 +++- SECURITY.md | 2 +- llms.txt | 6 +++-- tests/test_docs_action_pin.py | 44 +++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 tests/test_docs_action_pin.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6650e26..ffecfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `tests/test_docs_action_pin.py`, a regression contract asserting that every documented + root-action reference is a 40-character commit SHA and that all documented pins agree. + +### Fixed + +- Corrected the root-action pin in `llms.txt`, which still referenced the superseded `1a70edc` + commit after the audited pin moved to `671e8db`. +- Replaced the "replace it with an immutable release tag when one exists" guidance in `README.md`, + `SECURITY.md`, and `llms.txt`. The published `v0.1.0`, `v0.1.1`, and `v0.2.0` tags predate the + root `action.yml`, so following that guidance produced a workflow that cannot resolve the action. + ## [0.2.0] - 2026-08-08 ### Added diff --git a/README.md b/README.md index a4f5cc5..81777e5 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,9 @@ The action uploads `.pygate/` as an action-owned artifact, including hidden file ### Pinning policy -Use the root action at an immutable commit or an immutable release tag. The examples use the audited commit `671e8db37c2bd124d4b74653f8c81945d1592a8f`; replace it with an immutable release tag when one exists. Do not use a mutable branch reference for the root action. +Use the root action at an immutable commit. The examples use the audited commit `671e8db37c2bd124d4b74653f8c81945d1592a8f`. + +The published release tags `v0.1.0`, `v0.1.1`, and `v0.2.0` version the `pygate-ci` package on PyPI. They predate the root [`action.yml`](action.yml), so none of them resolves as a GitHub Actions reference: a workflow that pins the root action at `@v0.2.0` fails with a missing-action error. Pin the audited commit until a release tag is cut at or after the root action landed, then prefer that tag. Do not use a mutable branch reference for the root action. PyGate never grants merge authority. A workflow still decides whether a failed, timed-out, or escalated job blocks a pull request, and any comment or artifact should be treated as untrusted command output before security-sensitive rendering. diff --git a/SECURITY.md b/SECURITY.md index a551fc1..766a04b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -47,7 +47,7 @@ PyGate executes external tools (`ruff`, `pyright`, `pytest`) via subprocess. Sec - **Supply chain pinning**: All third-party actions in CI workflows and the composite action are pinned to SHA digests with version comments (e.g., `actions/checkout@ # v4`). This prevents compromised upstream tags from injecting malicious code. - **Permissions**: The composite action requires only `contents: read` by default. The optional PR comment feature requires `pull-requests: write`. No other permissions are requested. - **Full-mode dependencies**: The caller owns project and test dependencies. Before full mode, the caller must make `pytest` and `pytest-json-report` available in the Python version selected by the action; the action preflights them and does not install them. -- **Action pinning**: Use the root `hermes-labs-ai/quick-gate-python@671e8db37c2bd124d4b74653f8c81945d1592a8f` audited commit, or replace it with an immutable release tag when one exists. Do not use a mutable branch reference for the root action. +- **Action pinning**: Use the root `hermes-labs-ai/quick-gate-python@671e8db37c2bd124d4b74653f8c81945d1592a8f` audited commit. The `v0.1.0`, `v0.1.1`, and `v0.2.0` tags version the PyPI package and predate the root `action.yml`, so they do not resolve as an action reference. Do not use a mutable branch reference for the root action. - **Artifact trust**: Artifacts uploaded to `.pygate/` contain command output (stdout/stderr) from the target project. Downstream consumers should treat these as untrusted data and validate before rendering in security-sensitive contexts. ### Dependency Supply Chain diff --git a/llms.txt b/llms.txt index 9840196..e707dcc 100644 --- a/llms.txt +++ b/llms.txt @@ -13,10 +13,12 @@ Primary CLI: - `pygate repair --input ` Root GitHub Action quickstart (audited immutable commit): -- `hermes-labs-ai/quick-gate-python@1a70edc12bfd19e633983e0819b648bb2a5dda4e` +- `hermes-labs-ai/quick-gate-python@671e8db37c2bd124d4b74653f8c81945d1592a8f` - grant `contents: read` only for the blocking read-only baseline - use `mode: canary`, `fail-on-error: "true"`, and leave repair/comments disabled -- replace the commit with an immutable release tag when one exists; never use a mutable branch reference +- the `v0.1.0`, `v0.1.1`, and `v0.2.0` tags version the `pygate-ci` package and predate the root + `action.yml`, so they do not resolve as an action reference; pin the audited commit and never use a + mutable branch reference Root action inputs: - `mode`, `repair`, `max-attempts`, `python-version`, `post-comment` diff --git a/tests/test_docs_action_pin.py b/tests/test_docs_action_pin.py new file mode 100644 index 0000000..296bdbf --- /dev/null +++ b/tests/test_docs_action_pin.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] + +# Every place that tells a consumer how to reference the root action. +DOCUMENTED_PIN_SOURCES = ( + "README.md", + "SECURITY.md", + "llms.txt", + ".github/workflows/example-usage.yml", +) + +SELF_REFERENCE = re.compile(r"hermes-labs-ai/quick-gate-python@(\S+?)(?=[`\s]|$)") +COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$") + + +def _documented_refs() -> dict[str, list[str]]: + return {source: SELF_REFERENCE.findall((REPO_ROOT / source).read_text()) for source in DOCUMENTED_PIN_SOURCES} + + +def test_every_documented_source_pins_the_root_action() -> None: + for source, refs in _documented_refs().items(): + assert refs, f"{source} documents no root-action reference" + + +def test_documented_pins_are_immutable_commits() -> None: + for source, refs in _documented_refs().items(): + for ref in refs: + assert COMMIT_SHA.match(ref), ( + f"{source} references the root action at {ref!r}. The pinning policy requires a " + "40-character commit SHA: release tags predate the root action.yml and mutable " + "branch references are not allowed." + ) + + +def test_documented_pins_agree() -> None: + refs = _documented_refs() + distinct = {ref for source_refs in refs.values() for ref in source_refs} + assert len(distinct) == 1, "Documented root-action pins disagree, so at least one source is stale: " + "; ".join( + f"{source}={sorted(set(source_refs))}" for source, source_refs in refs.items() + )