From 41e9c29a01a7b52c89b883f9b27307fb33efc378 Mon Sep 17 00:00:00 2001 From: Leone Marcos Date: Sun, 13 Sep 2026 16:35:27 -0300 Subject: [PATCH 1/4] ci: dispatch Jules OpenCodeReview reviews --- .github/workflows/jules-ocr-review.yml | 209 +++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 .github/workflows/jules-ocr-review.yml diff --git a/.github/workflows/jules-ocr-review.yml b/.github/workflows/jules-ocr-review.yml new file mode 100644 index 0000000..c4f3464 --- /dev/null +++ b/.github/workflows/jules-ocr-review.yml @@ -0,0 +1,209 @@ +name: Jules OCR Review + +on: + pull_request: + types: [opened, reopened, synchronize, 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: + dispatch: + runs-on: ubuntu-latest + + steps: + - name: Dispatch Jules OpenCodeReview task + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + const marker = ``; + 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.info("PR closed; dispatch issue cleaned up."); + return; + } + + if (pr.draft) { + core.info("Draft PR; Jules review is skipped until ready_for_review."); + return; + } + + if (pr.head.repo.full_name !== `${owner}/${repo}`) { + core.warning( + "Fork PR detected; automatic Jules dispatch is disabled to protect the account task quota.", + ); + 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, create commits, publish a branch, or open another pull request. + + ### 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 \` 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. + + Post the final review result back to this issue. Keep this task review-only.`; + + let issue; + if (existing) { + await github.rest.issues.update({ + owner, + repo, + issue_number: existing.number, + title, + body, + state: "open", + labels: [reviewLabel], + }); + issue = existing; + } else { + const created = await github.rest.issues.create({ + owner, + repo, + title, + body, + labels: [reviewLabel], + }); + issue = created.data; + } + + // Add the trigger label last. Re-adding it on every PR revision + // creates a fresh Jules task while reusing one dispatch issue per PR. + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: issue.number, + name: triggerLabel, + }); + } catch (error) { + if (error.status !== 404) throw error; + } + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: issue.number, + labels: [triggerLabel], + }); + + const prCommentMarker = ""; + const commentBody = `${prCommentMarker} + Jules + OpenCodeReview review dispatched 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, + }); + } + + core.notice(`Jules review dispatched through issue #${issue.number}.`); From 538a00268a27a73ac1ea173fb0bcfc560d00ce06 Mon Sep 17 00:00:00 2001 From: Leone Marcos Date: Sun, 13 Sep 2026 16:42:30 -0300 Subject: [PATCH 2/4] ci: trigger Jules reviews with user identity --- .github/workflows/jules-ocr-review.yml | 64 ++++++++++++++++---------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/.github/workflows/jules-ocr-review.yml b/.github/workflows/jules-ocr-review.yml index c4f3464..b1c2da4 100644 --- a/.github/workflows/jules-ocr-review.yml +++ b/.github/workflows/jules-ocr-review.yml @@ -18,8 +18,9 @@ jobs: runs-on: ubuntu-latest steps: - - name: Dispatch Jules OpenCodeReview task - uses: actions/github-script@v7 + - name: Prepare Jules OpenCodeReview dispatch + id: prepare + uses: actions/github-script@v8 with: script: | const owner = context.repo.owner; @@ -81,16 +82,19 @@ jobs: 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.", ); @@ -132,7 +136,7 @@ jobs: let issue; if (existing) { - await github.rest.issues.update({ + const updated = await github.rest.issues.update({ owner, repo, issue_number: existing.number, @@ -141,7 +145,7 @@ jobs: state: "open", labels: [reviewLabel], }); - issue = existing; + issue = updated.data; } else { const created = await github.rest.issues.create({ owner, @@ -153,29 +157,15 @@ jobs: issue = created.data; } - // Add the trigger label last. Re-adding it on every PR revision - // creates a fresh Jules task while reusing one dispatch issue per PR. - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: issue.number, - name: triggerLabel, - }); - } catch (error) { - if (error.status !== 404) throw error; - } - - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: issue.number, - labels: [triggerLabel], - }); + // The normal GITHUB_TOKEN intentionally prepares the issue only. + // The next step adds "jules" with a user token so the Jules GitHub + // App sees a user-authored label event and consumes the user's plan. + core.setOutput("issue-number", String(issue.number)); + core.setOutput("should-trigger", "true"); const prCommentMarker = ""; const commentBody = `${prCommentMarker} - Jules + OpenCodeReview review dispatched for \`${shortSha}\`: #${issue.number}`; + Jules + OpenCodeReview review prepared for \`${shortSha}\`: #${issue.number}`; const { data: comments } = await github.rest.issues.listComments({ owner, @@ -206,4 +196,28 @@ jobs: }); } - core.notice(`Jules review dispatched through issue #${issue.number}.`); + - 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. This is only used to add the 'jules' label; no Jules API key is used." + exit 1 + fi + + - name: Trigger Jules with user identity + if: steps.prepare.outputs.should-trigger == 'true' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.JULES_GITHUB_TOKEN }} + script: | + const issueNumber = Number("${{ steps.prepare.outputs.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.`); From 880bc24936346f44135585c3775d1966453b0036 Mon Sep 17 00:00:00 2001 From: Leone Marcos Date: Sun, 13 Sep 2026 16:45:03 -0300 Subject: [PATCH 3/4] ci: relay Jules OCR results back to source PR --- .github/workflows/jules-ocr-review.yml | 100 +++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/.github/workflows/jules-ocr-review.yml b/.github/workflows/jules-ocr-review.yml index b1c2da4..997a31f 100644 --- a/.github/workflows/jules-ocr-review.yml +++ b/.github/workflows/jules-ocr-review.yml @@ -14,7 +14,91 @@ concurrency: cancel-in-progress: true jobs: + relay-result: + if: >- + github.event.action == 'opened' && + startsWith(github.event.pull_request.head.ref, 'chore/jules-ocr-review-') && + startsWith(github.event.pull_request.title, 'Review PR #') + 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 = ``; + 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: @@ -117,7 +201,7 @@ jobs: 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, create commits, publish a branch, or open another pull request. + 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 @@ -132,7 +216,7 @@ jobs: 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. - Post the final review result back to this issue. Keep this task review-only.`; + Keep this task review-only and ensure the complete report is present in the final result PR body.`; let issue; if (existing) { @@ -157,9 +241,9 @@ jobs: issue = created.data; } - // The normal GITHUB_TOKEN intentionally prepares the issue only. - // The next step adds "jules" with a user token so the Jules GitHub - // App sees a user-authored label event and consumes the user's plan. + // 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"); @@ -203,7 +287,7 @@ jobs: 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. This is only used to add the 'jules' label; no Jules API key is used." + 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 @@ -220,4 +304,6 @@ jobs: issue_number: issueNumber, labels: ["jules"], }); - core.notice(`Jules review triggered through issue #${issueNumber} using the user's GitHub identity.`); + core.notice( + `Jules review triggered through issue #${issueNumber} using the user's GitHub identity.`, + ); From 3b7820549863cc70d26b65bed41a681fe68eea95 Mon Sep 17 00:00:00 2001 From: Leone Marcos Date: Sun, 13 Sep 2026 20:17:14 -0300 Subject: [PATCH 4/4] fix(ci): enforce one Jules review per PR --- .github/workflows/jules-ocr-review.yml | 48 ++++++++++++-------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/.github/workflows/jules-ocr-review.yml b/.github/workflows/jules-ocr-review.yml index 997a31f..db81dc9 100644 --- a/.github/workflows/jules-ocr-review.yml +++ b/.github/workflows/jules-ocr-review.yml @@ -2,7 +2,7 @@ name: Jules OCR Review on: pull_request: - types: [opened, reopened, synchronize, ready_for_review, closed] + types: [opened, ready_for_review, closed] permissions: contents: read @@ -17,8 +17,7 @@ jobs: relay-result: if: >- github.event.action == 'opened' && - startsWith(github.event.pull_request.head.ref, 'chore/jules-ocr-review-') && - startsWith(github.event.pull_request.title, 'Review PR #') + startsWith(github.event.pull_request.head.ref, 'chore/jules-ocr-review-') runs-on: ubuntu-latest steps: @@ -185,6 +184,15 @@ jobs: 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} @@ -218,28 +226,14 @@ jobs: Keep this task review-only and ensure the complete report is present in the final result PR body.`; - let issue; - if (existing) { - const updated = await github.rest.issues.update({ - owner, - repo, - issue_number: existing.number, - title, - body, - state: "open", - labels: [reviewLabel], - }); - issue = updated.data; - } else { - const created = await github.rest.issues.create({ - owner, - repo, - title, - body, - labels: [reviewLabel], - }); - issue = created.data; - } + 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. @@ -294,10 +288,12 @@ jobs: - 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("${{ steps.prepare.outputs.issue-number }}"); + const issueNumber = Number(process.env.ISSUE_NUMBER); await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo,