feat: add PR walkthrough generation workflow (#2933)

This commit is contained in:
Carlos Florêncio
2026-08-22 21:46:57 +01:00
committed by GitHub
parent 906be89036
commit eb0df0a492
31 changed files with 4852 additions and 34 deletions
+41
View File
@@ -0,0 +1,41 @@
import { tool } from "@opencode-ai/plugin"
import path from "path"
export default tool({
description:
"Read one regular UTF-8 file from the immutable pull request head. Use this instead of repository read tools for PR-head contents.",
args: {
path: tool.schema
.string()
.max(4096)
.describe("Strict repository-relative path at the pull request head"),
},
async execute(args, context) {
const helper = path.join(context.worktree, ".opencode-walkthrough", "read-file")
const sourceEnv = globalThis.process.env
const headSha = sourceEnv.HEAD_SHA ?? ""
const childEnv = Object.fromEntries(
["PATH"]
.filter((name) => sourceEnv[name] !== undefined)
.map((name) => [name, sourceEnv[name] as string]),
)
const child = Bun.spawn(
["python3", helper, "--repo", context.worktree, "--head-sha", headSha, "--path", args.path],
{
cwd: context.worktree,
env: childEnv,
stdout: "pipe",
stderr: "pipe",
},
)
const [status, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
])
if (status !== 0) {
return `PR file read rejected. Choose a regular UTF-8 file from the changed-file list.\n${stderr || stdout}`
}
return stdout
},
})
@@ -0,0 +1,38 @@
import { tool } from "@opencode-ai/plugin"
import path from "path"
export default tool({
description:
"Validate and render a complete PR walkthrough JSON object. Retry with corrected JSON when this tool reports a renderer error.",
args: {
walkthrough: tool.schema
.string()
.max(2_000_000)
.describe("The complete PR walkthrough JSON object as a string"),
},
async execute(args, context) {
const helper = path.join(context.worktree, ".opencode-walkthrough", "render")
const sourceEnv = globalThis.process.env
const childEnv = Object.fromEntries(
["PATH", "PR_NUMBER", "PR_TITLE", "PR_URL", "PR_REPO", "PR_BASE", "PR_HEAD"]
.filter((name) => sourceEnv[name] !== undefined)
.map((name) => [name, sourceEnv[name] as string]),
)
const child = Bun.spawn(["python3", helper], {
cwd: context.worktree,
env: childEnv,
stdin: new Blob([args.walkthrough]),
stdout: "pipe",
stderr: "pipe",
})
const [status, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
])
if (status !== 0) {
return `Renderer rejected the walkthrough. Correct the JSON and call this tool again.\n${stderr || stdout}`
}
return stdout.trim()
},
})
@@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""Contract tests for PR walkthrough generation, publication, and linking."""
from pathlib import Path
import re
import unittest
REPO_ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "pr-walkthrough.yml"
REVIEW_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "opencode-code-review.yml"
LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
SETUP_OPENCODE_ACTION = REPO_ROOT / ".github" / "actions" / "setup-opencode" / "action.yml"
OPENCODE_RENDER_TOOL = REPO_ROOT / ".github" / "scripts" / "opencode-pr-walkthrough-tool.ts"
OPENCODE_READ_TOOL = REPO_ROOT / ".github" / "scripts" / "opencode-pr-file-tool.ts"
PR_BODY_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-pr-body"
PR_FILE_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-read-file"
def workflow_job(workflow: str, name: str) -> str:
marker = f" {name}:\n"
_, separator, remainder = workflow.partition(marker)
if not separator:
raise AssertionError(f"workflow job is missing: {name}")
next_job = re.search(r"^ [A-Za-z0-9_-]+:\n", remainder, re.MULTILINE)
return remainder[: next_job.start()] if next_job else remainder
class PRWalkthroughWorkflowContractTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
if not WORKFLOW.is_file():
raise AssertionError("PR walkthrough must have a dedicated workflow file")
cls.workflow = WORKFLOW.read_text(encoding="utf-8")
cls.review_workflow = REVIEW_WORKFLOW.read_text(encoding="utf-8")
cls.generation = workflow_job(cls.workflow, "pr-walkthrough-generate")
cls.publication = workflow_job(cls.workflow, "pr-walkthrough-publish")
cls.link = workflow_job(cls.workflow, "pr-walkthrough-link")
cls.same_repo_review = workflow_job(cls.review_workflow, "opencode-review-same-repo")
cls.fork_review = workflow_job(cls.review_workflow, "opencode-review-fork")
def test_walkthrough_is_separate_and_independently_enabled(self) -> None:
self.assertNotIn("pr-walkthrough-generate:", self.review_workflow)
self.assertNotIn("pr-walkthrough-publish:", self.review_workflow)
self.assertNotIn("PR_WALKTHROUGH_ENABLED", self.review_workflow)
self.assertIn("vars.PR_WALKTHROUGH_ENABLED == 'true'", self.generation)
self.assertIn("vars.PR_WALKTHROUGH_ENABLED == 'true'", self.publication)
self.assertNotIn("OPENCODE_REVIEW_ENABLED", self.workflow)
def test_generation_is_limited_to_authorized_same_repository_events(self) -> None:
self.assertIn("pull_request_target:", self.workflow)
self.assertIn(
"types: [opened, ready_for_review, reopened, labeled]",
self.workflow,
)
for condition in (
"github.event_name == 'pull_request_target'",
"github.event.pull_request.draft == false",
"github.event.pull_request.head.repo.full_name == github.repository",
"github.event.action == 'opened'",
"github.event.action == 'reopened'",
"github.event.action == 'ready_for_review'",
"github.event.action == 'labeled' && github.event.label.name == 'generate-pr-walkthrough'",
):
self.assertIn(condition, self.generation)
self.assertNotIn("synchronize", self.workflow)
def test_generation_uses_requested_model_native_high_reasoning_variant(self) -> None:
model = "opencode-go/muse-spark-1.2-contributor#high"
self.assertIn(f"PR_WALKTHROUGH_MODEL: {model}", self.workflow)
self.assertIn(f'model: "{model}"', self.generation)
self.assertIn('--model "$PR_WALKTHROUGH_MODEL"', self.generation)
self.assertNotIn("reasoningEffort", self.generation)
def test_generation_keeps_untrusted_head_out_of_secret_bearing_workspace(self) -> None:
self.assertIn("ref: ${{ github.event.pull_request.base.sha }}", self.generation)
self.assertNotIn("ref: ${{ github.event.pull_request.head.sha }}", self.generation)
self.assertIn("fetch-depth: 0", self.generation)
self.assertIn("persist-credentials: false", self.generation)
self.assertIn('test "$(git rev-parse HEAD)" = "$BASE_SHA"', self.generation)
self.assertIn('git fetch --no-tags --depth=1 origin "$HEAD_SHA"', self.generation)
self.assertIn('test "$(git rev-parse FETCH_HEAD)" = "$HEAD_SHA"', self.generation)
def test_generation_uses_trusted_base_skill_renderer_and_helper(self) -> None:
for path in (
".agents/skills/pr-walkthrough/SKILL.md",
".agents/skills/pr-walkthrough/references/build.py",
".agents/skills/pr-walkthrough/references/shell.html",
"scripts/pr-walkthrough-render",
"scripts/pr-walkthrough-read-file",
".github/scripts/opencode-pr-walkthrough-tool.ts",
".github/scripts/opencode-pr-file-tool.ts",
):
self.assertIn(f'git show "$BASE_SHA:{path}"', self.generation)
self.assertIn("git diff --find-renames --find-copies", self.generation)
self.assertIn("git diff --name-only", self.generation)
def test_opencode_walkthrough_agent_is_read_only(self) -> None:
self.assertIn('permission:\n "*": deny', self.generation)
for allowed in ("read: allow", "glob: allow", "grep: allow"):
self.assertIn(allowed, self.generation)
self.assertIn("external_directory: deny", self.generation)
self.assertIn("--agent github-pr-walkthrough", self.generation)
self.assertIn("--file .opencode-walkthrough/guidelines.md", self.generation)
self.assertIn("render_pr_walkthrough: allow", self.generation)
self.assertIn("read_pr_file: allow", self.generation)
self.assertNotIn("CLOUDFLARE_R2", self.generation)
self.assertNotIn("pull-requests: write", self.generation)
def test_managed_render_tool_is_fixed_to_the_trusted_helper(self) -> None:
self.assertTrue(OPENCODE_RENDER_TOOL.is_file())
tool = OPENCODE_RENDER_TOOL.read_text(encoding="utf-8")
self.assertIn('".opencode-walkthrough", "render"', tool)
self.assertIn('Bun.spawn(["python3", helper]', tool)
self.assertIn("const childEnv", tool)
self.assertNotIn("globalThis.process.env }", tool)
self.assertNotIn("OPENCODE_API_KEY", tool)
self.assertNotIn("args.command", tool)
for value in (
"PR_NUMBER: ${{ github.event.pull_request.number }}",
"PR_TITLE: ${{ github.event.pull_request.title }}",
"PR_URL: ${{ github.event.pull_request.html_url }}",
"PR_REPO: ${{ github.repository }}",
"PR_BASE: ${{ github.event.pull_request.base.ref }}",
"PR_HEAD: ${{ github.event.pull_request.head.ref }}",
):
self.assertIn(value, self.generation)
def test_pr_head_reader_rejects_filesystem_access_and_uses_fixed_helper(self) -> None:
self.assertTrue(OPENCODE_READ_TOOL.is_file())
self.assertTrue(PR_FILE_HELPER.is_file())
tool = OPENCODE_READ_TOOL.read_text(encoding="utf-8")
self.assertIn('".opencode-walkthrough", "read-file"', tool)
self.assertIn("Bun.spawn(", tool)
self.assertIn('"python3", helper', tool)
self.assertIn('"--head-sha", headSha', tool)
self.assertIn('"--path", args.path', tool)
self.assertNotIn("OPENCODE_API_KEY", tool)
def test_generation_uploads_agent_built_outputs_and_diagnostics(self) -> None:
self.assertIn("Verify agent-built walkthrough", self.generation)
self.assertIn('"docs/pr-walkthrough/pr-${PR_NUMBER}.json"', self.generation)
self.assertIn('"docs/pr-walkthrough/pr-${PR_NUMBER}.html"', self.generation)
self.assertIn(
"uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a",
self.generation,
)
self.assertIn("- name: Stage walkthrough artifact\n if: always()", self.generation)
self.assertIn("path: pr-walkthrough-artifact/", self.generation)
self.assertIn("if-no-files-found: error", self.generation)
def test_walkthrough_runs_are_serialized_for_the_entire_pr_pipeline(self) -> None:
workflow_header = self.workflow.split("jobs:", 1)[0]
self.assertIn("concurrency:", workflow_header)
self.assertIn("group: pr-walkthrough-${{ github.event.pull_request.number }}", workflow_header)
self.assertIn("cancel-in-progress: true", workflow_header)
self.assertNotIn("concurrency:", self.generation)
self.assertIn("concurrency:", self.same_repo_review)
self.assertIn("concurrency:", self.fork_review)
def test_walkthrough_label_does_not_run_the_normal_code_review(self) -> None:
self.assertIn("github.event.action != 'labeled'", self.same_repo_review)
self.assertIn(
"github.event.label.name != 'generate-pr-walkthrough'",
self.same_repo_review,
)
def test_all_opencode_jobs_use_the_trusted_reusable_setup_action(self) -> None:
workflows = self.workflow + self.review_workflow
self.assertEqual(workflows.count("uses: ./.trusted-actions/setup-opencode"), 3)
self.assertEqual(
workflows.count(
'git show "$BASE_SHA:.github/actions/setup-opencode/action.yml"'
),
3,
)
self.assertNotIn("curl -fsSL", workflows)
self.assertTrue(SETUP_OPENCODE_ACTION.is_file())
action = SETUP_OPENCODE_ACTION.read_text(encoding="utf-8")
self.assertIn(
"60fe5a92dc9af64ec079348fedde17e12da6a867efe7e8353be8038480607924",
action,
)
def test_fork_job_fetches_base_before_materializing_setup_action(self) -> None:
fetch = self.fork_review.index('git fetch --no-tags --prune origin "$BASE_SHA"')
materialize = self.fork_review.index(
'git show "$BASE_SHA:.github/actions/setup-opencode/action.yml"'
)
self.assertLess(fetch, materialize)
def test_publication_is_artifact_only_and_exports_validated_url(self) -> None:
self.assertIn("needs: pr-walkthrough-generate", self.publication)
self.assertIn(
"uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c",
self.publication,
)
self.assertNotIn("actions/checkout", self.publication)
self.assertNotIn("opencode run", self.publication.lower())
self.assertNotIn("CLOUDFLARE_API_TOKEN", self.publication)
self.assertIn("permissions: {}", self.publication)
self.assertIn("url: ${{ steps.publish.outputs.url }}", self.publication)
self.assertIn("printf 'url=%s\\n' \"$PUBLIC_URL\"", self.publication)
def test_publication_uploads_only_html_with_expected_metadata(self) -> None:
for value in (
'OBJECT_KEY="pr/${PR_NUMBER}/${HEAD_SHA}.html"',
"aws s3 cp",
'--content-type "text/html; charset=utf-8"',
'--cache-control "public, max-age=300"',
"aws s3api head-object",
'test "$content_type" = "text/html; charset=utf-8"',
'test "$content_length" -gt 0',
"CLOUDFLARE_R2_ACCESS_KEY_ID",
"CLOUDFLARE_R2_SECRET_ACCESS_KEY",
):
self.assertIn(value, self.publication)
def test_r2_secrets_are_scoped_to_the_upload_step(self) -> None:
job_header, _, steps = self.publication.partition(" steps:\n")
self.assertNotIn("CLOUDFLARE_R2_ACCESS_KEY_ID", job_header)
self.assertNotIn("CLOUDFLARE_R2_SECRET_ACCESS_KEY", job_header)
upload_step = steps.split(" - name:", 2)[2]
self.assertIn("CLOUDFLARE_R2_ACCESS_KEY_ID", upload_step)
self.assertIn("CLOUDFLARE_R2_SECRET_ACCESS_KEY", upload_step)
def test_publication_validates_public_url_before_linking(self) -> None:
self.assertIn("curl --fail", self.publication)
self.assertIn("--retry 5", self.publication)
self.assertIn("--retry-all-errors", self.publication)
self.assertIn("--retry-delay 5", self.publication)
self.assertIn("--retry-max-time 120", self.publication)
self.assertIn('cmp -- "$HTML_PATH" "$FETCHED_PATH"', self.publication)
self.assertIn("GITHUB_STEP_SUMMARY", self.publication)
self.assertIn("https://walkthrough.kandev.ai", self.publication)
self.assertNotIn("pull-requests: write", self.publication)
self.assertNotIn("gh api", self.publication)
def test_link_job_uses_trusted_base_helper_and_minimal_write_permission(self) -> None:
self.assertIn("needs: pr-walkthrough-publish", self.link)
self.assertIn("contents: read", self.link)
self.assertIn("pull-requests: write", self.link)
self.assertIn("ref: ${{ github.event.pull_request.base.sha }}", self.link)
self.assertIn("persist-credentials: false", self.link)
self.assertIn('test "$(git rev-parse HEAD)" = "$BASE_SHA"', self.link)
self.assertIn("scripts/pr-walkthrough-pr-body", self.link)
self.assertIn("--github-response", self.link)
self.assertIn("--input", self.link)
self.assertIn("needs.pr-walkthrough-publish.outputs.url", self.link)
self.assertNotIn("CLOUDFLARE_R2", self.link)
self.assertNotIn("OPENCODE_API_KEY", self.link)
self.assertTrue(PR_BODY_HELPER.is_file())
def test_lint_workflow_runs_contract_and_body_helper_tests(self) -> None:
lint_workflow = LINT_WORKFLOW.read_text(encoding="utf-8")
for command in (
"python3 .github/scripts/pr-walkthrough-workflow-contract_test.py",
"python3 scripts/pr-walkthrough-pr-body.test.py",
"python3 scripts/pr-walkthrough-read-file.test.py",
):
self.assertIn(command, lint_workflow)
if __name__ == "__main__":
unittest.main()