-
Notifications
You must be signed in to change notification settings - Fork 25.2k
179 lines (167 loc) · 9.27 KB
/
Copy pathexpo-code-review-command.yml
File metadata and controls
179 lines (167 loc) · 9.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# @ref LLP 0009#workflow-security-posture — issue_comment is not fork-restricted; runs with full secrets regardless of PR origin
name: AI code review (command)
# On-demand, ONE-SHOT reviewer triggered by a PR comment (maintainers only):
# /review run once now; the router picks the agents
# /review all run once with every agent
# /review correctness security run once with just those agents
# This never changes configuration. CONTINUOUS review is configured in
# expo-code-review.yml (the `pull_request` workflow) via the `review.trigger`
# policy in .expo-code-review/config.jsonc and the `ai-review:skip` label.
on:
issue_comment:
types: [created]
# Comment-only: read the repo, write PR comments (issue comments API).
permissions:
contents: read
pull-requests: write
issues: write
env:
# Published reviewer run via npx (override with repo variable ECR_VERSION; pin to
# a specific version to freeze it). Used for the guard AND the review so the engine
# that clears a config is the same engine that then reads it.
ECR_VERSION: ${{ vars.ECR_VERSION || '0.14.0' }}
concurrency:
group: ai-code-review-cmd-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
command:
# Only PR comments starting with /review, from a maintainer.
# @ref LLP 0009#workflow-security-posture [implements] — gate controls who triggers, not what code runs
if: >-
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/review') &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
# Bound the run so a slow/stalled review fails fast rather than hanging. Keep it
# above the passes budget (budget.totalPassesMinutes, 55m) + coordinator (10m) +
# verification + setup, like the auto-review workflow's cap.
timeout-minutes: 90
# A reviewer failure must never fail the PR's checks.
continue-on-error: true
steps:
# @ref LLP 0009#workflow-security-posture [implements] — comment body only via env:; agent ids sanitized before reaching argv
- name: Parse command
id: cmd
env:
# Via env (never inline ${{ }}) so an untrusted comment can't inject shell.
COMMENT: ${{ github.event.comment.body }}
run: |
line=$(printf '%s' "$COMMENT" | head -n1 | tr -d '\r')
verb=$(printf '%s' "$line" | awk '{print $1}')
rest=$(printf '%s' "$line" | cut -s -d' ' -f2-)
# Only /review (one-shot). Continuous review is policy/label-driven, not a
# comment; /review no longer changes any configuration.
if [ "$verb" != "/review" ]; then
echo "run=false" >> "$GITHUB_OUTPUT"; exit 0
fi
# Bare "/review" -> router picks; "all" -> every agent; names -> subset.
# Sanitize agent ids to [a-zA-Z0-9,_-] to keep the value shell-safe.
agents=""
route=false
if [ -z "$rest" ]; then
route=true
elif [ "$rest" != "all" ]; then
agents=$(printf '%s' "$rest" | tr ' ' ',' | tr -cd 'a-zA-Z0-9,_-')
fi
{
echo "run=true"
echo "agents=$agents"
echo "route=$route"
} >> "$GITHUB_OUTPUT"
- name: Acknowledge
if: steps.cmd.outputs.run == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh api -X POST "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes
# SECURITY: `issue_comment` is NOT fork-restricted by GitHub — it always
# runs in the base-repo context with full secrets and a write-scoped token,
# regardless of whether the commented-on PR is from a fork. We check out ONLY
# the trusted base ref (the default branch) for the `.expo-code-review/`
# config, and never `gh pr checkout` the PR head. The reviewer engine itself
# is the PUBLISHED @expo/code-review-cli (fetched by npx), not built from any
# checkout, so attacker-controlled PR code never runs here. The diff + PR
# metadata come from the API (`gh pr diff`/`gh pr view`); `ecr ci` loads
# configuration from the PR's immutable base commit and reads source from a
# head worktree scrubbed of ambient runtime config (opencode.json, plugins,
# AGENTS.md, .env, …) — the same trust model as the pull_request workflow.
- name: Checkout (base ref only — never the PR head)
if: steps.cmd.outputs.run == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
# The CLI's own git fetches authenticate through `gh` from GH_TOKEN, so
# the token never lands in .git/config.
persist-credentials: false
- name: Set up Node
if: steps.cmd.outputs.run == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
# The reviewer runs via npx and never installs with a package manager, so
# disable setup-node's auto package-manager cache (its post step would try
# to save an empty cache and error).
package-manager-cache: false
# SECURITY: the base-ref checkout above includes every .expo-code-review/
# config.jsonc + routing.jsonc, whose auth.tokenEnv names the env var the CLI
# forwards as the model credential. The canonical guard ships with the CLI:
# `ecr verify-config` sweeps every config (root + routing + all scopes, referenced
# or not) with the engine's real JSONC parser and refuses unless tokenEnv appears
# exactly once, in a ROOT-owned file, equal to ECR_EXPECTED_TOKEN_ENV — so a
# base-ref config change can't repoint it at another runner secret, sneak in a
# JSON-escaped key, or stage an unreferenced scope config with its own auth.
# This is layer 2; layer 1 is the runtime ECR_EXPECTED_TOKEN_ENV lock in `ecr ci`.
# Runs after Set up Node so the guard runs the SAME $ECR_VERSION `ecr ci` will.
# @ref LLP 0009#guard-step-ordering-and-job-budgets [implements] — same $ECR_VERSION feeds guard and review
- name: Guard config tokenEnv (root + routing + all scopes)
if: steps.cmd.outputs.run == 'true'
env:
# (Comma-separated set for a multi-credential auth.providers config.)
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'META_API_KEY' }}
run: npx --yes -p "@expo/code-review-cli@$ECR_VERSION" ecr verify-config
- name: Run AI review
if: steps.cmd.outputs.run == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Layer-1 auth lock: the CLI refuses to run when the tokenEnv it would honor
# differs from this. Keep it in sync with the guard's EXPECTED.
ECR_EXPECTED_TOKEN_ENV: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'META_API_KEY' }}
# Model credential — the env var named by auth.tokenEnv in config.jsonc.
# Store each as a repo secret under the same name.
# Updated per your setting: secret is EXPO_CODE_REVIEW_API_KEY, forwarded as META_API_KEY
META_API_KEY: ${{ secrets.EXPO_CODE_REVIEW_API_KEY }}
# Optional search-only credential for trusted platform documentation research.
BRAVE_SEARCH_API_KEY: ${{ secrets.EXPO_BRAVE_SEARCH_API_KEY }}
# Optional: override the model for every agent.
REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}
AGENTS: ${{ steps.cmd.outputs.agents }}
ROUTE: ${{ steps.cmd.outputs.route }}
# NOTE: running via `issue_comment` makes this a manual /review, which the CLI
# detects (GITHUB_EVENT_NAME=issue_comment) and treats as a trigger-gate bypass
# — it reviews even when the config trigger policy or an `ai-review:skip` label
# would skip the auto workflow. The bypass affects ONLY the trigger gate; the
# config guard above, break-glass, and the auth lock still apply.
run: |
# Array (not a string) so the flags expand as separate argv entries
# without unquoted word-splitting. AGENTS is a single sanitized,
# space-free comma list, so it stays one element.
ARGS=()
if [ -n "$AGENTS" ]; then
ARGS=(--agents "$AGENTS")
elif [ "$ROUTE" = "true" ]; then
ARGS=(--route)
fi
npx --yes -p "@expo/code-review-cli@$ECR_VERSION" ecr ci "${ARGS[@]}"
# Same ephemeral per-run log as the pull_request workflow — a /review command
# runs the full `ecr ci`, whose .expo-code-review/.runs/ log is gone when the
# runner tears down. always() captures it even on error, gated on run=='true'
# (a non-/review comment writes no log); issue.number IS the PR number here
# (issue_comment context has no pull_request.number).
- name: Upload review run log
if: always() && steps.cmd.outputs.run == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: review-run-log-pr${{ github.event.issue.number }}
path: .expo-code-review/.runs/reviews.jsonl
if-no-files-found: ignore
retention-days: 14