fix: harden portable PR walkthrough runner (#2942)
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
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
|
||||
},
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
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()
|
||||
},
|
||||
})
|
||||
@@ -11,10 +11,13 @@ 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"
|
||||
SKILL_DIR = REPO_ROOT / ".agents" / "skills" / "pr-walkthrough"
|
||||
SKILL_CONTEXT_HELPER = SKILL_DIR / "scripts" / "pr-walkthrough-context"
|
||||
SKILL_RENDER_HELPER = SKILL_DIR / "scripts" / "pr-walkthrough-render"
|
||||
PR_BODY_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-pr-body"
|
||||
PR_FILE_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-read-file"
|
||||
ROOT_CONTEXT_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-context"
|
||||
ROOT_RENDER_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-render"
|
||||
ROOT_READ_HELPER = REPO_ROOT / "scripts" / "pr-walkthrough-read-file"
|
||||
|
||||
|
||||
def workflow_job(workflow: str, name: str) -> str:
|
||||
@@ -33,6 +36,7 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
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.skill = (SKILL_DIR / "SKILL.md").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")
|
||||
@@ -50,7 +54,7 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
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]",
|
||||
"types: [opened, ready_for_review, reopened, synchronize, labeled]",
|
||||
self.workflow,
|
||||
)
|
||||
for condition in (
|
||||
@@ -60,10 +64,15 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
"github.event.action == 'opened'",
|
||||
"github.event.action == 'reopened'",
|
||||
"github.event.action == 'ready_for_review'",
|
||||
"github.event.action == 'synchronize'",
|
||||
"github.event.action == 'labeled' && github.event.label.name == 'generate-pr-walkthrough'",
|
||||
):
|
||||
self.assertIn(condition, self.generation)
|
||||
self.assertNotIn("synchronize", self.workflow)
|
||||
|
||||
def test_skill_explains_trusted_context_without_provider_names(self) -> None:
|
||||
self.assertNotIn("Kandev", self.skill)
|
||||
self.assertIn("managed runner", self.skill)
|
||||
self.assertIn("arbitrary Git or shell commands", self.skill)
|
||||
|
||||
def test_generation_uses_requested_model_native_high_reasoning_variant(self) -> None:
|
||||
model = "opencode-go/muse-spark-1.2-contributor#high"
|
||||
@@ -78,20 +87,30 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
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(
|
||||
'git fetch --no-tags --filter=blob:none '
|
||||
'--negotiation-tip="$BASE_SHA" origin "$HEAD_SHA"',
|
||||
self.generation,
|
||||
)
|
||||
self.assertNotIn("--depth=1", self.generation)
|
||||
self.assertIn('test "$(git rev-parse FETCH_HEAD)" = "$HEAD_SHA"', self.generation)
|
||||
self.assertIn('git merge-base "$BASE_SHA" "$HEAD_SHA"', self.generation)
|
||||
|
||||
def test_generation_uses_trusted_base_skill_renderer_and_helper(self) -> None:
|
||||
self.assertIn(
|
||||
'git archive "$BASE_SHA" .agents/skills/pr-walkthrough | tar -x',
|
||||
self.generation,
|
||||
)
|
||||
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",
|
||||
'test -f "$SKILL_DIR/SKILL.md"',
|
||||
'test -f "$SKILL_DIR/references/build.py"',
|
||||
'test -f "$SKILL_DIR/references/shell.html"',
|
||||
'test -f "$SKILL_DIR/scripts/pr-walkthrough-context"',
|
||||
'test -f "$SKILL_DIR/scripts/pr-walkthrough-render"',
|
||||
):
|
||||
self.assertIn(f'git show "$BASE_SHA:{path}"', self.generation)
|
||||
self.assertIn(path, self.generation)
|
||||
self.assertNotIn('git show "$BASE_SHA:scripts/pr-walkthrough-render"', self.generation)
|
||||
self.assertNotIn('git show "$BASE_SHA:scripts/pr-walkthrough-read-file"', self.generation)
|
||||
self.assertIn("git diff --find-renames --find-copies", self.generation)
|
||||
self.assertIn("git diff --name-only", self.generation)
|
||||
|
||||
@@ -100,22 +119,39 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
for allowed in ("read: allow", "glob: allow", "grep: allow"):
|
||||
self.assertIn(allowed, self.generation)
|
||||
self.assertIn("external_directory: deny", self.generation)
|
||||
for denied in ("patch: deny", "task: deny", "todo: deny", "fetch: deny", "web: deny"):
|
||||
self.assertIn(denied, self.generation)
|
||||
self.assertIn('".pr-walkthrough/draft.json": allow', self.generation)
|
||||
self.assertIn(
|
||||
'"python3 .agents/skills/pr-walkthrough/scripts/pr-walkthrough-render": allow',
|
||||
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.assertIn("--file .pr-walkthrough/guidelines.md", self.generation)
|
||||
self.assertIn("--file .pr-walkthrough/walkthrough.patch", self.generation)
|
||||
self.assertIn(".pr-walkthrough/head-context/manifest.json", self.generation)
|
||||
self.assertNotIn("render_pr_walkthrough", self.generation)
|
||||
self.assertNotIn("read_pr_file", self.generation)
|
||||
self.assertNotIn('mkdir -p .opencode/agents .opencode/tools', 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)
|
||||
def test_skill_bundle_is_the_only_walkthrough_generation_package(self) -> None:
|
||||
self.assertTrue(SKILL_CONTEXT_HELPER.is_file())
|
||||
self.assertTrue(SKILL_RENDER_HELPER.is_file())
|
||||
self.assertFalse(ROOT_CONTEXT_HELPER.exists())
|
||||
self.assertFalse(ROOT_RENDER_HELPER.exists())
|
||||
self.assertFalse(ROOT_READ_HELPER.exists())
|
||||
self.assertFalse((REPO_ROOT / "scripts" / "pr-walkthrough-read-file.test.py").exists())
|
||||
|
||||
def test_opencode_typescript_tools_are_removed(self) -> None:
|
||||
for path in (
|
||||
REPO_ROOT / ".github" / "scripts" / "opencode-pr-walkthrough-tool.ts",
|
||||
REPO_ROOT / ".github" / "scripts" / "opencode-pr-file-tool.ts",
|
||||
):
|
||||
self.assertFalse(path.exists())
|
||||
self.assertNotIn("opencode-pr-walkthrough-tool.ts", self.generation)
|
||||
self.assertNotIn("opencode-pr-file-tool.ts", self.generation)
|
||||
for value in (
|
||||
"PR_NUMBER: ${{ github.event.pull_request.number }}",
|
||||
"PR_TITLE: ${{ github.event.pull_request.title }}",
|
||||
@@ -126,17 +162,6 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
):
|
||||
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)
|
||||
@@ -256,7 +281,8 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
|
||||
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",
|
||||
"python3 .agents/skills/pr-walkthrough/scripts/pr-walkthrough-context.test.py",
|
||||
"python3 .agents/skills/pr-walkthrough/scripts/pr-walkthrough-render.test.py",
|
||||
):
|
||||
self.assertIn(command, lint_workflow)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user