From 64c0753ff0400fcf13b5e4288b053b05af02b30f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 30 Jun 2026 16:48:15 +0900 Subject: [PATCH 1/2] Fix OpenCode prompt template shell expansion --- .github/workflows/opencode-review.yml | 12 +++-- scripts/ci/render_opencode_prompt_template.py | 44 +++++++++++++++++++ tests/test_opencode_agent_contract.py | 8 ++++ tests/test_render_opencode_prompt_template.py | 26 +++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 scripts/ci/render_opencode_prompt_template.py create mode 100644 tests/test_render_opencode_prompt_template.py diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index c29d33b84..a0de9a179 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1891,7 +1891,7 @@ jobs: printf 'review_status=%s\n' "$1" >>"$GITHUB_OUTPUT" } prompt_file="${RUNNER_TEMP}/opencode-review-prompt.md" - cat >"$prompt_file" <"$prompt_file" <<'EOF' Review PR #${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR}. The trusted workflow checkout is ${GITHUB_WORKSPACE}; inspect the pull request head source only from ${OPENCODE_SOURCE_WORKDIR}. Be general-purpose and meticulous: actively consult CodeGraph MCP for structural checks, DeepWiki for repo docs, Context7 for current library/API docs, and web_search for bounded external lookups such as action/tool release facts, industry standards, international standards, official platform specifications, and comparable issue or PR precedents when applicable. Do not rely on model memory for user-claimed concepts, standards, runtime support, or domain terminology when a search source is available. If a configured MCP source is unavailable or not applicable, say so briefly in the review summary. Inspect changed files and focused hunks directly when MCP evidence is insufficient. Do not claim repository docs, images, or reference assets are unavailable, missing, or absent unless the changed docs repository tree evidence proves it. If an external MCP source is unavailable, state that as a source limitation, not as a repository fact. Structural exploration is mandatory for every PR, including dependency-only, lockfile-only, workflow-only, docs-only, and no-source-code changes; inspect the relevant manifest, lockfile, workflow, config, docs, dependency edges, generated side effects, code-to-documentation consistency, documentation-to-code consistency, and test-command contracts. Docs-only changes still require CodeGraph, DeepWiki, Context7, or web_search evidence when they make claims about behavior, APIs, setup, workflows, dependencies, standards, or product/domain concepts. If changed documentation contradicts current code, generated behavior, official docs, repository docs, or reachable standards evidence, request changes with a source-backed fix direction: either fix the documentation claim or update the code/contract that makes the claim false. Never state that structural exploration, structural analysis, or structural review is not required or unnecessary. If structural exploration was not possible or changed files could not be inspected after reading bounded-review-evidence.md and the changed files, do not approve. If evidence is truncated, inspect focused hunks and changed files directly before deciding. Do not request changes solely because the prompt did not inline the full evidence. Use CodeGraph for blast-radius, call graph, and test-coverage questions before broad local reads. Prefer deletion, stdlib/native platform features, and already-installed dependencies before proposing new code or packages, but do not simplify away trust-boundary validation, data-loss handling, security, accessibility, or required tests. Follow the Review language evidence section: write human-readable review prose in Korean when the PR title or body is primarily Korean, and in English when it is primarily English. Keep file paths, code identifiers, commands, logs, quoted source, error text, numbers, and protocol literals unchanged. For Korean prose, preserve facts, identifiers, numbers, and quotes while removing only formulaic filler or translationese. @@ -1915,6 +1915,7 @@ jobs: APPROVE only for no blockers, and when result is APPROVE the JSON findings value must be exactly [] with no advisory, informational, already-fixed, or positive findings. REQUEST_CHANGES findings require path,line,severity,title,problem,root_cause,fix_direction,regression_test_direction,suggested_diff. The line must be a positive line number from an actual changed or relevant local file; never use line 0. Failed-check findings must be line-specific and concrete; include the failed check label, exact failed log phrase, observable impact, and trigger condition that led to the line, then provide a minimal suggested diff that changes the identified line. The regression_test_direction should name an exact test target or verification command when the repository already provides one. The suggested_diff must be source-backed and GitHub suggestion-ready when possible: every removed line in the diff must exist in the cited current local file, so do not request changes for code you did not verify in the current source. Multiple Strix model reports must not be collapsed; preserve the model name, report title, severity, endpoint, and Code Locations/path:line evidence in each finding's problem or root_cause when present. One Strix model vulnerability report requires one distinct finding; do not combine duplicate titles or matching locations from different models into one finding. Unrelated speculative findings are invalid when failed-check evidence is present. Return only the review body. EOF + python3 "$GITHUB_WORKSPACE/scripts/ci/render_opencode_prompt_template.py" "$prompt_file" cd "$OPENCODE_REVIEW_WORKDIR" opencode_json_file="${OPENCODE_OUTPUT_FILE}.jsonl" opencode_export_file="${OPENCODE_OUTPUT_FILE}.session.json" @@ -2018,7 +2019,7 @@ jobs: printf 'review_status=%s\n' "$1" >>"$GITHUB_OUTPUT" } prompt_file="${RUNNER_TEMP}/opencode-review-prompt.md" - cat >"$prompt_file" <"$prompt_file" <<'EOF' DeepSeek R1-0528 failed; review PR #${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR} with DeepSeek V3-0324. The trusted workflow checkout is ${GITHUB_WORKSPACE}; inspect the pull request head source only from ${OPENCODE_SOURCE_WORKDIR}. Be general-purpose and meticulous: actively consult CodeGraph MCP for structural checks, DeepWiki for repo docs, Context7 for current library/API docs, and web_search for bounded external lookups such as action/tool release facts, industry standards, international standards, official platform specifications, and comparable issue or PR precedents when applicable. Do not rely on model memory for user-claimed concepts, standards, runtime support, or domain terminology when a search source is available. If a configured MCP source is unavailable or not applicable, say so briefly in the review summary. Inspect changed files and focused hunks directly when MCP evidence is insufficient. Do not claim repository docs, images, or reference assets are unavailable, missing, or absent unless the changed docs repository tree evidence proves it. If an external MCP source is unavailable, state that as a source limitation, not as a repository fact. Structural exploration is mandatory for every PR, including dependency-only, lockfile-only, workflow-only, docs-only, and no-source-code changes; inspect the relevant manifest, lockfile, workflow, config, docs, dependency edges, generated side effects, code-to-documentation consistency, documentation-to-code consistency, and test-command contracts. Docs-only changes still require CodeGraph, DeepWiki, Context7, or web_search evidence when they make claims about behavior, APIs, setup, workflows, dependencies, standards, or product/domain concepts. If changed documentation contradicts current code, generated behavior, official docs, repository docs, or reachable standards evidence, request changes with a source-backed fix direction: either fix the documentation claim or update the code/contract that makes the claim false. Never state that structural exploration, structural analysis, or structural review is not required or unnecessary. If structural exploration was not possible or changed files could not be inspected after reading bounded-review-evidence.md and the changed files, do not approve. If evidence is truncated, inspect focused hunks and changed files directly before deciding. Do not request changes solely because the prompt did not inline the full evidence. Use CodeGraph for blast-radius, call graph, and test-coverage questions before broad local reads. Prefer deletion, stdlib/native platform features, and already-installed dependencies before proposing new code or packages, but do not simplify away trust-boundary validation, data-loss handling, security, accessibility, or required tests. Follow the Review language evidence section: write human-readable review prose in Korean when the PR title or body is primarily Korean, and in English when it is primarily English. Keep file paths, code identifiers, commands, logs, quoted source, error text, numbers, and protocol literals unchanged. For Korean prose, preserve facts, identifiers, numbers, and quotes while removing only formulaic filler or translationese. @@ -2042,6 +2043,7 @@ jobs: APPROVE only for no blockers, and when result is APPROVE the JSON findings value must be exactly [] with no advisory, informational, already-fixed, or positive findings. REQUEST_CHANGES findings require path,line,severity,title,problem,root_cause,fix_direction,regression_test_direction,suggested_diff. The line must be a positive line number from an actual changed or relevant local file; never use line 0. Failed-check findings must be line-specific and concrete; include the failed check label, exact failed log phrase, observable impact, and trigger condition that led to the line, then provide a minimal suggested diff that changes the identified line. The regression_test_direction should name an exact test target or verification command when the repository already provides one. The suggested_diff must be source-backed and GitHub suggestion-ready when possible: every removed line in the diff must exist in the cited current local file, so do not request changes for code you did not verify in the current source. Multiple Strix model reports must not be collapsed; preserve the model name, report title, severity, endpoint, and Code Locations/path:line evidence in each finding's problem or root_cause when present. One Strix model vulnerability report requires one distinct finding; do not combine duplicate titles or matching locations from different models into one finding. Unrelated speculative findings are invalid when failed-check evidence is present. Return only the review body. EOF + python3 "$GITHUB_WORKSPACE/scripts/ci/render_opencode_prompt_template.py" "$prompt_file" cd "$OPENCODE_REVIEW_WORKDIR" opencode_json_file="${OPENCODE_OUTPUT_FILE}.jsonl" opencode_export_file="${OPENCODE_OUTPUT_FILE}.session.json" @@ -2146,7 +2148,7 @@ jobs: printf 'review_status=%s\n' "$1" >>"$GITHUB_OUTPUT" } prompt_file="${RUNNER_TEMP}/opencode-review-prompt.md" - cat >"$prompt_file" <"$prompt_file" <<'EOF' DeepSeek R1-0528 and DeepSeek V3-0324 failed; review PR #${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR} with GPT-5. The trusted workflow checkout is ${GITHUB_WORKSPACE}; inspect the pull request head source only from ${OPENCODE_SOURCE_WORKDIR}. Be general-purpose and meticulous: actively consult CodeGraph MCP for structural checks, DeepWiki for repo docs, Context7 for current library/API docs, and web_search for bounded external lookups such as action/tool release facts, industry standards, international standards, official platform specifications, and comparable issue or PR precedents when applicable. Do not rely on model memory for user-claimed concepts, standards, runtime support, or domain terminology when a search source is available. If a configured MCP source is unavailable or not applicable, say so briefly in the review summary. Inspect changed files and focused hunks directly when MCP evidence is insufficient. Do not claim repository docs, images, or reference assets are unavailable, missing, or absent unless the changed docs repository tree evidence proves it. If an external MCP source is unavailable, state that as a source limitation, not as a repository fact. Structural exploration is mandatory for every PR, including dependency-only, lockfile-only, workflow-only, docs-only, and no-source-code changes; inspect the relevant manifest, lockfile, workflow, config, docs, dependency edges, generated side effects, code-to-documentation consistency, documentation-to-code consistency, and test-command contracts. Docs-only changes still require CodeGraph, DeepWiki, Context7, or web_search evidence when they make claims about behavior, APIs, setup, workflows, dependencies, standards, or product/domain concepts. If changed documentation contradicts current code, generated behavior, official docs, repository docs, or reachable standards evidence, request changes with a source-backed fix direction: either fix the documentation claim or update the code/contract that makes the claim false. Never state that structural exploration, structural analysis, or structural review is not required or unnecessary. If structural exploration was not possible or changed files could not be inspected after reading bounded-review-evidence.md and the changed files, do not approve. If evidence is truncated, inspect focused hunks and changed files directly before deciding. Do not request changes solely because the prompt did not inline the full evidence. Use CodeGraph for blast-radius, call graph, and test-coverage questions before broad local reads. Prefer deletion, stdlib/native platform features, and already-installed dependencies before proposing new code or packages, but do not simplify away trust-boundary validation, data-loss handling, security, accessibility, or required tests. Follow the Review language evidence section: write human-readable review prose in Korean when the PR title or body is primarily Korean, and in English when it is primarily English. Keep file paths, code identifiers, commands, logs, quoted source, error text, numbers, and protocol literals unchanged. For Korean prose, preserve facts, identifiers, numbers, and quotes while removing only formulaic filler or translationese. @@ -2170,6 +2172,7 @@ jobs: APPROVE only for no blockers, and when result is APPROVE the JSON findings value must be exactly [] with no advisory, informational, already-fixed, or positive findings. REQUEST_CHANGES findings require path,line,severity,title,problem,root_cause,fix_direction,regression_test_direction,suggested_diff. The line must be a positive line number from an actual changed or relevant local file; never use line 0. Failed-check findings must be line-specific and concrete; include the failed check label, exact failed log phrase, observable impact, and trigger condition that led to the line, then provide a minimal suggested diff that changes the identified line. The regression_test_direction should name an exact test target or verification command when the repository already provides one. The suggested_diff must be source-backed and GitHub suggestion-ready when possible: every removed line in the diff must exist in the cited current local file, so do not request changes for code you did not verify in the current source. Multiple Strix model reports must not be collapsed; preserve the model name, report title, severity, endpoint, and Code Locations/path:line evidence in each finding's problem or root_cause when present. One Strix model vulnerability report requires one distinct finding; do not combine duplicate titles or matching locations from different models into one finding. Unrelated speculative findings are invalid when failed-check evidence is present. Return only the review body. EOF + python3 "$GITHUB_WORKSPACE/scripts/ci/render_opencode_prompt_template.py" "$prompt_file" cd "$OPENCODE_REVIEW_WORKDIR" opencode_json_file="${OPENCODE_OUTPUT_FILE}.jsonl" opencode_export_file="${OPENCODE_OUTPUT_FILE}.session.json" @@ -2296,7 +2299,7 @@ jobs: opencode_json_file="${candidate_output_file}.jsonl" opencode_export_file="${candidate_output_file}.session.json" prompt_file="${RUNNER_TEMP}/opencode-review-${model_candidate//\//-}-prompt.md" - cat >"$prompt_file" <"$prompt_file" <<'EOF' DeepSeek R1-0528, DeepSeek V3-0324, and GPT-5 did not produce a usable review. Review PR #${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR} with ${model_candidate}. The trusted workflow checkout is ${GITHUB_WORKSPACE}; inspect the pull request head source only from ${OPENCODE_SOURCE_WORKDIR}. CodeGraph MCP is mandatory for structural checks. Also use DeepWiki for repo docs, Context7 for current library/API docs, and web_search for bounded external lookups such as user-claimed concepts, industry standards, international standards, official platform specifications, and comparable issue or PR precedents when applicable. Do not rely on model memory for concepts, standards, runtime support, or domain terminology when a search source is available. Read ./bounded-review-evidence.md first, follow its Review language evidence for the final review language, then inspect changed files and focused hunks under the PR head worktree. Cover security/privacy boundaries, auth/authz and tenant isolation, workflow contracts, developer experience, user-facing behavior, tests, coverage/docstring evidence, cross-file compatibility, runtime-version matrix compatibility, package/build/test/lint/security contract completeness, repository conventions, accessibility/i18n, dependency license and supply-chain risk, IaC/cloud/Docker behavior, performance/resource usage, observability, deployment evidence, git history, breaking-change/backcompat impact, and regression risk. Read the Review execution contracts section before choosing commands; if source exists without a package, build, test, coverage, lint, or security contract, flag the packaging/operability gap with affected language and sample files. Compare repository-local patterns before judging DX or UX: preserve helpful automation, review, setup, documentation, and product-flow patterns from sibling repositories when they reduce cognitive load or user friction, and flag patterns that only add noise, false failures, misleading status, repeated waiting, or URL-only diagnostics. For schema, migration, database, API, workflow, security, or compliance changes, compare against nearby implementation, code conventions, reserved words, naming rules, applicable standards, and production deployment evidence before approving. @@ -2310,6 +2313,7 @@ jobs: --> APPROVE only for no blockers, and when result is APPROVE the JSON findings value must be exactly [] with no advisory, informational, already-fixed, or positive findings. Put all required Verification posture labels inside the JSON summary string itself, not only in prose after the control block. REQUEST_CHANGES findings require path,line,severity,title,problem,root_cause,fix_direction,regression_test_direction,suggested_diff. Use line-specific, source-backed findings only. Return only the review body. EOF + PROMPT_MODEL_CANDIDATE="$model_candidate" python3 "$GITHUB_WORKSPACE/scripts/ci/render_opencode_prompt_template.py" "$prompt_file" for opencode_attempt in $(seq 1 "$opencode_attempts"); do rm -f "$opencode_json_file" "$opencode_export_file" "$candidate_output_file" diff --git a/scripts/ci/render_opencode_prompt_template.py b/scripts/ci/render_opencode_prompt_template.py new file mode 100644 index 000000000..1f190a11c --- /dev/null +++ b/scripts/ci/render_opencode_prompt_template.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Render OpenCode prompt templates without shell expansion.""" + +from __future__ import annotations + +from collections.abc import Mapping +import os +from pathlib import Path +import sys + + +def placeholder_values(environ: Mapping[str, str]) -> dict[str, str]: + """Return the only workflow placeholders allowed in prompt templates.""" + return { + "${PR_NUMBER}": environ.get("PR_NUMBER", ""), + "${OPENCODE_SOURCE_WORKDIR}": environ.get("OPENCODE_SOURCE_WORKDIR", ""), + "${GITHUB_WORKSPACE}": environ.get("GITHUB_WORKSPACE", ""), + "${HEAD_SHA}": environ.get("HEAD_SHA", ""), + "${RUN_ID}": environ.get("RUN_ID", ""), + "${RUN_ATTEMPT}": environ.get("RUN_ATTEMPT", ""), + "${model_candidate}": environ.get("PROMPT_MODEL_CANDIDATE", ""), + } + + +def render_prompt(text: str, environ: Mapping[str, str]) -> str: + """Replace explicit placeholders while preserving shell metacharacters.""" + for old, new in placeholder_values(environ).items(): + text = text.replace(old, new) + return text + + +def main(argv: list[str]) -> int: + if len(argv) != 1: + print("usage: render_opencode_prompt_template.py PROMPT_FILE", file=sys.stderr) + return 2 + + prompt_path = Path(argv[0]) + text = prompt_path.read_text(encoding="utf-8") + prompt_path.write_text(render_prompt(text, os.environ), encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 001880232..579b94b74 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -82,6 +82,14 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "Packaging:" in workflow assert '"code-reviewer"' in workflow assert '"task": "allow"' in workflow + assert 'cat >"$prompt_file" <"$prompt_file" <<\'EOF\'') == 4 + assert workflow.count("render_opencode_prompt_template.py") == 4 + assert ( + 'PROMPT_MODEL_CANDIDATE="$model_candidate" ' + 'python3 "$GITHUB_WORKSPACE/scripts/ci/render_opencode_prompt_template.py" ' + '"$prompt_file"' + ) in workflow def test_merge_scheduler_uses_escalating_mutation_credentials(): diff --git a/tests/test_render_opencode_prompt_template.py b/tests/test_render_opencode_prompt_template.py new file mode 100644 index 000000000..98c092b22 --- /dev/null +++ b/tests/test_render_opencode_prompt_template.py @@ -0,0 +1,26 @@ +from scripts.ci.render_opencode_prompt_template import render_prompt + + +def test_render_prompt_replaces_only_explicit_placeholders(): + text = ( + "Review PR #${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR} with " + "${model_candidate}.\n" + "`python3 scripts/ci/sandboxed_verify.py --repo-root " + '"$OPENCODE_SOURCE_WORKDIR" -- `\n' + "$(echo should_not_run)\n" + ) + + rendered = render_prompt( + text, + { + "PR_NUMBER": "193", + "OPENCODE_SOURCE_WORKDIR": "/tmp/pr-head", + "PROMPT_MODEL_CANDIDATE": "github-models/openai/o4-mini", + }, + ) + + assert "Review PR #193 in /tmp/pr-head" in rendered + assert "github-models/openai/o4-mini" in rendered + assert '"$OPENCODE_SOURCE_WORKDIR"' in rendered + assert "`python3 scripts/ci/sandboxed_verify.py" in rendered + assert "$(echo should_not_run)" in rendered From 51b4220f9cc75342489f3b3ff40b5481360bffe8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 30 Jun 2026 16:55:19 +0900 Subject: [PATCH 2/2] Cover OpenCode prompt renderer entrypoint --- tests/test_render_opencode_prompt_template.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/test_render_opencode_prompt_template.py b/tests/test_render_opencode_prompt_template.py index 98c092b22..113941d8c 100644 --- a/tests/test_render_opencode_prompt_template.py +++ b/tests/test_render_opencode_prompt_template.py @@ -1,4 +1,9 @@ -from scripts.ci.render_opencode_prompt_template import render_prompt +import runpy +import sys + +import pytest + +from scripts.ci.render_opencode_prompt_template import main, render_prompt def test_render_prompt_replaces_only_explicit_placeholders(): @@ -24,3 +29,37 @@ def test_render_prompt_replaces_only_explicit_placeholders(): assert '"$OPENCODE_SOURCE_WORKDIR"' in rendered assert "`python3 scripts/ci/sandboxed_verify.py" in rendered assert "$(echo should_not_run)" in rendered + + +def test_main_renders_prompt_file(monkeypatch, tmp_path): + prompt_file = tmp_path / "prompt.md" + prompt_file.write_text("PR ${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR}\n", encoding="utf-8") + monkeypatch.setenv("PR_NUMBER", "193") + monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", "/tmp/pr-head") + + assert main([str(prompt_file)]) == 0 + + assert prompt_file.read_text(encoding="utf-8") == "PR 193 in /tmp/pr-head\n" + + +def test_main_rejects_wrong_arg_count(capsys): + assert main([]) == 2 + + assert "usage: render_opencode_prompt_template.py PROMPT_FILE" in capsys.readouterr().err + + +def test_module_entrypoint(monkeypatch, tmp_path): + prompt_file = tmp_path / "prompt.md" + prompt_file.write_text("${HEAD_SHA}\n", encoding="utf-8") + monkeypatch.setattr( + sys, + "argv", + ["render_opencode_prompt_template.py", str(prompt_file)], + ) + monkeypatch.setenv("HEAD_SHA", "abc123") + + with pytest.raises(SystemExit) as exc_info: + runpy.run_module("scripts.ci.render_opencode_prompt_template", run_name="__main__") + + assert exc_info.value.code == 0 + assert prompt_file.read_text(encoding="utf-8") == "abc123\n"