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
16 changes: 10 additions & 6 deletions .github/workflows/example-usage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ on:

permissions:
contents: read
pull-requests: write # Required for PR comments

jobs:
quality-gates:
Expand All @@ -18,10 +17,15 @@ jobs:
with:
fetch-depth: 0 # Required for changed file detection

- uses: hermes-labs-ai/quick-gate-python/.github/actions/pygate@main
# The root action owns gate execution, but this repository owns its
# project and development dependencies.
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
mode: canary # or "full" to include tests
repair: "true" # attempt auto-repair on failures
max-attempts: 3
python-version: "3.12"
post-comment: "true" # post findings as PR comment
- run: python -m pip install -e ".[dev]"

- uses: hermes-labs-ai/quick-gate-python@1a70edc12bfd19e633983e0819b648bb2a5dda4e
with:
mode: canary # or "full" to include caller-installed tests
python-version: "3.12"
fail-on-error: "true"
73 changes: 63 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,39 +179,40 @@ The same keys can be used in a standalone `pygate.toml` with `[policy]`, `[comma

## GitHub Actions

The repository ships a composite action at [`.github/actions/pygate/action.yml`](.github/actions/pygate/action.yml) and a copyable example at [`.github/workflows/example-usage.yml`](.github/workflows/example-usage.yml):
The repository ships the root Marketplace action at [`action.yml`](action.yml) and a copyable example at [`.github/workflows/example-usage.yml`](.github/workflows/example-usage.yml). Pin the root action to this currently audited immutable commit:

`hermes-labs-ai/quick-gate-python@1a70edc12bfd19e633983e0819b648bb2a5dda4e`

~~~yaml
name: PyGate
name: "Example: PyGate Quality Gates"

on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write # only needed when post-comment is true

jobs:
quality-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: hermes-labs-ai/quick-gate-python/.github/actions/pygate@main
- uses: hermes-labs-ai/quick-gate-python@1a70edc12bfd19e633983e0819b648bb2a5dda4e
with:
mode: canary
repair: "true"
max-attempts: 3
python-version: "3.12"
post-comment: "true"
fail-on-error: "true"
~~~

The action installs PyGate from the action checkout itself, then installs Ruff and Pyright, detects changed files, writes run artifacts to `.pygate/`, optionally attempts repair, optionally posts a pull-request comment, and uploads the artifact directory. This prevents the action implementation and the invoked CLI from drifting across releases. Canary mode skips tests by default. Full mode is caller-owned: before the action step, install the project and test dependencies into the same Python version selected by the action, for example:
This first example is read-only and blocking: it grants only `contents: read`, leaves repair and comments disabled, and fails the job when the final action status is `fail` or `escalated`.

The root action installs PyGate from its own checkout, then installs Ruff and Pyright, detects changed files, writes run artifacts to `.pygate/`, optionally attempts repair, optionally posts a pull-request comment, and uploads the artifact directory. Canary mode skips tests by default. Full mode is caller-owned: before the action step, install the project and test dependencies into the same Python version selected by the action, for example:

~~~yaml
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- run: python -m pip install -e ".[dev]"
Expand All @@ -220,6 +221,58 @@ The action installs PyGate from the action checkout itself, then installs Ruff a

The action does not provide an install-command, execute package installation for full mode, or infer project dependencies. It preflights `pytest` and `pytest-json-report` after setup and reports an actionable error before changed-file discovery when either is unavailable.

### Optional pull-request comments

To post a failure summary on a pull request, add `pull-requests: write` beside `contents: read` and set `post-comment: "true"` on the root action:

~~~yaml
permissions:
contents: read
pull-requests: write

steps:
- uses: hermes-labs-ai/quick-gate-python@1a70edc12bfd19e633983e0819b648bb2a5dda4e
with:
mode: canary
python-version: "3.12"
post-comment: "true"
~~~

Keep `repair: "false"` unless workspace mutation is explicitly intended. Pull requests from forks commonly receive a read-only `GITHUB_TOKEN`, so comment posting may be unavailable there; the read-only example above remains the safe baseline.

### Root action contract

The root action accepts these inputs:

| Input | Default | Contract |
| --- | --- | --- |
| `mode` | `canary` | `canary` runs Ruff and Pyright; `full` also runs pytest. |
| `repair` | `false` | When `true`, permits bounded Ruff repair that may mutate eligible consumer files. |
| `max-attempts` | `3` | Positive integer from `1` through `10`; applies to repair. |
| `python-version` | `3.12` | Python version selected for the action and its preflight. |
| `post-comment` | `false` | When `true`, posts a pull-request summary and requires `pull-requests: write`. |
| `changed-files` | empty | Optional path to newline-delimited or JSON-list changed paths. |
| `artifact-name` | `pygate-artifacts` | Caller-configurable uploaded artifact name. |
| `fail-on-error` | `true` | When `true`, a final `fail` or `escalated` status fails the action; `false` is observation-only. |

It exposes these outputs:

| Output | Values or meaning |
| --- | --- |
| `status` | Final result: `pass`, `fail`, or `escalated`. |
| `gate-status` | Raw initial gate result before repair: `pass` or `fail`. |
| `repair-status` | `pass`, `escalated`, or `skipped`. |
| `full-mode-dependency-status` | `ready`, `missing`, or `not-required`. |
| `full-mode-dependency-classification` | `caller-owned-dependencies`, `missing-test-dependency`, or `canary-does-not-run-tests`. |
| `changed-files-strategy` | Source label such as `caller-supplied`, `pull-request-diff`, `push-diff`, `parent-diff`, or `first-or-shallow-tracked-files`. |
| `failures-json` | Path to `.pygate/failures.json` when the gate runs. |

The action uploads `.pygate/` as an action-owned artifact, including hidden files. It includes `gate-result.json`, `failures.json`, `run-metadata.json`, and, in full mode, the pytest report; repair and summary artifacts are included when produced. Treat command output in these artifacts as untrusted data. The default artifact name is `pygate-artifacts`; set `artifact-name` to avoid collisions in matrices.

### Pinning policy

Use the root action at an immutable commit or an immutable release tag. The examples use the audited commit `1a70edc12bfd19e633983e0819b648bb2a5dda4e`; replace it with an immutable release tag when one exists. 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.

## Privacy, egress, and safety
Expand Down
8 changes: 5 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Version | Supported |
| ------- | ------------------ |
| 0.1.x | :white_check_mark: |
| 0.2.x | :white_check_mark: |

## Reporting a Vulnerability

Expand All @@ -26,7 +26,7 @@ PyGate executes external tools (`ruff`, `pyright`, `pytest`) via subprocess. Sec

### Command Execution

- **Shell commands**: PyGate constructs shell commands from configuration values. The `[commands]` section in `pygate.toml` or `[tool.pygate.commands]` in `pyproject.toml` is executed via `subprocess.run(shell=True)`. Users should review custom command overrides carefully, as they are passed directly to the shell.
- **Configured commands**: By default, command strings from `[commands]` in `pygate.toml` or `[tool.pygate.commands]` in `pyproject.toml` are tokenized into argv-safe arguments. `--unsafe-shell` or `allow_unsafe_shell = true` is an explicit compatibility escape hatch that executes command text through a shell; review custom command overrides carefully.
- **File path sanitization**: File paths passed to repair commands are escaped with `shlex.quote()` to prevent shell injection via crafted filenames. Paths containing `..` or absolute paths are rejected by the repair scope filter.

### File System Access
Expand All @@ -42,10 +42,12 @@ PyGate executes external tools (`ruff`, `pyright`, `pytest`) via subprocess. Sec

### GitHub Actions Composite Action

- **Input validation**: The composite action validates the `mode` input against an allowlist (`canary` or `full`) before passing it to the CLI. The `max-attempts` input is passed through to the CLI's argparse, which validates it as an integer; negative values are not rejected at the action layer. All inputs are passed via environment variables rather than string interpolation to prevent injection.
- **Input validation**: The root Marketplace action validates `mode`, the repair/comment/fail-on-error booleans, `max-attempts` from 1 through 10, and supplied changed-file content before running gates. Inputs are passed via environment variables rather than string interpolation to prevent injection.
- **Action defaults**: The root Marketplace action defaults to `repair: false` and `post-comment: false`. Enabling repair is an explicit opt-in that may modify eligible files in the consumer workspace; enabling comments is an explicit opt-in that requires `pull-requests: write`.
- **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@1a70edc12bfd19e633983e0819b648bb2a5dda4e` audited commit, or replace it with an immutable release tag when one exists. 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
19 changes: 19 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ Primary CLI:
- `pygate summarize --input <failures.json>`
- `pygate repair --input <failures.json>`

Root GitHub Action quickstart (audited immutable commit):
- `hermes-labs-ai/quick-gate-python@1a70edc12bfd19e633983e0819b648bb2a5dda4e`
- 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

Root action inputs:
- `mode`, `repair`, `max-attempts`, `python-version`, `post-comment`
- `changed-files`, `artifact-name`, `fail-on-error`

Root action outputs:
- `status`: final `pass`, `fail`, or `escalated`
- `gate-status`: raw initial `pass` or `fail`
- `repair-status`: `pass`, `escalated`, or `skipped`
- `full-mode-dependency-status`: `ready`, `missing`, or `not-required`
- `full-mode-dependency-classification`, `changed-files-strategy`, `failures-json`

Full mode is caller-owned: install project dependencies plus `pytest` and `pytest-json-report` before the action step in the selected Python environment. The action uploads hidden `.pygate/` evidence files under `artifact-name` (default `pygate-artifacts`). Repair is opt-in and can mutate eligible consumer files; comments are opt-in and require `pull-requests: write`, which is commonly unavailable for fork-origin pull requests.

Outputs:
- failures, run metadata, repair report, escalation, and agent-brief artifacts under `.pygate/`

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
[tool.pyright]
pythonVersion = "3.10"
typeCheckingMode = "basic"
exclude = ["tests/action-fixture/fail.py"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Loading