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
39 changes: 30 additions & 9 deletions .github/workflows/action-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
- case: pass-default
fixture: pass
repair: false
assert-unchanged: true
fail-on-error: true
expected-outcome: success
expected-gate-status: pass
Expand All @@ -29,6 +30,7 @@ jobs:
- case: fail-default
fixture: fail
repair: false
assert-unchanged: true
fail-on-error: true
expected-outcome: failure
expected-gate-status: fail
Expand All @@ -37,6 +39,7 @@ jobs:
- case: fail-observation
fixture: fail
repair: false
assert-unchanged: true
fail-on-error: false
expected-outcome: success
expected-gate-status: fail
Expand All @@ -45,6 +48,7 @@ jobs:
- case: repair-success
fixture: repair
repair: true
assert-unchanged: false
fail-on-error: true
expected-outcome: success
expected-gate-status: fail
Expand All @@ -53,6 +57,7 @@ jobs:
- case: repair-escalation
fixture: fail
repair: true
assert-unchanged: false
fail-on-error: true
expected-outcome: failure
expected-gate-status: fail
Expand Down Expand Up @@ -92,7 +97,7 @@ jobs:
git -C "$GITHUB_WORKSPACE" add check.py
git -C "$GITHUB_WORKSPACE" commit --quiet -m "Add $SCENARIO canary"

echo "/action-source" >> "$GITHUB_WORKSPACE/.git/info/exclude"
printf '/action-source\n/.pygate/\n/.pygate-changed-files.txt\n' >> "$GITHUB_WORKSPACE/.git/info/exclude"
ln -s "$action_source" "$GITHUB_WORKSPACE/action-source"

- name: Prove source and consumer separation
Expand All @@ -110,26 +115,37 @@ jobs:
test -f "$action_path/action.yml"
test -z "$(git status --porcelain)"

- name: Run the checked-out root action
id: gate
- name: Run the checked-out root action with safe defaults
id: gate-default
if: ${{ !matrix.repair }}
continue-on-error: true
uses: ./action-source
with:
mode: canary
repair: ${{ matrix.repair }}
python-version: "3.12"
post-comment: "false"
fail-on-error: ${{ matrix.fail-on-error }}
artifact-name: pygate-action-${{ matrix.case }}-${{ github.run_attempt }}

- name: Run the checked-out root action with explicit repair opt-in
id: gate-repair
if: ${{ matrix.repair }}
continue-on-error: true
uses: ./action-source
with:
mode: canary
repair: "true"
python-version: "3.12"
fail-on-error: ${{ matrix.fail-on-error }}
artifact-name: pygate-action-${{ matrix.case }}-${{ github.run_attempt }}

- name: Record and assert the action outcome and declared status
if: always()
shell: bash
env:
ACTUAL_OUTCOME: ${{ steps.gate.outcome }}
ACTUAL_GATE_STATUS: ${{ steps.gate.outputs.gate-status }}
ACTUAL_STATUS: ${{ steps.gate.outputs.status }}
ACTUAL_REPAIR_STATUS: ${{ steps.gate.outputs.repair-status }}
ACTUAL_OUTCOME: ${{ matrix.repair && steps.gate-repair.outcome || steps.gate-default.outcome }}
ACTUAL_GATE_STATUS: ${{ matrix.repair && steps.gate-repair.outputs.gate-status || steps.gate-default.outputs.gate-status }}
ACTUAL_STATUS: ${{ matrix.repair && steps.gate-repair.outputs.status || steps.gate-default.outputs.status }}
ACTUAL_REPAIR_STATUS: ${{ matrix.repair && steps.gate-repair.outputs.repair-status || steps.gate-default.outputs.repair-status }}
EXPECTED_OUTCOME: ${{ matrix.expected-outcome }}
EXPECTED_GATE_STATUS: ${{ matrix.expected-gate-status }}
EXPECTED_STATUS: ${{ matrix.expected-status }}
Expand All @@ -144,6 +160,11 @@ jobs:
test -f .pygate/gate-result.json
test -f .pygate/failures.json
jq -e '.schema == "gate-result/v1" and (.status | IN("pass", "fail", "timeout", "error"))' .pygate/gate-result.json >/dev/null
if [[ "${{ matrix.assert-unchanged }}" == "true" ]]; then
test -z "$(git status --porcelain --untracked-files=all)"
git diff --quiet
git diff --cached --quiet
fi

verify-action-artifacts:
if: always()
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ 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.
- **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.
- **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.
Expand Down
8 changes: 4 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ inputs:
description: "Gate mode: canary (lint+typecheck, tests optional) or full (all gates)"
default: "canary"
repair:
description: "Run bounded repair after failures"
default: "true"
description: "Run bounded repair after failures; when true, may mutate files in the consumer workspace"
default: "false"
max-attempts:
description: "Maximum repair attempts before escalation"
default: "3"
python-version:
description: "Python version to use"
default: "3.12"
post-comment:
description: "Post findings as PR comment"
default: "true"
description: "Post findings as a PR comment; when true, requires pull-requests: write"
default: "false"
artifact-name:
description: "Name for the uploaded PyGate artifact bundle"
default: "pygate-artifacts"
Expand Down
Loading