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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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@<sha> # 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
Expand Down
6 changes: 4 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ Primary CLI:
- `pygate repair --input <failures.json>`

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`
Expand Down
44 changes: 44 additions & 0 deletions tests/test_docs_action_pin.py
Original file line number Diff line number Diff line change
@@ -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()
)