# GitHub Action: Risk-Scored PR Review code-review-graph ships a composite GitHub Action (`action.yml` at the repo root) that posts a risk-scored, graph-aware review comment on every pull request — think of it as a hosted AI review bot (Greptile-style), except the analysis is **local-first**: the knowledge graph is built and queried entirely on your CI runner, and no source code is sent to any external service. On each PR run the action: 1. Installs `code-review-graph` from PyPI. 2. Restores the cached `.code-review-graph/` SQLite graph (or builds it from scratch on a cache miss) and incrementally re-parses the files changed by the PR. 3. Runs `code-review-graph detect-changes --base origin/` to get risk-scored functions, affected execution flows, and test gaps. 4. Renders a markdown report (via `scripts/render_pr_comment.py`) and upserts a single sticky PR comment — the same comment is updated on every push, so the PR thread is never spammed. 5. Optionally fails the job when the overall risk score crosses a threshold (`fail-on-risk`). ## Quick start (external repositories) ```yaml # .github/workflows/code-review-graph.yml name: code-review-graph on: pull_request: permissions: contents: read pull-requests: write jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: tirth8205/code-review-graph@v2.3.6 with: github-token: ${{ secrets.GITHUB_TOKEN }} ``` That is the whole setup. The default `GITHUB_TOKEN` provided by Actions is sufficient — no PAT, no API key, no third-party service. Self-hosted runners must be version `2.327.1` or newer. The composite action uses Node 24-based GitHub actions, including `actions/setup-python@v6`, `actions/cache@v6`, and the recommended `actions/checkout@v7` example. To turn the review into a merge gate: ```yaml - uses: tirth8205/code-review-graph@v2.3.6 with: github-token: ${{ secrets.GITHUB_TOKEN }} fail-on-risk: high ``` ## Inputs | Input | Required | Default | Description | |-------|----------|---------|-------------| | `github-token` | yes | — | Token used to post the sticky PR comment via the GitHub API. The workflow's default `GITHUB_TOKEN` works when the job has `pull-requests: write`. | | `comment` | no | `true` | Post (and keep updated) the sticky PR comment. Set to `false` to run analysis/gating without commenting. | | `fail-on-risk` | no | `none` | Fail the job when the overall risk score reaches a level: `none` (never fail), `high` (risk ≥ 0.70), `critical` (risk ≥ 0.85). | | `python-version` | no | `3.12` | Python version used to run code-review-graph (3.10+ supported). | ## Outputs | Output | Description | |--------|-------------| | `comment-file` | Runner-local path to the rendered markdown report. Use with `comment: false` when a separate trusted workflow will publish it. | ### Risk levels `detect-changes` produces a 0.0–1.0 overall risk score (max across changed functions; see `code_review_graph/changes.py:compute_risk_score` for the scoring factors: flow participation, community crossing, test coverage, security-sensitive names, caller count). The action maps it to levels: | Level | Score | |-------|-------| | low | < 0.40 | | medium | 0.40 – 0.69 | | high | 0.70 – 0.84 | | critical | ≥ 0.85 | ## What the comment contains - **Overall risk** score and level, with counts of changed functions, affected flows, and test gaps. - **Risk-scored changes** — a table of the top changed symbols ordered by risk, with file:line locations and test-coverage status. - **Affected execution flows** — which entry-point flows the change touches, ordered by criticality. - **Test gaps** — changed functions with no direct test coverage. - **Token savings** — how many tokens the graph-backed report saved versus reading every changed file in full. This is the same `context_savings` estimate the CLI's Token Savings panel shows (a `chars / 4` approximation labelled `estimated: true` — see [REPRODUCING.md](REPRODUCING.md) for the calibration methodology). - A `Powered by code-review-graph` footer. The comment starts with a hidden HTML marker (``). The action looks the marker up via `gh api` on each run and PATCHes the existing comment instead of creating a new one (a "sticky" comment). ## Cache behavior The action caches the `.code-review-graph/` directory (the SQLite graph database) with `actions/cache`: - **Key**: `code-review-graph-schema9--`, where the lockfile hash covers common Python/JS/Go/Rust/Ruby/PHP lockfiles (`uv.lock`, `poetry.lock`, `requirements*.txt`, `package-lock.json`, `go.sum`, `Cargo.lock`, …). - **Schema segment**: `schema9` tracks the database schema version (`LATEST_VERSION` in `code_review_graph/migrations.py`). It is bumped when the schema changes so stale caches are never restored across incompatible versions. - **Restore keys**: fall back to any cache for the same OS and schema, so a lockfile change still reuses the previous graph. - **On cache hit**: the action runs `code-review-graph update --base origin/`, which re-parses only the files that differ from the PR's base ref. If the restored database turns out to be unusable, it falls back to a full `build`. - **On cache miss**: a full `code-review-graph build` runs (one-time cost; subsequent PR runs are incremental). ## Security notes - **Token scope**: direct commenting needs `contents: read` for checkout and `pull-requests: write` to post the comment. In the split fork-safe setup, the analysis workflow needs only `contents: read`; the trusted commenter needs only `actions: read` and `pull-requests: write`. Grant exactly those permissions in each workflow. - **Local-first**: analysis runs entirely on the runner. No code, diff, or metadata leaves GitHub's infrastructure; there is no external API, account, or key. - **Untrusted input**: all dynamic values (`github.base_ref`, the PR number, action inputs) are passed to scripts through environment variables, never interpolated into shell commands. The markdown renderer escapes table/markup characters and strips control characters from symbol names and file paths before they reach the comment body, on top of the server-side `_sanitize_name()` sanitization. - **Pinning**: when consuming the action from another repository, pin `uses:` to a release tag or commit SHA rather than `@main`. - **Fork PRs**: `pull_request` runs from forks receive a read-only `GITHUB_TOKEN`, so they cannot post the comment directly. Use an unprivileged `pull_request` workflow with `comment: false`, upload the `comment-file` as an artifact, and publish it from a separate trusted `workflow_run` workflow. See [`.github/workflows/pr-review.yml`](../.github/workflows/pr-review.yml) and [`.github/workflows/pr-review-comment.yml`](../.github/workflows/pr-review-comment.yml). GitHub loads the `workflow_run` workflow from the default branch, so the trusted commenting half becomes active only after that workflow is merged. The privileged workflow must verify the source event and analyzed commit, extract only under `runner.temp`, cap and validate the artifact, and add its own sticky marker before posting. Avoid `pull_request_target` with a checkout of PR code because it can execute untrusted code with a privileged token ([details](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/)). ## Dogfooding This repository runs the action on its own PRs via [`.github/workflows/pr-review.yml`](../.github/workflows/pr-review.yml), which runs the local `action.yml` without write permissions and uploads the rendered report. The trusted [`pr-review-comment.yml`](../.github/workflows/pr-review-comment.yml) workflow validates that artifact and posts the sticky comment without checking out or executing PR-controlled code. ## Rendering script The markdown rendering and risk gating logic lives in [`scripts/render_pr_comment.py`](../scripts/render_pr_comment.py) (stdlib only, unit-tested in `tests/test_action_render.py`) rather than inline YAML, so it can be tested and reused: ```bash code-review-graph detect-changes --base origin/main | \ python scripts/render_pr_comment.py # markdown to stdout python scripts/render_pr_comment.py --input report.json \ --fail-on-risk high --quiet # gate only: exit 3 on breach ```