diff --git a/.github/workflows/example-usage.yml b/.github/workflows/example-usage.yml index 5fc286b..743a870 100644 --- a/.github/workflows/example-usage.yml +++ b/.github/workflows/example-usage.yml @@ -8,7 +8,6 @@ on: permissions: contents: read - pull-requests: write # Required for PR comments jobs: quality-gates: @@ -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" diff --git a/README.md b/README.md index 152999d..6fef202 100644 --- a/README.md +++ b/README.md @@ -179,10 +179,12 @@ 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: @@ -190,28 +192,27 @@ on: 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]" @@ -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 diff --git a/SECURITY.md b/SECURITY.md index f2e0a59..ac3b069 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ------- | ------------------ | -| 0.1.x | :white_check_mark: | +| 0.2.x | :white_check_mark: | ## Reporting a Vulnerability @@ -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 @@ -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@ # 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 diff --git a/llms.txt b/llms.txt index a19519d..9840196 100644 --- a/llms.txt +++ b/llms.txt @@ -12,6 +12,25 @@ Primary CLI: - `pygate summarize --input ` - `pygate repair --input ` +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/` diff --git a/pyproject.toml b/pyproject.toml index 9f58ae7..c9e856d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"]