Skip to content

Commit 852297e

Browse files
authored
Merge pull request #35 from 777genius/feat/revision-aware-review-evidence
feat(review): add revision-aware T0 runtime
2 parents be133b0 + 3c75bf1 commit 852297e

109 files changed

Lines changed: 114493 additions & 36240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
name: ReviewRouter Execution Internal
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
review_action_lane:
7+
description: 'Internal execution lane: legacy or t0'
8+
required: true
9+
type: string
10+
runtime_ref:
11+
required: true
12+
type: string
13+
api_url:
14+
required: true
15+
type: string
16+
runtime_config_mode:
17+
required: true
18+
type: string
19+
static_runtime_env_json:
20+
required: true
21+
type: string
22+
pr_number:
23+
required: false
24+
type: string
25+
default: ''
26+
review_app_client_id:
27+
required: false
28+
type: string
29+
default: ''
30+
review_app_repository:
31+
required: false
32+
type: string
33+
default: ''
34+
provider_instance_id:
35+
required: false
36+
type: string
37+
default: ''
38+
workflow_schema_version:
39+
required: true
40+
type: number
41+
review_timeout_minutes:
42+
required: true
43+
type: number
44+
max_changed_lines:
45+
required: true
46+
type: string
47+
secrets:
48+
REVIEW_ROUTER_LEDGER_KEY:
49+
required: false
50+
REVIEW_THREAD_LIFECYCLE_RESOLVE_TOKEN:
51+
required: false
52+
REVIEW_APP_PRIVATE_KEY:
53+
required: false
54+
CODEX_AUTH_JSON:
55+
required: false
56+
CODEX_CONFIG_TOML:
57+
required: false
58+
CLAUDE_CODE_OAUTH_TOKEN:
59+
required: false
60+
OPENAI_API_KEY:
61+
required: false
62+
OPENROUTER_API_KEY:
63+
required: false
64+
65+
jobs:
66+
review:
67+
name: review
68+
runs-on: ubuntu-latest
69+
timeout-minutes: ${{ inputs.review_timeout_minutes }}
70+
env:
71+
RR_REVIEW_ACTION_LANE: ${{ inputs.review_action_lane }}
72+
RR_RUNTIME_REF: ${{ inputs.runtime_ref }}
73+
RR_STATIC_RUNTIME_ENV_JSON: ${{ inputs.static_runtime_env_json }}
74+
RR_WORKFLOW_REPOSITORY: ${{ job.workflow_repository }}
75+
RR_WORKFLOW_SHA: ${{ job.workflow_sha }}
76+
REVIEWROUTER_API_URL: ${{ inputs.api_url }}
77+
REVIEWROUTER_OIDC_AUDIENCE: reviewrouter
78+
REVIEWROUTER_RUNTIME_CONFIG_MODE: ${{ inputs.runtime_config_mode }}
79+
REVIEWROUTER_STATIC_CONFIG_FALLBACK: 'true'
80+
CODEX_AUTH_JSON_PRESENT: ${{ secrets.CODEX_AUTH_JSON != '' && '1' || '0' }}
81+
CLAUDE_CODE_OAUTH_TOKEN_PRESENT: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN != '' && '1' || '0' }}
82+
OPENAI_API_KEY_PRESENT: ${{ secrets.OPENAI_API_KEY != '' && '1' || '0' }}
83+
REVIEW_APP_PRIVATE_KEY_PRESENT: ${{ secrets.REVIEW_APP_PRIVATE_KEY != '' && '1' || '0' }}
84+
steps:
85+
- name: Prepare ReviewRouter runtime settings
86+
id: runtime
87+
shell: bash
88+
run: |
89+
set -euo pipefail
90+
node <<'NODE'
91+
const crypto = require('node:crypto');
92+
const fs = require('node:fs');
93+
94+
const fail = (message) => {
95+
console.error(`::error::${message}`);
96+
process.exit(1);
97+
};
98+
99+
const reviewActionLane = (process.env.RR_REVIEW_ACTION_LANE || '').trim();
100+
if (!['legacy', 't0'].includes(reviewActionLane)) {
101+
fail('Invalid internal Review Action execution lane.');
102+
}
103+
104+
const requestedRuntimeRef = (process.env.RR_RUNTIME_REF || '').trim();
105+
if (!/^(main|v1|v1\.[0-9]+\.[0-9]+|[a-fA-F0-9]{40})$/.test(requestedRuntimeRef)) {
106+
fail('Invalid ReviewRouter runtime_ref. Use main, v1, v1.x.x, or a 40-character commit SHA.');
107+
}
108+
const runtimeRepository =
109+
reviewActionLane === 't0'
110+
? (process.env.RR_WORKFLOW_REPOSITORY || '').trim()
111+
: '777genius/review-router';
112+
const runtimeRef =
113+
reviewActionLane === 't0'
114+
? (process.env.RR_WORKFLOW_SHA || '').trim().toLowerCase()
115+
: requestedRuntimeRef;
116+
if (reviewActionLane === 't0') {
117+
if (runtimeRepository !== '777genius/review-router') {
118+
fail('The T0 runtime must execute from the ReviewRouter reusable workflow repository.');
119+
}
120+
if (!/^[a-f0-9]{40}$/.test(runtimeRef)) {
121+
fail('The T0 runtime requires the immutable reusable workflow commit SHA.');
122+
}
123+
}
124+
125+
const runtimeConfigMode = (process.env.REVIEWROUTER_RUNTIME_CONFIG_MODE || '').trim();
126+
if (!['oidc', 'static'].includes(runtimeConfigMode)) {
127+
fail('Invalid ReviewRouter runtime_config_mode. Use oidc or static.');
128+
}
129+
130+
const githubEnv = process.env.GITHUB_ENV;
131+
const githubOutput = process.env.GITHUB_OUTPUT;
132+
if (!githubEnv || !githubOutput) {
133+
fail('GitHub environment files are unavailable.');
134+
}
135+
136+
const appendEnv = (key, value) => {
137+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
138+
fail(`Invalid static runtime env key: ${key}`);
139+
}
140+
const text = String(value);
141+
if (text.includes('\0')) {
142+
fail(`Invalid static runtime env value for ${key}`);
143+
}
144+
const delimiter = `RR_ENV_${key}_${crypto.randomUUID().replace(/-/g, '')}`;
145+
fs.appendFileSync(githubEnv, `${key}<<${delimiter}\n${text}\n${delimiter}\n`);
146+
};
147+
148+
appendEnv('REVIEWROUTER_ACTION_VERSION', runtimeRef);
149+
appendEnv(
150+
'REVIEWROUTER_COMMENT_TOKEN_MODE',
151+
runtimeConfigMode === 'oidc' ? 'app-oidc' : 'github-token'
152+
);
153+
154+
let staticEnv;
155+
try {
156+
staticEnv = JSON.parse(process.env.RR_STATIC_RUNTIME_ENV_JSON || '{}');
157+
} catch (error) {
158+
fail(`static_runtime_env_json is not valid JSON: ${error.message}`);
159+
}
160+
if (!staticEnv || typeof staticEnv !== 'object' || Array.isArray(staticEnv)) {
161+
fail('static_runtime_env_json must be a JSON object.');
162+
}
163+
if (staticEnv.FAIL_ON_NO_HEALTHY_PROVIDERS === undefined) {
164+
staticEnv.FAIL_ON_NO_HEALTHY_PROVIDERS = 'true';
165+
}
166+
const staticRuntimeEnvAllowlist = new Set(['TARGET_TOKENS_PER_BATCH']);
167+
const isSecretLikeStaticRuntimeEnvKey = (key) =>
168+
!staticRuntimeEnvAllowlist.has(key) &&
169+
/(TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|AUTH_JSON)/.test(key);
170+
for (const [key, value] of Object.entries(staticEnv)) {
171+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
172+
fail(`Invalid static runtime env key: ${key}`);
173+
}
174+
if (key === 'REVIEWROUTER_ACTION_V2_MODE') {
175+
fail('Set review_action_v2_mode on the reusable workflow instead of static_runtime_env_json.');
176+
}
177+
if (isSecretLikeStaticRuntimeEnvKey(key)) {
178+
fail(`Static runtime env cannot contain secret-like key: ${key}`);
179+
}
180+
if (typeof value !== 'string') {
181+
fail(`Static runtime env value must be a string: ${key}`);
182+
}
183+
appendEnv(key, value);
184+
}
185+
186+
const appTokenNeeded =
187+
reviewActionLane === 'legacy' &&
188+
runtimeConfigMode === 'static' &&
189+
process.env.REVIEW_APP_PRIVATE_KEY_PRESENT === '1' &&
190+
(process.env.RR_REVIEW_APP_CLIENT_ID || '').trim().length > 0;
191+
const eventName = process.env.GITHUB_EVENT_NAME || '';
192+
const isForkPullRequest =
193+
eventName === 'pull_request' &&
194+
process.env.GITHUB_HEAD_REPO_FULL_NAME !== process.env.GITHUB_REPOSITORY;
195+
const isMergeGroup = eventName === 'merge_group';
196+
fs.appendFileSync(githubOutput, `runtime_ref=${runtimeRef}\n`);
197+
fs.appendFileSync(githubOutput, `runtime_repository=${runtimeRepository}\n`);
198+
fs.appendFileSync(githubOutput, `can_run=${!isForkPullRequest && !isMergeGroup ? 'true' : 'false'}\n`);
199+
fs.appendFileSync(githubOutput, `skip_reason=${isMergeGroup ? 'merge_group' : isForkPullRequest ? 'fork' : ''}\n`);
200+
fs.appendFileSync(githubOutput, `app_token_needed=${appTokenNeeded ? 'true' : 'false'}\n`);
201+
NODE
202+
env:
203+
GITHUB_HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name || '' }}
204+
RR_REVIEW_APP_CLIENT_ID: ${{ inputs.review_app_client_id }}
205+
206+
- name: Skip unsupported review context
207+
if: ${{ steps.runtime.outputs.can_run == 'false' }}
208+
shell: bash
209+
run: |
210+
if [ "${{ steps.runtime.outputs.skip_reason }}" = "merge_group" ]; then
211+
echo "ReviewRouter merge queue check passed. Full review runs on pull_request events where a PR number is available."
212+
else
213+
echo "ReviewRouter skipped this fork pull request because secret-backed provider execution is disabled by default."
214+
fi
215+
216+
- name: Checkout pull request code
217+
if: ${{ steps.runtime.outputs.can_run == 'true' }}
218+
uses: actions/checkout@v6
219+
with:
220+
persist-credentials: false
221+
222+
- name: Checkout ReviewRouter runtime
223+
if: ${{ steps.runtime.outputs.can_run == 'true' }}
224+
uses: actions/checkout@v6
225+
with:
226+
repository: ${{ steps.runtime.outputs.runtime_repository }}
227+
ref: ${{ steps.runtime.outputs.runtime_ref }}
228+
path: .reviewrouter-runtime
229+
persist-credentials: false
230+
231+
- name: Setup Node.js
232+
if: ${{ steps.runtime.outputs.can_run == 'true' }}
233+
uses: actions/setup-node@v6
234+
with:
235+
node-version: '24'
236+
237+
- name: Resolve ReviewRouter runtime provider tooling
238+
id: provider-tooling
239+
if: ${{ steps.runtime.outputs.can_run == 'true' }}
240+
shell: bash
241+
env:
242+
REVIEW_ROUTER_MODE: runtime-preflight
243+
run: node .reviewrouter-runtime/dist/index.js
244+
245+
- name: Create ReviewRouter GitHub App token
246+
id: app-token
247+
if: ${{ inputs.review_action_lane == 'legacy' && steps.runtime.outputs.can_run == 'true' && steps.runtime.outputs.app_token_needed == 'true' }}
248+
uses: actions/create-github-app-token@v3
249+
with:
250+
client-id: ${{ inputs.review_app_client_id }}
251+
private-key: ${{ secrets.REVIEW_APP_PRIVATE_KEY }}
252+
owner: ${{ github.repository_owner }}
253+
repositories: ${{ inputs.review_app_repository }}
254+
permission-contents: read
255+
permission-issues: write
256+
permission-pull-requests: write
257+
258+
- name: Install Codex CLI
259+
if: ${{ steps.runtime.outputs.can_run == 'true' && steps.provider-tooling.outputs.codex_cli_needed == 'true' }}
260+
shell: bash
261+
run: npm install -g @openai/codex@0.125.0
262+
263+
- name: Install Claude Code CLI
264+
if: ${{ steps.runtime.outputs.can_run == 'true' && steps.provider-tooling.outputs.claude_cli_needed == 'true' }}
265+
shell: bash
266+
run: |
267+
curl -fsSL https://claude.ai/install.sh | bash
268+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
269+
270+
- name: Restore Codex subscription auth
271+
if: ${{ steps.runtime.outputs.can_run == 'true' && steps.provider-tooling.outputs.codex_oauth_needed == 'true' && env.CODEX_AUTH_JSON_PRESENT == '1' }}
272+
shell: bash
273+
env:
274+
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
275+
CODEX_CONFIG_TOML: ${{ secrets.CODEX_CONFIG_TOML }}
276+
run: |
277+
set -euo pipefail
278+
node <<'NODE'
279+
const payload = process.env.CODEX_AUTH_JSON || '';
280+
const fail = (message) => {
281+
console.error('::error::' + message);
282+
process.exit(1);
283+
};
284+
const warn = (message) => {
285+
console.error('::warning::' + message);
286+
};
287+
let auth;
288+
try {
289+
auth = JSON.parse(payload);
290+
} catch (error) {
291+
fail('CODEX_AUTH_JSON is not valid JSON. reseed auth.json from a trusted machine. ' + error.message);
292+
}
293+
if (auth.auth_mode !== 'chatgpt') {
294+
fail('CODEX_AUTH_JSON auth_mode must be chatgpt. reseed auth.json with Codex CLI subscription login or switch this repo to API-key mode.');
295+
}
296+
if (!auth.tokens || typeof auth.tokens.refresh_token !== 'string' || auth.tokens.refresh_token.length === 0) {
297+
fail('CODEX_AUTH_JSON tokens.refresh_token is missing. Run codex login on a trusted machine and reseed auth.json.');
298+
}
299+
if (!auth.last_refresh) {
300+
warn('CODEX_AUTH_JSON last_refresh is missing. If Codex later fails with an auth error, run codex login on a trusted machine and reseed auth.json.');
301+
}
302+
NODE
303+
export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
304+
mkdir -p "$CODEX_HOME"
305+
chmod 700 "$CODEX_HOME"
306+
printf '%s' "$CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json"
307+
chmod 600 "$CODEX_HOME/auth.json"
308+
if [ -n "${CODEX_CONFIG_TOML:-}" ]; then
309+
printf '%s' "$CODEX_CONFIG_TOML" > "$CODEX_HOME/config.toml"
310+
chmod 600 "$CODEX_HOME/config.toml"
311+
fi
312+
313+
- name: Run ReviewRouter T0
314+
if: ${{ inputs.review_action_lane == 't0' && steps.runtime.outputs.can_run == 'true' }}
315+
shell: bash
316+
env:
317+
REVIEWROUTER_ACTION_V2_MODE: t0
318+
REVIEW_ROUTER_MODE: codex-oauth-rotating
319+
INPUT_API_URL: ${{ inputs.api_url }}
320+
INPUT_PROVIDER_INSTANCE_ID: ${{ inputs.provider_instance_id }}
321+
INPUT_WORKFLOW_SCHEMA_VERSION: ${{ inputs.workflow_schema_version }}
322+
INPUT_MAX_CHANGED_LINES: ${{ inputs.max_changed_lines }}
323+
INPUT_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
324+
INPUT_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
325+
INPUT_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
326+
REVIEW_ROUTER_LEDGER_KEY: ${{ secrets.REVIEW_ROUTER_LEDGER_KEY }}
327+
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
328+
REVIEW_ROUTER_MEMORY_ENABLED: 'true'
329+
REVIEW_ROUTER_MEMORY_PROTOCOL_VERSION: '1'
330+
REVIEW_ROUTER_MEMORY_BUNDLE_ENDPOINT: /api/action/v1/memory
331+
CODEX_CONFIG_TOML: ${{ secrets.CODEX_CONFIG_TOML }}
332+
run: node .reviewrouter-runtime/dist/index.js
333+
334+
- name: Run ReviewRouter legacy
335+
if: ${{ inputs.review_action_lane == 'legacy' && steps.runtime.outputs.can_run == 'true' }}
336+
shell: bash
337+
env:
338+
REVIEWROUTER_ACTION_V2_MODE: disabled
339+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
340+
REVIEW_ROUTER_LEDGER_KEY: ${{ secrets.REVIEW_ROUTER_LEDGER_KEY }}
341+
REVIEW_THREAD_LIFECYCLE_RESOLVE_TOKEN: ${{ secrets.REVIEW_THREAD_LIFECYCLE_RESOLVE_TOKEN }}
342+
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
343+
REVIEW_ROUTER_MEMORY_ENABLED: 'true'
344+
REVIEW_ROUTER_MEMORY_PROTOCOL_VERSION: '1'
345+
REVIEW_ROUTER_MEMORY_BUNDLE_ENDPOINT: /api/action/v1/memory
346+
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
347+
CODEX_CONFIG_TOML: ${{ secrets.CODEX_CONFIG_TOML }}
348+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
349+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
350+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
351+
run: node .reviewrouter-runtime/dist/index.js

0 commit comments

Comments
 (0)