Repository navigation
ci(guardian): a reusable review workflow other repositories call #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e6ca61b
ci(guardian): a reusable review workflow other repositories call
zaebee fce8bd0
fix(guardian): let a caller in another org pass the secrets, and tigh…
zaebee e4bd584
fix(guardian): refuse a backslash in source_root
zaebee bd9eef6
fix(guardian): one source_root rule in shell and Python, portable base64
zaebee 8c85a1d
docs(guardian): name the two steps that read the app token
zaebee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,280 @@ | ||
| name: Guardian Review (reusable) | ||
|
|
||
| # Called by a repository's own guardian.yml on `/guardian review` (#739): | ||
| # | ||
| # jobs: | ||
| # review: | ||
| # if: github.event.issue.pull_request != null && github.event.comment.body == '/guardian review' | ||
| # uses: zaebee/codegraph-brain/.github/workflows/guardian-reusable.yml@<sha> | ||
| # with: | ||
| # pr_number: ${{ github.event.issue.number }} | ||
| # tooling_ref: <the same sha> | ||
| # source_root: src | ||
| # secrets: | ||
| # ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| # GUARDIAN_APP_PRIVATE_KEY: ${{ secrets.GUARDIAN_APP_PRIVATE_KEY }} | ||
| # permissions: {contents: read, pull-requests: write, issues: write} | ||
| # | ||
| # `secrets: inherit` reaches a called workflow only within one organisation or | ||
| # enterprise; any other caller passes the secrets declared below by name. | ||
| # Variables are always the caller's: GUARDIAN_PROVIDER, GUARDIAN_MODEL, | ||
| # GUARDIAN_ANTHROPIC_EFFORT, GUARDIAN_FEATURES, GUARDIAN_SKEPTIC, | ||
| # GUARDIAN_SKEPTIC_MODEL, GUARDIAN_APP_CLIENT_ID. | ||
| # | ||
| # The caller's Actions policy must allow this workflow and the actions it uses | ||
| # (actions/*, astral-sh/setup-uv, peter-evans/create-or-update-comment). | ||
| # Pin `uses:` and `tooling_ref` to the SAME commit SHA, never a tag: the | ||
| # tooling runs on the caller's runner, and a tag can move. | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| pr_number: | ||
| description: The pull request to review. | ||
| type: number | ||
| required: true | ||
| tooling_ref: | ||
| description: >- | ||
| The zaebee/codegraph-brain commit whose tooling runs. Pass the same ref | ||
| the caller's `uses:` names, so the job and its scripts agree. | ||
| type: string | ||
| required: true | ||
| source_root: | ||
| description: The directory inside the reviewed repository to build the graph from. | ||
| type: string | ||
| default: src | ||
| runner: | ||
| description: The job's runs-on, as JSON (a self-hosted label list works). | ||
| type: string | ||
| default: '["ubuntu-latest"]' | ||
| save_metrics: | ||
| description: >- | ||
| Restore guardian_metrics.jsonl from codegraph-brain's own data/guardian-metrics | ||
| branch. For codegraph-brain's caller only; any other caller leaves it false. | ||
| type: boolean | ||
| default: false | ||
| secrets: | ||
| ANTHROPIC_API_KEY: | ||
| required: false | ||
| GEMINI_API_KEY: | ||
| required: false | ||
| MISTRAL_API_KEY: | ||
| required: false | ||
| GUARDIAN_APP_PRIVATE_KEY: | ||
| required: false | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| review: | ||
| runs-on: ${{ fromJSON(inputs.runner) }} | ||
| # Below the app token's one-hour lifetime (#734): a review cannot outlive | ||
| # the token it posts with. | ||
| timeout-minutes: 45 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| issues: write | ||
|
|
||
| steps: | ||
| # Here rather than in each caller, so no caller can forget it. | ||
| - name: Check commenter has write access | ||
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 | ||
| with: | ||
| script: | | ||
| const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| username: context.payload.comment.user.login, | ||
| }); | ||
| const allowed = ['admin', 'write']; | ||
| if (!allowed.includes(perm.permission)) { | ||
| core.setFailed(`@${context.payload.comment.user.login} needs write access to trigger Guardian Review (has: ${perm.permission})`); | ||
| } | ||
|
|
||
| - name: React to trigger comment | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| COMMENT_ID: ${{ github.event.comment.id }} | ||
| run: | | ||
| gh api repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID/reactions \ | ||
| -X POST -f content=eyes | ||
|
|
||
| - name: Validate inputs and get base branch | ||
| env: | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| PR_NUMBER: ${{ inputs.pr_number }} | ||
| SOURCE_ROOT: ${{ inputs.source_root }} | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| if [ "$EVENT_NAME" != issue_comment ]; then | ||
| echo "Call this on issue_comment (a /guardian review comment), not $EVENT_NAME" && exit 1 | ||
| fi | ||
| if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then | ||
| echo "Invalid PR number: $PR_NUMBER" && exit 1 | ||
| fi | ||
| # The same rule as guardian_review.py: `..` only as a path component, | ||
| # no absolute path, drive, backslash or whitespace. | ||
| case "$SOURCE_ROOT" in /*) SOURCE_ROOT_BAD=1 ;; esac | ||
| case "/$SOURCE_ROOT/" in | ||
| */../*|*\\*|*:*|*[[:space:]]*) SOURCE_ROOT_BAD=1 ;; | ||
| esac | ||
| case "${SOURCE_ROOT_BAD:-}" in | ||
| 1) | ||
| echo "source_root must be a plain path inside the repository: '$SOURCE_ROOT'" && exit 1 ;; | ||
| esac | ||
| echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV | ||
| BASE_BRANCH=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json baseRefName --jq '.baseRefName') | ||
| echo "BASE_BRANCH=$BASE_BRANCH" >> $GITHUB_ENV | ||
|
|
||
| # The PR is data, never code (#730). Everything that runs — uv sync, the | ||
| # guardian scripts, cgis itself — comes from codegraph-brain at | ||
| # tooling_ref, checked out here; the PR lands in pr/ and is only diffed, | ||
| # read and parsed. This step and the next keep the job's provider keys | ||
| # out of reach of whoever wrote the PR. | ||
| - name: Checkout trusted tooling (codegraph-brain) | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| with: | ||
| repository: zaebee/codegraph-brain | ||
| ref: ${{ inputs.tooling_ref }} | ||
| persist-credentials: false | ||
|
|
||
| # tooling_ref must name the commit the caller's `uses:` does: the steps | ||
| # here and the scripts they run then agree. An older checkout fails here, | ||
| # before any spend, instead of on an unknown flag. | ||
| - name: Check the tooling matches this workflow | ||
| run: | | ||
| grep -q -- '"--source-root"' scripts/guardian_review.py || { | ||
| echo "tooling_ref predates this workflow: pin uses: and tooling_ref to the same SHA" && exit 1 | ||
| } | ||
|
|
||
| - name: Checkout PR branch as data | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| with: | ||
| repository: ${{ github.repository }} | ||
| # PR_NUMBER validated as digits-only in a previous step | ||
| ref: refs/pull/${{ env.PR_NUMBER }}/head | ||
| path: pr | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| # The caller's GITHUB_TOKEN, passed for this one command through git's | ||
| # environment config (not argv, which other processes on a self-hosted | ||
| # runner can read) and never stored in pr/.git/config. Masked as | ||
| # actions/checkout masks it. A private repository needs it; a public one | ||
| # ignores it. | ||
| - name: Fetch base branch for diff | ||
| working-directory: pr | ||
| env: | ||
| FETCH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| cred=$(printf 'x-access-token:%s' "$FETCH_TOKEN" | base64 | tr -d '\n') | ||
| echo "::add-mask::$cred" | ||
| GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.https://github.com/.extraheader \ | ||
| GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $cred" git fetch origin "$BASE_BRANCH" | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 | ||
|
|
||
| - name: Install dependencies | ||
| run: uv sync --frozen --extra guardian | ||
|
|
||
| # diff_check (#733) reads data keys, which the graph has only when | ||
| # ingested with --data-keys; without the feature the graph is as before. | ||
| - name: Build code graph | ||
| env: | ||
| GUARDIAN_FEATURES: ${{ vars.GUARDIAN_FEATURES }} | ||
| SOURCE_ROOT: ${{ inputs.source_root }} | ||
| run: | | ||
| flags="" | ||
| # Same reading as parse_features: comma-separated, any whitespace ignored. | ||
| case ",$(printf '%s' "$GUARDIAN_FEATURES" | tr -d '[:space:]')," in | ||
| *,diff_check,*) flags="--data-keys" ;; | ||
| esac | ||
| uv run --frozen cgis ingest "pr/$SOURCE_ROOT" --output graph.db $flags | ||
|
|
||
| # Fetches anonymously: the tooling checkout keeps no credentials (#730), | ||
| # and the metrics branch is codegraph-brain's own. Off for other callers. | ||
| - name: Restore metrics from data branch | ||
| if: inputs.save_metrics | ||
| run: | | ||
| git fetch origin data/guardian-metrics 2>/dev/null && \ | ||
| git show origin/data/guardian-metrics:guardian_metrics.jsonl \ | ||
| > guardian_metrics.jsonl 2>/dev/null || true | ||
|
|
||
| # Post as the codegraph-brain GitHub App when it is configured (#734): | ||
| # set vars.GUARDIAN_APP_CLIENT_ID and secrets.GUARDIAN_APP_PRIVATE_KEY. | ||
| # With the variable unset this step is skipped and posting uses | ||
| # GITHUB_TOKEN, as before. With it set, a missing key, a wrong key or an | ||
| # app not installed here fails the job before any LLM call: no silent | ||
| # fallback to another identity. The app's key reaches every repository | ||
| # the app is installed on, so the token is narrowed to this one and to | ||
| # pull-requests: write. Two steps read it, both of which post: "Run | ||
| # Guardian Review", which posts the inline review itself, and the fallback | ||
| # comment. Both run only this repository's tooling, never PR code. It | ||
| # lives one hour; the job's timeout-minutes keeps the review inside that. | ||
| - name: Mint GitHub App token | ||
| id: app-token | ||
| if: vars.GUARDIAN_APP_CLIENT_ID != '' | ||
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | ||
| with: | ||
| client-id: ${{ vars.GUARDIAN_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.GUARDIAN_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| repositories: ${{ github.event.repository.name }} | ||
| permission-pull-requests: write | ||
|
|
||
| - name: Run Guardian Review | ||
| id: guardian | ||
| env: | ||
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | ||
| MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} | ||
| GUARDIAN_PROVIDER: ${{ vars.GUARDIAN_PROVIDER }} | ||
| GUARDIAN_MODEL: ${{ vars.GUARDIAN_MODEL }} | ||
| # Thinking depth for an anthropic finder or skeptic; unset = the | ||
| # provider's default ("low"). #246 J measured the finder at "medium". | ||
| GUARDIAN_ANTHROPIC_EFFORT: ${{ vars.GUARDIAN_ANTHROPIC_EFFORT }} | ||
| GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} | ||
| GITHUB_REPOSITORY: ${{ github.repository }} | ||
| GUARDIAN_FEATURES: ${{ vars.GUARDIAN_FEATURES }} | ||
| GUARDIAN_SKEPTIC: ${{ vars.GUARDIAN_SKEPTIC }} | ||
| GUARDIAN_SKEPTIC_MODEL: ${{ vars.GUARDIAN_SKEPTIC_MODEL }} | ||
| SOURCE_ROOT: ${{ inputs.source_root }} | ||
| # GUARDIAN_EVIDENCE (#401) is deliberately not passed: the evidence | ||
| # pass runs `uv run mypy/ruff` inside the reviewed checkout, which is | ||
| # the PR, while this step holds the provider keys (#730). | ||
| # guardian_review.py refuses that combination with --repo-root. | ||
| run: | | ||
| uv run --frozen python scripts/guardian_review.py \ | ||
| --output guardian_report.md \ | ||
| --db graph.db \ | ||
| --pr "$PR_NUMBER" \ | ||
| --metrics guardian_metrics.jsonl \ | ||
| --record-finder guardian_finder.json \ | ||
| --inline \ | ||
| --repo-root pr \ | ||
| --source-root "$SOURCE_ROOT" \ | ||
| --base-branch "$BASE_BRANCH" | ||
|
|
||
| - name: Upload metrics artifact | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | ||
| with: | ||
| name: guardian-metrics-${{ env.PR_NUMBER }} | ||
| path: guardian_metrics.jsonl | ||
| retention-days: 7 | ||
|
|
||
| - name: Upload finder recording artifact | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | ||
| with: | ||
| name: guardian-finder-${{ env.PR_NUMBER }} | ||
| path: guardian_finder.json | ||
| retention-days: 7 | ||
|
|
||
| - name: Post review as PR comment | ||
| if: steps.guardian.outputs.posted_inline != 'true' | ||
| uses: peter-evans/create-or-update-comment@a111a2c3bacd7be7898ee22d0d71d9aec2bb972c # v4 | ||
| with: | ||
| token: ${{ steps.app-token.outputs.token || github.token }} | ||
| issue-number: ${{ env.PR_NUMBER }} | ||
| body-path: guardian_report.md | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.