Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions .github/workflows/self-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: 6529bot Self Review
run-name: "self-review ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }} (${{ github.event_name }})"

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to review.
required: true
type: string
review_kinds_json:
description: JSON array of review kinds. Empty uses the initial set.
required: false
default: ""
type: string

permissions:
contents: read

jobs:
plan:
name: Plan review kinds
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
(startsWith(github.event.comment.body, '/6529bot') || startsWith(github.event.comment.body, '@6529bot')))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
outputs:
jobs_json: ${{ steps.plan.outputs.jobs_json }}
steps:
- name: Checkout bot
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22

- name: Plan review kinds
id: plan
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action || 'manual' }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref || '' }}
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || '' }}
COMMENT_BODY: ${{ github.event.comment.body || '' }}
REQUESTED_KINDS_JSON: ${{ inputs.review_kinds_json || '' }}
INITIAL_KINDS_JSON: '["general","security","auth-api","deploy-actions","db-lambda","glm-swarm"]'
SYNCHRONIZE_KINDS_JSON: '["followup"]'
SELF_INSTALLATION_ID: ${{ vars.REVIEWBOT_SELF_INSTALLATION_ID }}
run: node bin/self-review-plan.cjs

review:
name: "${{ matrix.job.reviewKind }} @ ${{ matrix.job.headSha }}"
needs: plan
if: ${{ needs.plan.outputs.jobs_json != '' && needs.plan.outputs.jobs_json != '[]' }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
timeout-minutes: ${{ fromJSON(vars.REVIEWBOT_WORKER_TIMEOUT_MINUTES || '20') }}
strategy:
fail-fast: false
matrix:
job: ${{ fromJSON(needs.plan.outputs.jobs_json) }}
concurrency:
group: reviewbot-worker-${{ matrix.job.runKey }}
cancel-in-progress: false
steps:
- name: Checkout bot
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: npm

- name: Install bot dependencies
run: npm ci

- name: Configure AWS credentials for reviewbot ledgers
if: ${{ vars.REVIEW_USAGE_AWS_ROLE_ARN != '' }}
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
aws-region: ${{ vars.REVIEW_USAGE_AWS_REGION || 'us-east-1' }}
role-to-assume: ${{ vars.REVIEW_USAGE_AWS_ROLE_ARN }}

- name: Mint target installation token
id: installation-token
env:
REVIEWBOT_GITHUB_APP_ID: ${{ secrets.REVIEWBOT_GITHUB_APP_ID }}
REVIEWBOT_GITHUB_APP_PRIVATE_KEY_BASE64: ${{ secrets.REVIEWBOT_GITHUB_APP_PRIVATE_KEY_BASE64 }}
run: |
node bin/github-app-installation-token.cjs \
--installation-id "${{ matrix.job.installationId }}" \
--github-actions-output

- name: Checkout target repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ matrix.job.headRepoFullName }}
ref: ${{ matrix.job.headSha }}
path: target
token: ${{ steps.installation-token.outputs.token }}
persist-credentials: false

- name: Write job payload
env:
JOB_JSON: ${{ toJSON(matrix.job) }}
run: printf '%s\n' "$JOB_JSON" > job.json

- name: Run 6529bot review job
env:
GH_TOKEN: ${{ steps.installation-token.outputs.token }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
REVIEW_WORKSPACE: ${{ github.workspace }}/target
PR_BASE_SHA: ${{ matrix.job.baseSha }}
PR_HEAD_REF: ${{ matrix.job.headRefName }}
PR_HEAD_REPO: ${{ matrix.job.headRepoFullName }}
REVIEW_USAGE_ENABLED: ${{ vars.REVIEW_USAGE_AWS_ROLE_ARN != '' }}
REVIEW_USAGE_AWS_REGION: ${{ vars.REVIEW_USAGE_AWS_REGION }}
REVIEW_USAGE_DB_RESOURCE_ARN: ${{ vars.REVIEW_USAGE_DB_RESOURCE_ARN }}
REVIEW_USAGE_DB_SECRET_ARN: ${{ vars.REVIEW_USAGE_DB_SECRET_ARN }}
REVIEW_USAGE_DB_NAME: ${{ vars.REVIEW_USAGE_DB_NAME }}
REVIEW_USAGE_DB_SCHEMA: ${{ vars.REVIEW_USAGE_DB_SCHEMA }}
REVIEWBOT_RUN_CONTROL_LEDGER_ENABLED: ${{ vars.REVIEWBOT_RUN_CONTROL_LEDGER_ENABLED || 'false' }}
REVIEWBOT_RUN_CONTROL_LEDGER_AWS_REGION: ${{ vars.REVIEWBOT_RUN_CONTROL_LEDGER_AWS_REGION || vars.REVIEW_USAGE_AWS_REGION }}
REVIEWBOT_RUN_CONTROL_LEDGER_DB_RESOURCE_ARN: ${{ vars.REVIEWBOT_RUN_CONTROL_LEDGER_DB_RESOURCE_ARN || vars.REVIEW_USAGE_DB_RESOURCE_ARN }}
REVIEWBOT_RUN_CONTROL_LEDGER_DB_SECRET_ARN: ${{ vars.REVIEWBOT_RUN_CONTROL_LEDGER_DB_SECRET_ARN || vars.REVIEW_USAGE_DB_SECRET_ARN }}
REVIEWBOT_RUN_CONTROL_LEDGER_DB_NAME: ${{ vars.REVIEWBOT_RUN_CONTROL_LEDGER_DB_NAME || vars.REVIEW_USAGE_DB_NAME }}
REVIEWBOT_RUN_CONTROL_LEDGER_DB_SCHEMA: ${{ vars.REVIEWBOT_RUN_CONTROL_LEDGER_DB_SCHEMA || vars.REVIEW_USAGE_DB_SCHEMA }}
REVIEWBOT_WORKER_INCLUDE_OUTPUT: ${{ vars.REVIEWBOT_WORKER_INCLUDE_OUTPUT || 'false' }}
REVIEW_DRAFT_PR_MODE: allow
REVIEW_MAX_CHANGED_FILES: ${{ vars.REVIEW_MAX_CHANGED_FILES || '300' }}
REVIEW_MAX_CHANGED_LINES: ${{ vars.REVIEW_MAX_CHANGED_LINES || '30000' }}
REVIEW_LARGE_PR_CHANGED_LINES: ${{ vars.REVIEW_LARGE_PR_CHANGED_LINES || '3500' }}
REVIEW_MAX_DIFF_CHARS: ${{ vars.REVIEW_MAX_DIFF_CHARS || '250000' }}
REVIEW_MAX_CONTEXT_CHARS: ${{ vars.REVIEW_MAX_CONTEXT_CHARS || '100000' }}
REVIEW_MAX_INPUT_CHARS: ${{ vars.REVIEW_MAX_INPUT_CHARS || '350000' }}
REVIEW_MAX_OUTPUT_TOKENS: ${{ vars.REVIEW_MAX_OUTPUT_TOKENS || '4000' }}
REVIEW_OVERSIZE_BEHAVIOR: ${{ vars.REVIEW_OVERSIZE_BEHAVIOR || 'skip' }}
REVIEW_GLM_SWARM_ENABLED: ${{ vars.REVIEW_GLM_SWARM_ENABLED || 'true' }}
REVIEW_GLM_SWARM_MAX_REVIEWERS: ${{ vars.REVIEW_GLM_SWARM_MAX_REVIEWERS || '4' }}
REVIEW_GLM_SWARM_MAX_FILES_PER_REVIEWER: ${{ vars.REVIEW_GLM_SWARM_MAX_FILES_PER_REVIEWER || '12' }}
REVIEW_GLM_SWARM_REVIEWER_MAX_OUTPUT_TOKENS: ${{ vars.REVIEW_GLM_SWARM_REVIEWER_MAX_OUTPUT_TOKENS || '1200' }}
REVIEW_GLM_SWARM_SYNTHESIS_MAX_OUTPUT_TOKENS: ${{ vars.REVIEW_GLM_SWARM_SYNTHESIS_MAX_OUTPUT_TOKENS || '1800' }}
REVIEW_GLM_SWARM_MAX_TOTAL_OUTPUT_TOKENS: ${{ vars.REVIEW_GLM_SWARM_MAX_TOTAL_OUTPUT_TOKENS || '7000' }}
REVIEW_GLM_SWARM_MAX_COST_USD: ${{ vars.REVIEW_GLM_SWARM_MAX_COST_USD || '2' }}
REVIEW_GLM_SWARM_RAW_OUTPUTS_MODE: ${{ vars.REVIEW_GLM_SWARM_RAW_OUTPUTS_MODE || 'off' }}
REVIEW_GLM_SWARM_RAW_OUTPUTS_FAIL_CLOSED: ${{ vars.REVIEW_GLM_SWARM_RAW_OUTPUTS_FAIL_CLOSED || 'false' }}
REVIEW_GLM_SWARM_RAW_OUTPUTS_AWS_REGION: ${{ vars.REVIEW_GLM_SWARM_RAW_OUTPUTS_AWS_REGION || vars.REVIEW_USAGE_AWS_REGION }}
REVIEW_GLM_SWARM_RAW_OUTPUTS_S3_BUCKET: ${{ vars.REVIEW_GLM_SWARM_RAW_OUTPUTS_S3_BUCKET }}
REVIEW_GLM_SWARM_RAW_OUTPUTS_S3_PREFIX: ${{ vars.REVIEW_GLM_SWARM_RAW_OUTPUTS_S3_PREFIX || 'glm-swarm' }}
run: npm run worker:job -- -- --job-file job.json
198 changes: 198 additions & 0 deletions bin/self-review-plan.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env node

"use strict";

const fs = require("fs");
const { execFileSync } = require("child_process");
const { REVIEW_KINDS, parseReviewCommand } = require("../src/github-webhook.cjs");
const { createReviewJobs } = require("../src/review-job.cjs");

const SELF_REVIEW_LANES = [{ provider: "anthropic", model: "claude-opus-4-8" }];
const SELF_REVIEW_MAX_JOBS = 12;

function planSelfReview(env = process.env, options = {}) {
const eventName = String(env.EVENT_NAME || "").trim();
const eventAction = String(env.EVENT_ACTION || "manual").trim() || "manual";
const prNumber = String(env.PR_NUMBER || "").trim();
if (!prNumber) {
throw new Error("PR_NUMBER is required.");
}
let reviewKinds;
if (eventName === "workflow_dispatch") {
reviewKinds = String(env.REQUESTED_KINDS_JSON || "").trim()
? parseKindsJson(env.REQUESTED_KINDS_JSON, "review_kinds_json")
: parseKindsJson(env.INITIAL_KINDS_JSON, "INITIAL_KINDS_JSON");
} else if (eventName === "pull_request") {
reviewKinds =
eventAction === "synchronize"
? parseKindsJson(env.SYNCHRONIZE_KINDS_JSON, "SYNCHRONIZE_KINDS_JSON")
: parseKindsJson(env.INITIAL_KINDS_JSON, "INITIAL_KINDS_JSON");
} else if (eventName === "issue_comment") {
const command = parseReviewCommand(String(env.COMMENT_BODY || ""));
reviewKinds = command?.reviewKinds?.length ? command.reviewKinds : [];
} else {
throw new Error(`Unsupported event '${eventName}'.`);
}
const trigger = eventName === "issue_comment" ? "comment" : eventAction;
if (reviewKinds.length === 0) {
return {
prNumber,
eventAction,
trigger,
reviewKinds: [],
jobs: [],
headSha: "",
};
}
const repository = String(env.GITHUB_REPOSITORY || "").trim();
if (!repository) {
throw new Error("GITHUB_REPOSITORY is required.");
}
const installationId = String(env.SELF_INSTALLATION_ID || "").trim();
if (!installationId) {
throw new Error(
"SELF_INSTALLATION_ID is required. Set the REVIEWBOT_SELF_INSTALLATION_ID repository variable."
);
}
let headSha = String(env.PR_HEAD_SHA || "").trim();
let baseSha = String(env.PR_BASE_SHA || "").trim();
let headRefName = String(env.PR_HEAD_REF || "").trim();
let headRepoFullName = String(env.PR_HEAD_REPO || "").trim();
if (!headSha || !baseSha || !headRefName || !headRepoFullName) {
const resolvePullRequest = options.resolvePullRequest || resolvePullRequestWithGh;
const context = resolvePullRequest(prNumber, env);
headSha = headSha || context.headSha;
baseSha = baseSha || context.baseSha;
headRefName = headRefName || context.headRefName;
headRepoFullName = headRepoFullName || String(context.headRepoFullName || "").trim();
}
if (!headSha) {
throw new Error(`Could not resolve head SHA for PR #${prNumber}.`);
}
if (!baseSha) {
throw new Error(`Could not resolve base SHA for PR #${prNumber}.`);
}
if (!headRefName) {
throw new Error(`Could not resolve head ref for PR #${prNumber}.`);
}
if (headRepoFullName && headRepoFullName !== repository) {
throw new Error(
`Self review only supports branches in ${repository}; PR #${prNumber} head is ${headRepoFullName}.`
);
}
const deliveryId = `self-review-${String(env.GITHUB_RUN_ID || "").trim() || "manual"}`;
const event = {
kind: "self_review",
trigger,
shouldEnqueue: true,
deliveryId,
repository: { fullName: repository },
headRepoFullName: repository,
prNumber,
headSha,
baseSha,
headRefName,
installationId,
reviewKinds,
actor: String(env.GITHUB_ACTOR || "").trim(),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const jobs = createReviewJobs(
event,
{},
{ maxJobsPerDelivery: SELF_REVIEW_MAX_JOBS, lanes: SELF_REVIEW_LANES }
);
return {
prNumber,
eventAction,
trigger,
reviewKinds,
jobs,
headSha,
};
}

function parseKindsJson(value, label) {
let parsed;
try {
parsed = JSON.parse(String(value || ""));
} catch (error) {
throw new Error(`${label} must be a JSON array of review kinds: ${error.message}`);
}
if (!Array.isArray(parsed) || parsed.length === 0) {
throw new Error(`${label} must be a non-empty JSON array of review kinds.`);
}
for (const kind of parsed) {
if (!REVIEW_KINDS.includes(kind)) {
throw new Error(`${label} includes unknown review kind '${kind}'.`);
}
}
return parsed;
}

function resolvePullRequestWithGh(prNumber, env = process.env) {
const repository = String(env.GITHUB_REPOSITORY || "").trim();
if (!repository) {
throw new Error("GITHUB_REPOSITORY is required to resolve the PR context.");
}
const output = execFileSync(
"gh",
[
"api",
`repos/${repository}/pulls/${prNumber}`,
"--jq",
"{headSha: .head.sha, baseSha: .base.sha, headRefName: .head.ref, headRepoFullName: .head.repo.full_name}",
],
{ encoding: "utf8" }
);
let context;
try {
context = JSON.parse(String(output || ""));
} catch (error) {
throw new Error(`Could not parse PR #${prNumber} context: ${error.message}`);
}
const headSha = String(context.headSha || "").trim();
if (!headSha) {
throw new Error(`Could not resolve head SHA for PR #${prNumber}.`);
}
return {
headSha,
baseSha: String(context.baseSha || "").trim(),
headRefName: String(context.headRefName || "").trim(),
headRepoFullName: String(context.headRepoFullName || "").trim(),
};
}

function main() {
const plan = planSelfReview(process.env);
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required.");
}
const lines = [
`pr_number=${plan.prNumber}`,
`event_action=${plan.eventAction}`,
`review_kinds_json=${JSON.stringify(plan.reviewKinds)}`,
`jobs_json=${JSON.stringify(plan.jobs)}`,
];
fs.appendFileSync(outputPath, `${lines.join("\n")}\n`);
if (plan.jobs.length === 0) {
console.log(`No review kinds requested for PR #${plan.prNumber}; skipping review.`);
return;
}
console.log(
`Planned ${plan.jobs.length} self review job(s) for PR #${plan.prNumber} @ ${plan.headSha}: ${plan.reviewKinds.join(", ")}`
);
}

if (require.main === module) {
try {
main();
} catch (error) {
console.error(error.message || error);
process.exit(1);
}
}

module.exports = {
planSelfReview,
};
Loading