-
Notifications
You must be signed in to change notification settings - Fork 38
163 lines (149 loc) · 7.29 KB
/
Copy pathai-review.yml
File metadata and controls
163 lines (149 loc) · 7.29 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
# Drop into your repo at .github/workflows/ai-review.yml.
# Posts a signed webhook to the AI Review Runner on:
# 1. PR opened from the same repo — automatic first review.
# 2. A trusted user posts one of these slash commands in a PR comment:
# /code-review → review only
# /fix → apply fixes for the latest review verdict
# /code-review-and-fix → review then fix (chained)
#
# Required secrets in the repo (or org):
# RUNNER_HMAC_SECRET — same value the runner has at RUNNER_HMAC_SECRET / _FILE.
# RUNNER_URL — e.g. https://runner.example.com/review
# RUNNER_GH_PAT — fine-grained PAT with:
# Contents: Read (review-only repos)
# Contents: Read & Write (when /fix is enabled — push to PR branch)
# Pull requests: Read & Write
# Issues: Read & Write
# Metadata: Read
# See README / DEPLOY.md for why GITHUB_TOKEN won't work
# (it's revoked when this workflow job ends, but the
# runner needs the token 30–180 s later.)
# RUNNER_TRUSTED_USERS — JSON array string of GitHub logins allowed to re-trigger
# via slash commands. Example: `["lee-to","alice"]`.
# If unset / empty, slash-command triggers are blocked
# and only same-repo `pull_request: opened` events
# start a review.
name: AI Review
on:
pull_request:
types: [opened]
issue_comment:
types: [created]
permissions:
pull-requests: write
contents: read
issues: read
jobs:
ai-review:
# Trigger on:
# - same-repo PR opened (automatic first review)
# - issue_comment on a PR, by a trusted user, containing one of the
# slash commands. Order in the if() matters only for documentation;
# `contains()` is unordered.
# Job-level `if:` cannot read `secrets`, so trusted-users whitelist is
# enforced in the bash step below. Here we only filter by event type +
# slash command structure; non-trusted senders fall through to a no-op
# exit in the step (workflow run still appears in Actions, but skipped).
if: |
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
github.event.sender.type != 'Bot' &&
(contains(github.event.comment.body, '/code-review-and-fix') ||
contains(github.event.comment.body, '/code-review') ||
contains(github.event.comment.body, '/fix')))
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Send signed webhook
env:
RUNNER_URL: ${{ secrets.RUNNER_URL }}
RUNNER_HMAC_SECRET: ${{ secrets.RUNNER_HMAC_SECRET }}
# PAT, NOT secrets.GITHUB_TOKEN — that one expires when the job
# finishes, but the runner needs the token 30–180 s later.
GH_TOKEN: ${{ secrets.RUNNER_GH_PAT }}
# JSON-array string, e.g. ["lee-to","alice"]. Empty/unset → no slash
# commands accepted; only same-repo `pull_request: opened` auto-fires.
TRUSTED_USERS: ${{ secrets.RUNNER_TRUSTED_USERS }}
shell: bash
run: |
set -euo pipefail
REPO="${GITHUB_REPOSITORY}"
if [ -z "${RUNNER_URL:-}" ] || [ -z "${RUNNER_HMAC_SECRET:-}" ] || [ -z "${GH_TOKEN:-}" ]; then
echo "::warning::AI Review runner secrets are unavailable; skipping webhook dispatch"
exit 0
fi
# PR number + head_sha + action differ between event types.
if [ "${{ github.event_name }}" = "pull_request" ]; then
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "::notice::automatic review skipped for fork PR; a trusted maintainer can comment /code-review to run it"
exit 0
fi
PR_NUMBER="${{ github.event.pull_request.number }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
ACTION="review"
else
# issue_comment path — enforce trusted-users whitelist here
# (job-level `if:` can't read secrets in GitHub Actions).
SENDER="${{ github.event.sender.login }}"
TRUSTED_JSON="${TRUSTED_USERS:-[]}"
if ! echo "${TRUSTED_JSON}" | jq -e --arg u "${SENDER}" 'index($u) != null' >/dev/null 2>&1; then
echo "::notice::user '${SENDER}' is not in RUNNER_TRUSTED_USERS — skipping"
exit 0
fi
PR_NUMBER="${{ github.event.issue.number }}"
HEAD_SHA=$(gh pr view "${PR_NUMBER}" \
--repo "${REPO}" \
--json headRefOid -q .headRefOid)
# Map slash → action. Check the LONGEST match first so
# `/code-review` doesn't shadow `/code-review-and-fix`.
COMMENT="${{ github.event.comment.body }}"
if [[ "$COMMENT" == *"/code-review-and-fix"* ]]; then
ACTION="review-and-fix"
elif [[ "$COMMENT" == *"/fix"* ]]; then
ACTION="fix"
elif [[ "$COMMENT" == *"/code-review"* ]]; then
ACTION="review"
else
# `if:` already filtered, but stay defensive.
echo "::warning::no slash command matched; skipping"
exit 0
fi
fi
missing=()
[ -n "${RUNNER_URL:-}" ] || missing+=("RUNNER_URL")
[ -n "${RUNNER_HMAC_SECRET:-}" ] || missing+=("RUNNER_HMAC_SECRET")
[ -n "${GH_TOKEN:-}" ] || missing+=("RUNNER_GH_PAT")
if [ "${#missing[@]}" -gt 0 ]; then
echo "::error::missing required secret(s): ${missing[*]}"
exit 1
fi
TS="$(date +%s)"
# Build payload — keep this single-line JSON; the runner verifies HMAC
# over `<ts>.<rawBody>` byte-for-byte.
BODY="$(jq -nc \
--arg action "${ACTION}" \
--arg repo "${REPO}" \
--argjson pr ${PR_NUMBER} \
--arg sha "${HEAD_SHA}" \
--arg tok "${GH_TOKEN}" \
--argjson ts ${TS} \
'{action: $action, repo: $repo, pr_number: $pr, head_sha: $sha, gh_token: $tok, ts: $ts}')"
# X-Signature = sha256= + HMAC( "<ts>." + BODY )
SIG="sha256=$(printf '%s.%s' "${TS}" "${BODY}" \
| openssl dgst -sha256 -hmac "${RUNNER_HMAC_SECRET}" -hex \
| awk '{print $2}')"
# POST and capture status; fail the workflow on >= 400 from the runner.
HTTP_CODE=$(curl -sS -o /tmp/resp.json -w '%{http_code}' \
-X POST "${RUNNER_URL}" \
-H "Content-Type: application/json" \
-H "X-Signature: ${SIG}" \
-H "X-Timestamp: ${TS}" \
--data-raw "${BODY}")
echo "runner http: ${HTTP_CODE} (action=${ACTION})"
cat /tmp/resp.json || true
if [ "${HTTP_CODE}" -ge 400 ]; then
echo "::error::Runner returned ${HTTP_CODE}"
exit 1
fi