name: Repowise CI gates description: >- Gate a pull request on patch coverage, documentation drift, new security findings, new code-health findings and change risk, and select the tests it needs. The gates need git and nothing else (no index, no API key); test selection reads a cached index. author: repowise-dev branding: icon: check-circle color: blue inputs: checks: description: >- Comma-separated gates to run: coverage, doc-drift, security, health, risk. Coverage needs a report, so it is not on by default. Doc drift checks the whole tree; on a repository that already has drift, commit a baseline first. Health is opt-in: it fails on code-shape findings the change introduces. Risk is opt-in too, and gates only when risk-fail-above-percentile is set. default: doc-drift,security version: description: Repowise version to install from PyPI. Empty installs the latest. default: "" python-version: description: Python used to install and run Repowise (3.11 or newer). default: "3.12" working-directory: description: Path inside the repository to run from. default: "." base: description: >- Revision range for coverage, security, health and risk, e.g. origin/main...HEAD. Empty reads the pull request's target branch, else the default branch. default: "" coverage-report: description: >- Coverage reports, one per line. Each line is a path or a glob such as artifacts/**/lcov.info (Repowise expands it), optionally path=prefix to prepend a directory to that report's paths. Empty uses coverage.paths from .repowise/config.yaml, else discovery. default: "" coverage-fail-under: description: >- Minimum patch coverage percent. Empty uses coverage.fail_under from .repowise/config.yaml, else reports without gating. default: "" coverage-min-coverable-lines: description: >- Small-change tolerance: a change with fewer changed executable lines than this is reported but never fails the coverage gate. Empty uses coverage.min_coverable_lines from .repowise/config.yaml. default: "" coverage-fail-under-risky: description: >- Minimum patch coverage percent over risky files only: hotspots or bug magnets from the index; on git alone, the top quartile of files with bug-fix history. A change with no risky file is not gated. Empty uses coverage.fail_under_risky from .repowise/config.yaml, else no risky gate. default: "" coverage-fail-under-branches: description: >- Minimum percent of branches taken on changed lines, judged beside patch coverage and never blended into it. A change with no branching line is not gated; a report without per-line branch data fails the step as cannot evaluate (exit 2). Empty uses coverage.fail_under_branches from .repowise/config.yaml (where missing branch data is only a note), else no branch gate. default: "" coverage-base-report: description: >- Coverage reports measured at the change's base commit (the merge-base with the target branch), one per line, read like coverage-report. Adds project coverage at the base against the head, and the files whose coverage changed outside the change. default: "" coverage-max-drop: description: >- Most project coverage may fall from the change's base, in percentage points. Needs coverage-base-report (or an index that stored coverage at the base). Empty uses coverage.max_drop from .repowise/config.yaml, else reports without gating. default: "" doc-drift-baseline: description: Committed doc-drift baseline file; only findings it does not list fail. default: "" security-fail-on: description: Lowest severity that fails the security gate (high, med or low). default: high security-baseline: description: Committed security baseline file; only findings it does not list fail. default: "" fail-on-baseline-growth: description: >- "true" fails doc-drift and security when their baseline lists a finding the same file at the change's base does not, so a change cannot hide new findings by re-saving the baseline. A baseline new in the change passes. default: "false" health-fail-on: description: >- What fails the health gate: introduced (findings the change wrote) or worsened (those, plus findings it made worse). Below it they warn. default: introduced health-fail-at: description: >- Lowest severity that fails the health gate (medium, high or critical); findings below it warn. default: high risk-fail-above-percentile: description: >- Fail when the change ranks above this percentile of the repository's recent commits by size and spread: a size gate, not a bug predictor. Empty reports the rank without gating. default: "" comment: description: >- "true" posts one pull-request comment summarizing every gate and updates it in place on each run. The workflow needs the pull-requests write permission; on a fork's pull request the comment is skipped with a warning. default: "false" github-token: description: >- Token the summary comment is posted with; a later run edits only the comment this token's own account posted. default: ${{ github.token }} upload-sarif: description: >- "true" uploads doc-drift, security and health findings to GitHub code scanning. The workflow needs the security-events write permission. default: "false" impacted-tests: description: >- "true" selects the tests the change needs (repowise impacted-tests --format args) and sets the impacted-tests and run-all-tests outputs. It requires a cached Repowise index: restore .repowise into the working directory first, and the step brings it up to date with repowise update --index-only. Without one every answer is run-all-tests, with the reason. default: "false" impacted-tests-runner: description: Runner the tests output is written for (auto, pytest, go, jest or files). default: auto outputs: coverage: description: Exit code of the coverage gate (0 passed, 1 failed, 2 could not evaluate), empty when not run. value: ${{ steps.gates.outputs.coverage }} doc-drift: description: Exit code of the doc-drift gate, empty when not run. value: ${{ steps.gates.outputs.doc-drift }} security: description: Exit code of the security gate, empty when not run. value: ${{ steps.gates.outputs.security }} health: description: Exit code of the health gate, empty when not run. value: ${{ steps.gates.outputs.health }} risk: description: Exit code of the change-risk gate, empty when not run. value: ${{ steps.gates.outputs.risk }} verdict: description: >- All gates combined: "fail" when any gate failed or could not evaluate, "warn" when any passed with warnings or exited 0 without a result (an older Repowise), else "pass". Empty when no gate ran. value: ${{ steps.summary.outputs.verdict }} sarif-dir: description: Directory holding the SARIF files, when upload-sarif is on. value: ${{ steps.gates.outputs.sarif-dir }} impacted-tests: description: With impacted-tests, the runner arguments for the selected tests, or ":all" to run every test. value: ${{ steps.impacted.outputs.impacted-tests }} run-all-tests: description: >- With impacted-tests, "false" only when the subset is safe to run alone; "true" when every test must run, also when selection could not run. Branch on != 'false' so a skipped step runs everything. value: ${{ steps.impacted.outputs.run-all-tests }} runs: using: composite steps: - uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - name: Install Repowise shell: bash env: REPOWISE_VERSION: ${{ inputs.version }} run: python -m pip install --quiet "repowise${REPOWISE_VERSION:+==$REPOWISE_VERSION}" - name: Select impacted tests id: impacted if: inputs.impacted-tests == 'true' shell: bash working-directory: ${{ inputs.working-directory }} env: BASE: ${{ inputs.base }} RUNNER: ${{ inputs.impacted-tests-runner }} PYTHONUTF8: "1" run: bash "$GITHUB_ACTION_PATH/ci/github/impacted-tests.sh" - name: Run gates id: gates # Test selection alone runs no gate. if: inputs.checks != '' || inputs.impacted-tests != 'true' shell: bash working-directory: ${{ inputs.working-directory }} env: CHECKS: ${{ inputs.checks }} BASE: ${{ inputs.base }} COVERAGE_REPORT: ${{ inputs.coverage-report }} COVERAGE_FAIL_UNDER: ${{ inputs.coverage-fail-under }} COVERAGE_MIN_COVERABLE_LINES: ${{ inputs.coverage-min-coverable-lines }} COVERAGE_FAIL_UNDER_RISKY: ${{ inputs.coverage-fail-under-risky }} COVERAGE_FAIL_UNDER_BRANCHES: ${{ inputs.coverage-fail-under-branches }} COVERAGE_BASE_REPORT: ${{ inputs.coverage-base-report }} COVERAGE_MAX_DROP: ${{ inputs.coverage-max-drop }} DOC_DRIFT_BASELINE: ${{ inputs.doc-drift-baseline }} SECURITY_FAIL_ON: ${{ inputs.security-fail-on }} SECURITY_BASELINE: ${{ inputs.security-baseline }} FAIL_ON_BASELINE_GROWTH: ${{ inputs.fail-on-baseline-growth }} HEALTH_FAIL_ON: ${{ inputs.health-fail-on }} HEALTH_FAIL_AT: ${{ inputs.health-fail-at }} RISK_FAIL_ABOVE_PERCENTILE: ${{ inputs.risk-fail-above-percentile }} SARIF: ${{ inputs.upload-sarif }} SARIF_DIR: ${{ runner.temp }}/repowise-sarif REPOWISE_GATE_SUMMARY_DIR: ${{ runner.temp }}/repowise-gates PYTHONUTF8: "1" run: bash "$GITHUB_ACTION_PATH/ci/github/run-gates.sh" - name: Summarize gates id: summary if: ${{ !cancelled() && steps.gates.outcome != 'skipped' }} shell: bash working-directory: ${{ inputs.working-directory }} env: EXIT_CODES: >- coverage=${{ steps.gates.outputs.coverage }} doc-drift=${{ steps.gates.outputs.doc-drift }} security=${{ steps.gates.outputs.security }} health=${{ steps.gates.outputs.health }} risk=${{ steps.gates.outputs.risk }} SUMMARY_DIR: ${{ runner.temp }}/repowise-gates WORKING_DIRECTORY: ${{ inputs.working-directory }} COMMENT: ${{ inputs.comment }} GH_TOKEN: ${{ inputs.github-token }} PYTHONUTF8: "1" run: bash "$GITHUB_ACTION_PATH/ci/github/summary.sh" - name: Upload SARIF if: always() && inputs.upload-sarif == 'true' && steps.gates.outputs.sarif-dir != '' # A fork's pull request has a read-only token; the gates decide the check. continue-on-error: true uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.gates.outputs.sarif-dir }}