@clearlydev/nx-archweave 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,8 @@
2
2
 
3
3
  // src/bin/review-verdict.ts
4
4
  import { spawnSync } from "node:child_process";
5
- import { realpathSync } from "node:fs";
5
+ import { randomBytes } from "node:crypto";
6
+ import { appendFileSync, realpathSync } from "node:fs";
6
7
 
7
8
  // ../review-policy/dist/schema/review-finding.js
8
9
  function computeVerdict(findings) {
@@ -231,7 +232,19 @@ var realExec = (command, args, stdin) => {
231
232
  const result = spawnSync(command, args, { encoding: "utf-8", input: stdin });
232
233
  return { status: result.status ?? null, stdout: result.stdout ?? "" };
233
234
  };
234
- function runReviewVerdictBin(env, exec) {
235
+ var realWriteOutput = (path, text) => appendFileSync(path, text);
236
+ function outputModePath(env) {
237
+ return env["ARCHWEAVE_VERDICT_OUTPUT"] === "github-output" && env["GITHUB_OUTPUT"] ? env["GITHUB_OUTPUT"] : void 0;
238
+ }
239
+ function renderStepOutputs(commentBody, previousId, blocked) {
240
+ let delimiter;
241
+ do
242
+ delimiter = `ARCHWEAVE_${randomBytes(8).toString("hex")}`;
243
+ while (commentBody.split("\n").includes(delimiter));
244
+ return [`comment-body<<${delimiter}`, commentBody, delimiter, `previous-comment-id=${previousId ?? ""}`, `blocked=${blocked}`, ""].join("\n");
245
+ }
246
+ var UNREAD_COMMENTS_ERROR = "::error title=Review verdict not computed::Could not read PR comments, so the verdict could not be computed. Re-run the failed jobs to retry. If it fails again, check that the review job has pull-requests: read.";
247
+ function runReviewVerdictBin(env, exec, writeOutput = realWriteOutput) {
235
248
  const logs = [];
236
249
  const repo = env["GITHUB_REPOSITORY"];
237
250
  const pr = env["ARCHWEAVE_PR_NUMBER"];
@@ -245,14 +258,14 @@ function runReviewVerdictBin(env, exec) {
245
258
  const roundAfterId = watermark !== void 0 && /^\d+$/.test(watermark) ? Number(watermark) : void 0;
246
259
  const sha = env["ARCHWEAVE_HEAD_SHA"];
247
260
  const headSha = sha !== void 0 && /^[0-9a-f]{7,40}$/i.test(sha) ? sha : void 0;
261
+ const outputPath = outputModePath(env);
248
262
  const fetched = exec("gh", ["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
249
- if (fetched.status !== 0) {
250
- logs.push("verdict gate: could not fetch PR comments via gh \u2014 skipping (check stays green).");
251
- return { exitCode: 0, logs };
252
- }
253
- const pages = splitConcatenatedJson(fetched.stdout);
263
+ const pages = fetched.status === 0 ? splitConcatenatedJson(fetched.stdout) : null;
254
264
  if (pages === null) {
255
- logs.push("verdict gate: unexpected gh output \u2014 skipping (check stays green).");
265
+ if (outputPath !== void 0) return { exitCode: 1, logs: [UNREAD_COMMENTS_ERROR] };
266
+ logs.push(
267
+ fetched.status !== 0 ? "verdict gate: could not fetch PR comments via gh \u2014 skipping (check stays green)." : "verdict gate: unexpected gh output \u2014 skipping (check stays green)."
268
+ );
256
269
  return { exitCode: 0, logs };
257
270
  }
258
271
  const comments = pages.flatMap((page) => Array.isArray(page) ? page : []).filter(
@@ -264,22 +277,25 @@ function runReviewVerdictBin(env, exec) {
264
277
  return { exitCode: 0, logs };
265
278
  }
266
279
  const previous = [...comments].reverse().find((c) => c.body.includes(VERDICT_MARKER) && c.id !== void 0);
267
- const payload = JSON.stringify({ body: result.commentBody });
268
- const updated = previous !== void 0 && exec("gh", ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${previous.id}`, "--input", "-"], payload).status === 0;
269
- if (!updated) {
270
- const post = exec("gh", ["api", "-X", "POST", `repos/${repo}/issues/${pr}/comments`, "--input", "-"], payload);
271
- if (post.status !== 0) logs.push("verdict gate: could not post the verdict comment (continuing).");
272
- }
273
- if (result.kind === "no-round") {
274
- logs.push("verdict gate: this run posted no review round for the current commit \u2014 earlier rounds are not judged again; check stays green.");
275
- return { exitCode: 0, logs };
280
+ const settled = result.kind === "no-round" || result.kind === "unparseable";
281
+ if (outputPath !== void 0) {
282
+ writeOutput(outputPath, renderStepOutputs(result.commentBody, previous?.id, !settled && result.exitCode !== 0));
283
+ } else {
284
+ const payload = JSON.stringify({ body: result.commentBody });
285
+ const updated = previous !== void 0 && exec("gh", ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${previous.id}`, "--input", "-"], payload).status === 0;
286
+ if (!updated) {
287
+ const post = exec("gh", ["api", "-X", "POST", `repos/${repo}/issues/${pr}/comments`, "--input", "-"], payload);
288
+ if (post.status !== 0) logs.push("verdict gate: could not post the verdict comment (continuing).");
289
+ }
276
290
  }
277
- if (result.kind === "unparseable") {
278
- logs.push("verdict gate: review output was not machine-readable \u2014 human review requested, check stays green.");
291
+ if (settled) {
292
+ logs.push(
293
+ result.kind === "no-round" ? "verdict gate: this run posted no review round for the current commit \u2014 earlier rounds are not judged again; check stays green." : "verdict gate: review output was not machine-readable \u2014 human review requested, check stays green."
294
+ );
279
295
  return { exitCode: 0, logs };
280
296
  }
281
297
  logs.push(`verdict gate: ${result.verdict}`);
282
- return { exitCode: result.exitCode, logs };
298
+ return { exitCode: outputPath === void 0 ? result.exitCode : 0, logs };
283
299
  }
284
300
  function splitConcatenatedJson(stdout) {
285
301
  const text = stdout.trim();
@@ -177,13 +177,14 @@ function renderConcurrencyBlock() {
177
177
  " cancel-in-progress: false"
178
178
  ].join("\n");
179
179
  }
180
- function renderPermissionsBlock() {
181
- return ["permissions:", " contents: read", " pull-requests: write", " id-token: write"].join("\n");
180
+ var REVIEW_GATE_CONDITION = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
181
+ function renderReviewPermissions(idToken) {
182
+ return ["permissions:", " contents: read", " pull-requests: write", ...idToken ? [" id-token: write"] : []].join("\n");
182
183
  }
183
184
  function renderCheckoutStep(gateCondition, extraWith = []) {
184
185
  return [
185
186
  "- name: Checkout repository",
186
- ` if: ${gateCondition}`,
187
+ ...gateCondition === void 0 ? [] : [` if: ${gateCondition}`],
187
188
  ` uses: ${pinnedUses("actions/checkout")}`,
188
189
  " with:",
189
190
  ...extraWith.map((line) => ` ${line}`),
@@ -225,10 +226,46 @@ function renderRoundGateStep(maxRounds) {
225
226
  " fi"
226
227
  ].join("\n");
227
228
  }
229
+ var JUDGE_CONDITION = "steps.ai-review-result.outputs.run == 'true'";
230
+ function renderAiReviewResultStep() {
231
+ return [
232
+ "- name: Check the AI review result",
233
+ " id: ai-review-result",
234
+ " env:",
235
+ " AI_REVIEW_RESULT: ${{ needs.ai-review.result }}",
236
+ " REVIEW_ENABLED: ${{ needs.ai-review.outputs.enabled }}",
237
+ " REVIEW_ALLOWED: ${{ needs.ai-review.outputs.allowed }}",
238
+ " LAST_COMMENT_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
239
+ " run: |",
240
+ ' if [ "$AI_REVIEW_RESULT" != success ]; then',
241
+ ` echo "::error title=AI review did not succeed::The AI review job ended with '$AI_REVIEW_RESULT'."`,
242
+ " exit 1",
243
+ " fi",
244
+ ' case "$REVIEW_ENABLED/$REVIEW_ALLOWED" in',
245
+ " false/* | true/false)",
246
+ ' echo "The AI review did not run (no secret, or the round limit) - nothing to judge."',
247
+ ' echo "run=false" >> "$GITHUB_OUTPUT"',
248
+ " exit 0",
249
+ " ;;",
250
+ " true/true)",
251
+ ' case "$LAST_COMMENT_ID" in',
252
+ " '' | *[!0-9]*) ;;",
253
+ " *)",
254
+ ' echo "run=true" >> "$GITHUB_OUTPUT"',
255
+ " exit 0",
256
+ " ;;",
257
+ " esac",
258
+ " ;;",
259
+ " esac",
260
+ ` echo "::error title=Review verdict not computed::The AI review job's results did not reach this job, so no verdict can be computed. Re-run all jobs."`,
261
+ " exit 1"
262
+ ].join("\n");
263
+ }
228
264
  function renderVerdictGateStep(maxRounds, locale) {
229
265
  return [
230
266
  "- name: Compute review verdict",
231
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
267
+ " id: verdict",
268
+ ` if: ${JUDGE_CONDITION}`,
232
269
  " env:",
233
270
  " GH_TOKEN: ${{ github.token }}",
234
271
  " GITHUB_REPOSITORY: ${{ github.repository }}",
@@ -236,7 +273,8 @@ function renderVerdictGateStep(maxRounds, locale) {
236
273
  ` ARCHWEAVE_MAX_ROUNDS: '${maxRounds}'`,
237
274
  ` ARCHWEAVE_LOCALE: '${locale}'`,
238
275
  " ARCHWEAVE_HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
239
- " ARCHWEAVE_ROUND_AFTER_ID: ${{ steps.round-gate.outputs.last-comment-id }}",
276
+ " ARCHWEAVE_ROUND_AFTER_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
277
+ " ARCHWEAVE_VERDICT_OUTPUT: 'github-output'",
240
278
  " run: |",
241
279
  " if [ -x node_modules/.bin/archweave-review-verdict ]; then",
242
280
  " node_modules/.bin/archweave-review-verdict",
@@ -245,16 +283,46 @@ function renderVerdictGateStep(maxRounds, locale) {
245
283
  " fi"
246
284
  ].join("\n");
247
285
  }
286
+ function renderBlockStep() {
287
+ return [
288
+ "- name: Fail on a blocking verdict",
289
+ " if: steps.verdict.outputs.blocked == 'true'",
290
+ " run: |",
291
+ ' echo "The review verdict is BLOCK - see the verdict comment on this pull request."',
292
+ " exit 1"
293
+ ].join("\n");
294
+ }
295
+ function renderVerdictCommentStep() {
296
+ return [
297
+ "- name: Post review verdict",
298
+ " env:",
299
+ " GH_TOKEN: ${{ github.token }}",
300
+ " REPOSITORY: ${{ github.repository }}",
301
+ " PR_NUMBER: ${{ github.event.pull_request.number }}",
302
+ " VERDICT_BODY: ${{ needs.review.outputs.comment-body }}",
303
+ " PREVIOUS_ID: ${{ needs.review.outputs.previous-comment-id }}",
304
+ " run: |",
305
+ ` payload=$(jq -n --arg body "$VERDICT_BODY" '{body: $body}')`,
306
+ ' case "$PREVIOUS_ID" in',
307
+ " ''|*[!0-9]*) PREVIOUS_ID= ;;",
308
+ " esac",
309
+ ` if [ -n "$PREVIOUS_ID" ] && printf '%s' "$payload" | gh api -X PATCH "repos/$REPOSITORY/issues/comments/$PREVIOUS_ID" --input - > /dev/null; then`,
310
+ " exit 0",
311
+ " fi",
312
+ ` printf '%s' "$payload" | gh api -X POST "repos/$REPOSITORY/issues/$PR_NUMBER/comments" --input - > /dev/null \\`,
313
+ ' || echo "Could not post the verdict comment (continuing)."'
314
+ ].join("\n");
315
+ }
248
316
  var SETUP_NODE_VERSION = "22";
249
317
  var FALLBACK_PNPM_VERSION = "10";
250
318
  var INSTALL_LINES = {
251
- pnpm: [" pnpm install --frozen-lockfile"],
252
- npm: [" npm ci"],
319
+ pnpm: [" pnpm install --frozen-lockfile --ignore-scripts"],
320
+ npm: [" npm ci --ignore-scripts"],
253
321
  yarn: [
254
322
  " corepack enable yarn",
255
- ' if [ "$(yarn --version | cut -d. -f1)" -ge 2 ]; then yarn install --immutable; else yarn install --frozen-lockfile; fi'
323
+ ' if [ "$(yarn --version | cut -d. -f1)" -ge 2 ]; then YARN_ENABLE_SCRIPTS=false yarn install --immutable --mode=skip-build; else yarn install --frozen-lockfile --ignore-scripts; fi'
256
324
  ],
257
- bun: [" bun install --frozen-lockfile"]
325
+ bun: [" bun install --frozen-lockfile --ignore-scripts"]
258
326
  };
259
327
  function renderPnpmVersionStep(gate) {
260
328
  return [
@@ -270,7 +338,7 @@ function renderPnpmVersionStep(gate) {
270
338
  ].join("\n");
271
339
  }
272
340
  function renderWorkspaceSetupSteps(packageManager) {
273
- const gate = " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
341
+ const gate = ` if: ${JUDGE_CONDITION}`;
274
342
  const steps = [];
275
343
  if (packageManager === "pnpm") {
276
344
  steps.push(renderPnpmVersionStep(gate));
@@ -305,6 +373,60 @@ function renderWorkspaceSetupSteps(packageManager) {
305
373
  ].join("\n"));
306
374
  return steps.join("\n\n");
307
375
  }
376
+ function renderReviewWorkflow(input, parts) {
377
+ const aiReviewSteps = [renderSecretGateStep(parts.secretName), renderRoundGateStep(input.maxRounds), ...parts.reviewSteps];
378
+ const reviewSteps = [
379
+ renderAiReviewResultStep(),
380
+ renderCheckoutStep(JUDGE_CONDITION),
381
+ renderWorkspaceSetupSteps(input.packageManager),
382
+ renderVerdictGateStep(input.maxRounds, input.locale),
383
+ renderBlockStep()
384
+ ];
385
+ return [
386
+ `name: ${parts.name}`,
387
+ "",
388
+ renderTriggerBlock(),
389
+ "",
390
+ renderConcurrencyBlock(),
391
+ "",
392
+ "permissions: {}",
393
+ "",
394
+ "jobs:",
395
+ " ai-review:",
396
+ " runs-on: ubuntu-latest",
397
+ indentBlock(renderReviewPermissions(parts.idToken), 4),
398
+ " outputs:",
399
+ " enabled: ${{ steps.secret-gate.outputs.enabled }}",
400
+ " allowed: ${{ steps.round-gate.outputs.allowed }}",
401
+ " last-comment-id: ${{ steps.round-gate.outputs.last-comment-id }}",
402
+ " steps:",
403
+ indentBlock(aiReviewSteps.join("\n\n"), 6),
404
+ "",
405
+ " review:",
406
+ " needs: ai-review",
407
+ " if: always()",
408
+ " runs-on: ubuntu-latest",
409
+ " permissions:",
410
+ " contents: read",
411
+ " pull-requests: read",
412
+ " outputs:",
413
+ " comment-body: ${{ steps.verdict.outputs.comment-body }}",
414
+ " previous-comment-id: ${{ steps.verdict.outputs.previous-comment-id }}",
415
+ " steps:",
416
+ indentBlock(reviewSteps.join("\n\n"), 6),
417
+ "",
418
+ " review-comment:",
419
+ " needs: review",
420
+ // Runs after a BLOCK failed the review job, too: the comment explains the red check.
421
+ " if: ${{ !cancelled() && needs.review.outputs.comment-body != '' }}",
422
+ " runs-on: ubuntu-latest",
423
+ " permissions:",
424
+ " pull-requests: write",
425
+ " steps:",
426
+ indentBlock(renderVerdictCommentStep(), 6),
427
+ ""
428
+ ].join("\n");
429
+ }
308
430
 
309
431
  // ../template-engine/dist/review/claude/claude-provider.js
310
432
  var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
@@ -337,40 +459,26 @@ function composePrompt(input) {
337
459
  return sections.join("\n\n---\n\n");
338
460
  }
339
461
  function buildWorkflowYaml(input) {
340
- const steps = [
341
- renderSecretGateStep(SECRET_NAME),
342
- renderRoundGateStep(input.maxRounds),
343
- renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
344
- [
345
- "- name: Run Claude review",
346
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
347
- ` uses: ${pinnedUses("anthropics/claude-code-action")}`,
348
- " with:",
349
- ` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
350
- " prompt: |",
351
- indentBlock(composePrompt(input), 6),
352
- " claude_args: |",
353
- ` --allowedTools "${ALLOWED_TOOLS}"`
354
- ].join("\n"),
355
- renderWorkspaceSetupSteps(input.packageManager),
356
- renderVerdictGateStep(input.maxRounds, input.locale)
357
- ];
358
- return [
359
- "name: Claude Code Review",
360
- "",
361
- renderTriggerBlock(),
362
- "",
363
- renderConcurrencyBlock(),
364
- "",
365
- renderPermissionsBlock(),
366
- "",
367
- "jobs:",
368
- " review:",
369
- " runs-on: ubuntu-latest",
370
- " steps:",
371
- indentBlock(steps.join("\n\n"), 6),
372
- ""
373
- ].join("\n");
462
+ return renderReviewWorkflow(input, {
463
+ name: "Claude Code Review",
464
+ secretName: SECRET_NAME,
465
+ // The Claude GitHub App signs in with the workflow's OIDC token (claude-code-action's FAQ).
466
+ idToken: true,
467
+ reviewSteps: [
468
+ renderCheckoutStep(REVIEW_GATE_CONDITION),
469
+ [
470
+ "- name: Run Claude review",
471
+ ` if: ${REVIEW_GATE_CONDITION}`,
472
+ ` uses: ${pinnedUses("anthropics/claude-code-action")}`,
473
+ " with:",
474
+ ` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
475
+ " prompt: |",
476
+ indentBlock(composePrompt(input), 6),
477
+ " claude_args: |",
478
+ ` --allowedTools "${ALLOWED_TOOLS}"`
479
+ ].join("\n")
480
+ ]
481
+ });
374
482
  }
375
483
  var claudeProvider = {
376
484
  id: "claude",
@@ -410,49 +518,39 @@ function composePrompt2(input) {
410
518
  return sections.join("\n\n---\n\n");
411
519
  }
412
520
  function buildWorkflowYaml2(input) {
413
- const gateCondition = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
414
- const steps = [
415
- renderSecretGateStep(SECRET_NAME2),
416
- renderRoundGateStep(input.maxRounds),
417
- renderCheckoutStep(gateCondition),
418
- [
419
- "- name: Install cursor-agent CLI",
420
- ` if: ${gateCondition}`,
421
- " run: |",
422
- " curl https://cursor.com/install -fsS | bash",
423
- ' echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"'
424
- ].join("\n"),
425
- [
426
- "- name: Run Cursor review",
427
- ` if: ${gateCondition}`,
428
- " env:",
429
- ` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
430
- " GH_TOKEN: ${{ github.token }}",
431
- " run: |",
432
- ` agent -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
433
- indentBlock(composePrompt2(input), 4),
434
- ` ${PROMPT_HEREDOC_DELIMITER}`,
435
- ' )"'
436
- ].join("\n"),
437
- renderWorkspaceSetupSteps(input.packageManager),
438
- renderVerdictGateStep(input.maxRounds, input.locale)
439
- ];
440
- return [
441
- "name: Cursor Code Review",
442
- "",
443
- renderTriggerBlock(),
444
- "",
445
- renderConcurrencyBlock(),
446
- "",
447
- renderPermissionsBlock(),
448
- "",
449
- "jobs:",
450
- " review:",
451
- " runs-on: ubuntu-latest",
452
- " steps:",
453
- indentBlock(steps.join("\n\n"), 6),
454
- ""
455
- ].join("\n");
521
+ return renderReviewWorkflow(input, {
522
+ name: "Cursor Code Review",
523
+ secretName: SECRET_NAME2,
524
+ // An API key: the Cursor CLI needs no OIDC token.
525
+ idToken: false,
526
+ reviewSteps: [
527
+ renderCheckoutStep(REVIEW_GATE_CONDITION),
528
+ [
529
+ "- name: Install cursor-agent CLI",
530
+ ` if: ${REVIEW_GATE_CONDITION}`,
531
+ " run: |",
532
+ // HTTPS only, TLS 1.2 or newer. Cursor publishes no pinned version or checksum for
533
+ // this installer; docs/pr-review.md states what that leaves open.
534
+ " curl --proto '=https' --tlsv1.2 -fsS https://cursor.com/install | bash",
535
+ // The docs name ~/.cursor/bin; the installer currently links `agent` into ~/.local/bin.
536
+ // Both go on the PATH, so the next step finds it wherever the installer puts it.
537
+ ' echo "$HOME/.local/bin" >> "$GITHUB_PATH"',
538
+ ' echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"'
539
+ ].join("\n"),
540
+ [
541
+ "- name: Run Cursor review",
542
+ ` if: ${REVIEW_GATE_CONDITION}`,
543
+ " env:",
544
+ ` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
545
+ " GH_TOKEN: ${{ github.token }}",
546
+ " run: |",
547
+ ` agent -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
548
+ indentBlock(composePrompt2(input), 4),
549
+ ` ${PROMPT_HEREDOC_DELIMITER}`,
550
+ ' )"'
551
+ ].join("\n")
552
+ ]
553
+ });
456
554
  }
457
555
  var cursorProvider = {
458
556
  id: "cursor",
@@ -491,40 +589,26 @@ function composePrompt3(input) {
491
589
  return sections.join("\n\n---\n\n");
492
590
  }
493
591
  function buildWorkflowYaml3(input) {
494
- const steps = [
495
- renderSecretGateStep(SECRET_NAME3),
496
- renderRoundGateStep(input.maxRounds),
497
- renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
498
- [
499
- "- name: Run Gemini review",
500
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
501
- ` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
502
- " env:",
503
- " GITHUB_TOKEN: ${{ github.token }}",
504
- " with:",
505
- ` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
506
- " prompt: |",
507
- indentBlock(composePrompt3(input), 6)
508
- ].join("\n"),
509
- renderWorkspaceSetupSteps(input.packageManager),
510
- renderVerdictGateStep(input.maxRounds, input.locale)
511
- ];
512
- return [
513
- "name: Gemini Code Review",
514
- "",
515
- renderTriggerBlock(),
516
- "",
517
- renderConcurrencyBlock(),
518
- "",
519
- renderPermissionsBlock(),
520
- "",
521
- "jobs:",
522
- " review:",
523
- " runs-on: ubuntu-latest",
524
- " steps:",
525
- indentBlock(steps.join("\n\n"), 6),
526
- ""
527
- ].join("\n");
592
+ return renderReviewWorkflow(input, {
593
+ name: "Gemini Code Review",
594
+ secretName: SECRET_NAME3,
595
+ // An API key: run-gemini-cli uses OIDC only for Google Cloud workload identity federation.
596
+ idToken: false,
597
+ reviewSteps: [
598
+ renderCheckoutStep(REVIEW_GATE_CONDITION),
599
+ [
600
+ "- name: Run Gemini review",
601
+ ` if: ${REVIEW_GATE_CONDITION}`,
602
+ ` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
603
+ " env:",
604
+ " GITHUB_TOKEN: ${{ github.token }}",
605
+ " with:",
606
+ ` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
607
+ " prompt: |",
608
+ indentBlock(composePrompt3(input), 6)
609
+ ].join("\n")
610
+ ]
611
+ });
528
612
  }
529
613
  var geminiProvider = {
530
614
  id: "gemini",
@@ -563,64 +647,49 @@ function composePrompt4(input) {
563
647
  return sections.join("\n\n---\n\n");
564
648
  }
565
649
  function buildWorkflowYaml4(input) {
566
- const gateCondition = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
567
- const steps = [
568
- renderSecretGateStep(SECRET_NAME4),
569
- renderRoundGateStep(input.maxRounds),
570
- renderCheckoutStep(gateCondition, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
571
- [
572
- "- name: Fetch base and head refs for the PR",
573
- ` if: ${gateCondition}`,
574
- " env:",
575
- " PR_BASE_REF: ${{ github.event.pull_request.base.ref }}",
576
- " PR_NUMBER: ${{ github.event.pull_request.number }}",
577
- " run: |",
578
- " # Explicit destinations: the sandboxed review step has no network, so both sides",
579
- " # of the diff must survive as stable local refs (FETCH_HEAD alone is ambiguous).",
580
- " git fetch --no-tags origin \\",
581
- ' "+refs/heads/$PR_BASE_REF:refs/remotes/origin/pr-base" \\',
582
- ' "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"'
583
- ].join("\n"),
584
- [
585
- "- name: Run Codex review",
586
- " id: codex-review",
587
- ` if: ${gateCondition}`,
588
- ` uses: ${pinnedUses("openai/codex-action")}`,
589
- " with:",
590
- ` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
591
- ' permission-profile: ":read-only"',
592
- " prompt: |",
593
- indentBlock(composePrompt4(input), 6)
594
- ].join("\n"),
595
- [
596
- "- name: Post review comment",
597
- ` if: ${gateCondition}`,
598
- " env:",
599
- " GH_TOKEN: ${{ github.token }}",
600
- " PR_NUMBER: ${{ github.event.pull_request.number }}",
601
- " REVIEW_BODY: ${{ steps.codex-review.outputs.final-message }}",
602
- " run: |",
603
- ' printf %s "$REVIEW_BODY" | gh pr comment "$PR_NUMBER" --body-file -'
604
- ].join("\n"),
605
- renderWorkspaceSetupSteps(input.packageManager),
606
- renderVerdictGateStep(input.maxRounds, input.locale)
607
- ];
608
- return [
609
- "name: GPT Code Review",
610
- "",
611
- renderTriggerBlock(),
612
- "",
613
- renderConcurrencyBlock(),
614
- "",
615
- renderPermissionsBlock(),
616
- "",
617
- "jobs:",
618
- " review:",
619
- " runs-on: ubuntu-latest",
620
- " steps:",
621
- indentBlock(steps.join("\n\n"), 6),
622
- ""
623
- ].join("\n");
650
+ return renderReviewWorkflow(input, {
651
+ name: "GPT Code Review",
652
+ secretName: SECRET_NAME4,
653
+ // An API key: codex-action needs no OIDC token.
654
+ idToken: false,
655
+ reviewSteps: [
656
+ renderCheckoutStep(REVIEW_GATE_CONDITION, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
657
+ [
658
+ "- name: Fetch base and head refs for the PR",
659
+ ` if: ${REVIEW_GATE_CONDITION}`,
660
+ " env:",
661
+ " PR_BASE_REF: ${{ github.event.pull_request.base.ref }}",
662
+ " PR_NUMBER: ${{ github.event.pull_request.number }}",
663
+ " run: |",
664
+ " # Explicit destinations: the sandboxed review step has no network, so both sides",
665
+ " # of the diff must survive as stable local refs (FETCH_HEAD alone is ambiguous).",
666
+ " git fetch --no-tags origin \\",
667
+ ' "+refs/heads/$PR_BASE_REF:refs/remotes/origin/pr-base" \\',
668
+ ' "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"'
669
+ ].join("\n"),
670
+ [
671
+ "- name: Run Codex review",
672
+ " id: codex-review",
673
+ ` if: ${REVIEW_GATE_CONDITION}`,
674
+ ` uses: ${pinnedUses("openai/codex-action")}`,
675
+ " with:",
676
+ ` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
677
+ ' permission-profile: ":read-only"',
678
+ " prompt: |",
679
+ indentBlock(composePrompt4(input), 6)
680
+ ].join("\n"),
681
+ [
682
+ "- name: Post review comment",
683
+ ` if: ${REVIEW_GATE_CONDITION}`,
684
+ " env:",
685
+ " GH_TOKEN: ${{ github.token }}",
686
+ " PR_NUMBER: ${{ github.event.pull_request.number }}",
687
+ " REVIEW_BODY: ${{ steps.codex-review.outputs.final-message }}",
688
+ " run: |",
689
+ ' printf %s "$REVIEW_BODY" | gh pr comment "$PR_NUMBER" --body-file -'
690
+ ].join("\n")
691
+ ]
692
+ });
624
693
  }
625
694
  var gptProvider = {
626
695
  id: "gpt",