Skip to content

fix: substitute {pr} in posting-format.md and log html_url - #111

Open
derekmisler wants to merge 4 commits into
mainfrom
fix/posting-format-pr-placeholder
Open

fix: substitute {pr} in posting-format.md and log html_url#111
derekmisler wants to merge 4 commits into
mainfrom
fix/posting-format-pr-placeholder

Conversation

@derekmisler

@derekmisler derekmisler commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes the false '⚠️ Review did not complete' double-posts seen in docker/sandboxes (evidence: run 34488043917, PR 5929), and eliminates the guaranteed 404 on every feedback reply.

Root cause: {pr} in gh api URLs is not a gh CLI template variable — only {owner} and {repo} are. The literal string {pr} produced a 404 on first attempt. For the review path, the agent retried but the retry only logged {id, state} (no html_url), so the pullrequestreview-[0-9]+ grep in action.yml missed it and posted a spurious '⚠️ Review did not complete' on top of a completed review. For the reply path, every reply paid a guaranteed 404 + retry.

Changes

review-pr/agents/refs/posting-format.md

  • Keep {pr} as the placeholder (baked in at render time by sed)
  • Add set -o pipefail before the posting pipeline so gh api failures propagate
  • Pipe through | jq '{id, state, html_url}' so html_url (containing pullrequestreview-XXXXXXX) is always logged

review-pr/action.yml — 'Copy reference files' step

  • Add PR_NUMBER to env block; validate it is numeric before substitution (mirrors the SHA guard)
  • Substitute {pr} → actual PR number via sed alongside __PR_HEAD_SHA__
  • Broaden the {pr} guard to grep -rq '{pr}' /tmp/refs/ (catches future placeholders in any staged ref file)
  • Broaden the validation error message to cover all failure conditions

review-pr/reply/action.yml (new 'Stage reply agent' step)

  • Render pr-review-reply.yaml to /tmp/pr-review-reply.yaml with {pr} substituted; fail-open with a warning if pr-number is missing/non-numeric
  • Point the run-reply step at the rendered copy; add pr-number input

.github/workflows/review-pr.yml

  • Pass pr-number: ${{ steps.feedback.outputs.pr-number }} at the reply call site

src/resolve-trigger-context/__tests__/workflow-security.test.ts

  • Extend runCopyReference() with prNumber param + 3 new cases (empty, non-numeric, shell metacharacters)
  • Add runStageReplyAgent() helper + 4 new cases for the reply staging step

Testing

pnpm lint (biome + tsc + actionlint) and pnpm test (1050 tests) pass clean.

Closes #112

posting-format.md used {pr} in the gh api URL, but {pr} is not a gh
CLI template variable — only {owner} and {repo} are. The literal string
{pr} produced a 404 on first attempt, forcing the agent to retry with
an explicit URL. The retry only logged {id, state} (no html_url), so
the pullrequestreview-[0-9]+ grep in action.yml missed it and posted a
spurious '⚠️ Review did not complete' notice even though the real review
had already been posted.

Two changes:
1. Replace {pr} with $PR_NUMBER in posting-format.md. The action already
   resolves the PR number into steps.resolve-context.outputs.pr-number;
   expose it as PR_NUMBER in the 'Copy reference files' env block and
   substitute it via sed alongside __PR_HEAD_SHA__. Add a validation
   guard so a rendered template containing {pr} fails the step.
2. Pipe the gh api response through jq '{id, state, html_url}' so
   html_url (which contains pullrequestreview-XXXXXXX) is always logged,
   making the completion grep robust even on retries.
@derekmisler derekmisler self-assigned this Sep 10, 2026
@derekmisler
derekmisler marked this pull request as ready for review September 10, 2026 18:09
The previous commit replaced {pr} with $PR_NUMBER in posting-format.md,
making the sed substitution a no-op. At agent runtime PR_NUMBER is not
in the run-review step's env, so $PR_NUMBER expanded to empty, producing
pulls//reviews → 404, reproducing the original bug.

Option A: keep {pr} in posting-format.md as the placeholder; the sed
substitution in action.yml's 'Copy reference files' step bakes the
actual PR number in at render time, before the agent ever sees the file.
The {pr} validation guard now has teeth — it fires whenever the sed
substitution fails to replace the placeholder.

Also broaden the validation error message to cover all failure conditions
(unreplaced placeholder or missing SHA), not just the SHA check.

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🔴 CRITICAL

Comment thread review-pr/agents/refs/posting-format.md Outdated

@aheritier aheritier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

Root cause is correct and well evidenced — I reproduced it from the verbose-log artifact of docker/sandboxes run 34488043917: the {pr} URL returned gh: Not Found (HTTP 404), the retry piped through jq '{id, state}' so no pullrequestreview- token ever reached the verbose log, and the ⚠️ Review did not complete notice (review 5168391083) landed 5s after the real review (5168390196). gh api really does only expand {owner}, {repo}, {branch}. Rendering the number at staging time and logging html_url is the right fix, and CI is green at 1417968.

Four things worth addressing before merge.

[should-fix] PR_NUMBER is substituted into an agent-executed snippet without validationreview-pr/action.yml:876,884-885,888

The adjacent PR_HEAD_SHA is regex-guarded before substitution; PR_NUMBER is not, and the post-render guard only detects a surviving {pr}, never a bad substitution. Executing the step body with mocked env:

  • PR_NUMBER=""gh api repos/{owner}/{repo}/pulls//reviews, step exits 0 — a silently invalid URL, i.e. the exact failure mode this PR fixes. (Not reachable today: resolve-context hard-fails on an empty number — defense-in-depth only.)
  • PR_NUMBER='111 ; echo INJECTED'pulls/111 ; echo INJECTED/reviews, step exits 0 — arbitrary shell in a snippet the agent executes. pr-number is a public composite-action input.
  • PR_NUMBER='111/reviews' → sed aborts, exit 1 (fails closed, good).
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
  echo "::error::Resolved PR number is invalid; refusing to stage review posting"
  exit 1
fi

[should-fix] No test for the new substitution/guardsrc/resolve-trigger-context/__tests__/workflow-security.test.ts:1315-1333

runCopyReference() already runs this exact step body, and the it.each table covers the SHA guard exhaustively (valid/empty/non-hex/short/long/unresolved). The helper never sets PR_NUMBER and no {pr} case was added — which is why the empty-value hole above is invisible to CI. Please extend the table (empty / non-numeric / valid) alongside the numeric guard.

[should-fix] The added | jq masks gh api failurereview-pr/agents/refs/posting-format.md:121-122

A pipeline exits with the last command's status and jq exits 0 on empty input (false | jq '{id, state, html_url}' → exit 0). The wild evidence shows this already biting: the 404'd attempt printed {"id": null, "state": null} and the agent moved on. Extending that pipe keeps the silent-failure class alive — consider set -o pipefail before the pipeline, or asserting .id != null.

[should-fix] Same defect remains in the reply pathreview-pr/agents/pr-review-reply.yaml:76

gh api repos/{owner}/{repo}/pulls/{pr}/comments --input - has the identical root cause, and that file is not sed-rendered (only posting-format.md is), so every feedback reply still pays a guaranteed 404 + retry. Fine to defer, but worth a tracked follow-up — otherwise "eliminate the false double-posts" is only half done.

[optional] The PR description still describes commit 6c2d97c ("replace {pr} with $PR_NUMBER"), which 1417968 reverted — the final diff correctly keeps {pr} as the placeholder. Worth updating so the merged record isn't misleading.

[optional] The render guard covers only posting-format.md; other refs/*.md are staged verbatim, so a future {pr} elsewhere would ship unsubstituted. A grep -rq '{pr}' /tmp/refs/ guard would close that.

Four improvements from code review (aheritier):

1. Validate PR_NUMBER before substitution: add a numeric guard
   (`[[ $PR_NUMBER =~ ^[0-9]+$ ]]`) in the 'Copy reference files' step,
   mirroring the existing SHA guard. Prevents empty or injected values
   from reaching the sed substitution and the staged template.

2. Add `set -o pipefail` before the posting pipeline in posting-format.md
   so a `gh api` 404 propagates as a non-zero exit instead of being
   silently swallowed by the trailing `jq`.

3. Broaden the {pr} guard from `grep -q '{pr}' posting-format.md` to
   `grep -rq '{pr}' /tmp/refs/` so a future {pr} in any other staged
   ref file is also caught.

4. Extend the runCopyReference test table with three PR_NUMBER cases:
   empty, non-numeric, and shell metacharacters — all must exit 1.
   Pass PR_NUMBER through the test helper's env block so the existing
   SHA cases continue to pass with the default '5929'.
@derekmisler

Copy link
Copy Markdown
Collaborator Author

addressed all four should-fix items from the review in commit b513bd7:

[should-fix 1] PR_NUMBER numeric validation — added [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] guard before the sed substitution, mirroring the adjacent SHA guard. Empty, non-numeric, and shell-metacharacter values all exit 1.

[should-fix 2] Tests for the new substitution/guard — extended runCopyReference() to accept a prNumber parameter (default '5929') and added three new it.each rows: empty PR number, non-numeric, and PR number with shell metacharacters ('111; echo INJECTED'). All three correctly exit 1.

[should-fix 3] | jq masking gh api failure — added set -o pipefail before the posting pipeline in posting-format.md so a gh api 404 propagates as a non-zero exit instead of being swallowed by jq.

[should-fix 4] Same defect in reply path — filed issue #112 as a tracked follow-up. The reply path (pr-review-reply.yaml) is not sed-rendered, so fixing it requires a separate change. Keeping it out of this PR to keep the scope tight.

[optional] PR description — updated below to reflect the final diff (keeping {pr} as the placeholder).

[optional] Broader {pr} guard — changed grep -q '{pr}' /tmp/refs/posting-format.md to grep -rq '{pr}' /tmp/refs/ so any future {pr} in other staged ref files is also caught.

The reply agent template (pr-review-reply.yaml) used {pr} in the gh api
URL for posting inline replies. {pr} is not a gh CLI template variable,
so every reply attempt paid a guaranteed 404 before the agent retried
with an explicit URL.

Add a 'Stage reply agent' step to review-pr/reply/action.yml that renders
a copy of pr-review-reply.yaml to /tmp/pr-review-reply.yaml with {pr}
substituted for the actual PR number, then point the run-reply step at
the rendered copy. If pr-number is missing or non-numeric the step emits
a warning and copies the unrendered template (fail-open, matching the
existing reply path behaviour).

Pass pr-number from steps.feedback.outputs.pr-number at the workflow
call site in review-pr.yml.

Add a runStageReplyAgent test helper and four it.each cases (valid,
empty, non-numeric, shell metacharacters) to workflow-security.test.ts.

Closes #112 (consolidated into #111).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: substitute {pr} in pr-review-reply.yaml posting command

3 participants