Skip to content
Open
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
305 changes: 305 additions & 0 deletions .github/workflows/jules-ocr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
name: Jules OCR Review

on:
pull_request:
types: [opened, ready_for_review, closed]

permissions:
contents: read
issues: write
pull-requests: write

concurrency:
group: jules-ocr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
relay-result:
if: >-
github.event.action == 'opened' &&
startsWith(github.event.pull_request.head.ref, 'chore/jules-ocr-review-')
runs-on: ubuntu-latest

steps:
- name: Relay Jules review and close empty result PR
uses: actions/github-script@v8
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const reviewPrNumber = context.payload.pull_request.number;

const { data: reviewPr } = await github.rest.pulls.get({
owner,
repo,
pull_number: reviewPrNumber,
});

// Safety guard: a review-only Jules task should never change files.
// If it does, leave the PR open for human inspection.
if (reviewPr.changed_files !== 0) {
core.setFailed(
`Jules result PR #${reviewPrNumber} changed ${reviewPr.changed_files} file(s); refusing to auto-close it.`,
);
return;
}

const body = reviewPr.body || "";
const targetMatch =
body.match(/code review of PR #(\d+)/i) ||
reviewPr.title.match(/Review PR #(\d+)/i);

if (!targetMatch) {
core.setFailed("Could not identify the original PR from the Jules result.");
return;
}

const originalPrNumber = Number(targetMatch[1]);
const issueMatch = body.match(/Fixes #(\d+)/i);
const dispatchIssueNumber = issueMatch ? Number(issueMatch[1]) : null;
const report = body
.replace(/\n\nFixes #\d+[\s\S]*$/i, "")
.trim();

const marker = `<!-- jules-ocr-result:${reviewPrNumber} -->`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: originalPrNumber,
body: `${marker}
### Jules + OpenCodeReview

${report}

Review artifact: #${reviewPrNumber}`,
});

await github.rest.pulls.update({
owner,
repo,
pull_number: reviewPrNumber,
state: "closed",
});

if (dispatchIssueNumber) {
await github.rest.issues.update({
owner,
repo,
issue_number: dispatchIssueNumber,
state: "closed",
state_reason: "completed",
});
}

core.notice(
`Relayed Jules review from #${reviewPrNumber} to PR #${originalPrNumber} and closed the empty review artifact.`,
);

dispatch:
if: >-
!startsWith(github.event.pull_request.head.ref, 'chore/jules-ocr-review-')
runs-on: ubuntu-latest

steps:
- name: Prepare Jules OpenCodeReview dispatch
id: prepare
uses: actions/github-script@v8
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr = context.payload.pull_request;
const marker = `<!-- jules-ocr-review:pr=${pr.number} -->`;
const reviewLabel = "jules-review";
const triggerLabel = "jules";

async function ensureLabel(name, color, description) {
try {
await github.rest.issues.getLabel({ owner, repo, name });
} catch (error) {
if (error.status !== 404) throw error;
await github.rest.issues.createLabel({
owner,
repo,
name,
color,
description,
});
}
}

async function findDispatchIssue() {
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "all",
labels: reviewLabel,
per_page: 100,
});

return issues.find(
(issue) => !issue.pull_request && issue.body?.includes(marker),
);
}

await ensureLabel(
reviewLabel,
"5319e7",
"Automated Jules + OpenCodeReview review dispatch",
);
await ensureLabel(
triggerLabel,
"4285f4",
"Triggers a Jules task through the Jules GitHub App",
);

const existing = await findDispatchIssue();

if (context.payload.action === "closed") {
if (existing && existing.state !== "closed") {
await github.rest.issues.update({
owner,
repo,
issue_number: existing.number,
state: "closed",
state_reason: "completed",
});
}
core.setOutput("should-trigger", "false");
core.info("PR closed; dispatch issue cleaned up.");
return;
}

if (pr.draft) {
core.setOutput("should-trigger", "false");
core.info("Draft PR; Jules review is skipped until ready_for_review.");
return;
}

if (pr.head.repo.full_name !== `${owner}/${repo}`) {
core.setOutput("should-trigger", "false");
core.warning(
"Fork PR detected; automatic Jules dispatch is disabled to protect the account task quota.",
);
return;
}

// Hard rule: one Jules task per source PR.
if (existing) {
core.setOutput("should-trigger", "false");
core.info(
`Jules was already dispatched once for PR #${pr.number} via issue #${existing.number}; skipping.`,
);
return;
}

const shortSha = pr.head.sha.slice(0, 7);
const title = `[Jules OCR] Review PR #${pr.number} @ ${shortSha}`;
const body = `${marker}
## Review target

- Pull request: #${pr.number}
- Base: \`${pr.base.ref}\`
- Head: \`${pr.head.ref}\`
- Expected head SHA: \`${pr.head.sha}\`

## Task

Perform a **review-only** code review of PR #${pr.number} using **OpenCodeReview Delegation Mode** with Jules as the host model.

Do not configure or use an external LLM API. The purpose of delegation mode is to reuse the Jules model/task allowance attached to this GitHub App session.

Do **not** modify repository files. Keep the result PR empty. Jules issue tasks publish a PR even for review-only work; put the complete review report in your final summary / result PR body so automation can relay it back to PR #${pr.number} and close the empty artifact.

### Required procedure

1. Fetch the target refs and verify the reviewed head resolves to \`${pr.head.sha}\`.
2. Install the latest OCR CLI if needed:
\`npm install -g @alibaba-group/open-code-review@latest\`
3. Run the delegation scope preview against the PR:
\`ocr delegate preview --from origin/${pr.base.ref} --to origin/${pr.head.ref} --format json\`
If this installed OCR version does not support \`--format json\`, rerun without that flag.
4. Use \`ocr delegate rule <paths...>\` for the reviewable files returned by the preview.
5. Inspect the actual git diff plus any full files/tests needed for context, then perform the review yourself as the host agent.
6. Report only actionable **critical, high, or medium** confidence findings. For each finding include severity, file/line, why it is a problem, and the smallest safe fix.
7. If there are no critical/high/medium findings, state that explicitly.

Keep this task review-only and ensure the complete report is present in the final result PR body.`;

const created = await github.rest.issues.create({
owner,
repo,
title,
body,
labels: [reviewLabel],
});
const issue = created.data;

// The normal GITHUB_TOKEN prepares the issue only. Jules did not
// react to a github-actions[bot] label event in the live test.
// The next step adds "jules" with a user-scoped GitHub token.
core.setOutput("issue-number", String(issue.number));
core.setOutput("should-trigger", "true");

const prCommentMarker = "<!-- jules-ocr-dispatch -->";
const commentBody = `${prCommentMarker}
Jules + OpenCodeReview review prepared for \`${shortSha}\`: #${issue.number}`;

const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: pr.number,
per_page: 100,
});

const previous = comments.find(
(comment) =>
comment.user?.login === "github-actions[bot]" &&
comment.body?.includes(prCommentMarker),
);

if (previous) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: previous.id,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: commentBody,
});
}

- name: Verify user trigger token
if: steps.prepare.outputs.should-trigger == 'true'
shell: bash
env:
JULES_GITHUB_TOKEN: ${{ secrets.JULES_GITHUB_TOKEN }}
run: |
if [ -z "$JULES_GITHUB_TOKEN" ]; then
echo "::error::Missing JULES_GITHUB_TOKEN. Add a fine-grained GitHub PAT for your account, scoped to this repository with Issues: Read and write. It is used only to add the 'jules' label; no Jules API key or model API is used."
exit 1
fi

- name: Trigger Jules with user identity
if: steps.prepare.outputs.should-trigger == 'true'
uses: actions/github-script@v8
env:
ISSUE_NUMBER: ${{ steps.prepare.outputs.issue-number }}
with:
github-token: ${{ secrets.JULES_GITHUB_TOKEN }}
script: |
const issueNumber = Number(process.env.ISSUE_NUMBER);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ["jules"],
});
core.notice(
`Jules review triggered through issue #${issueNumber} using the user's GitHub identity.`,
);
Loading