Skip to content

create_pull_request safe-output never registers temporary_id#aw_prN references silently break #33436

Description

@corygehr

gh-aw version: v0.74.4
Affected component: actions/setup/js/create_pull_request.cjs + actions/setup/js/safe_output_handler_manager.cjs
Severity: Medium — silent failure, output rendered to end users contains literal placeholder text instead of resolved PR links

TL;DR

When a safe-output create_pull_request call sets temporary_id, the framework silently discards it. The PR is created successfully, but the temp ID is never written to temporary-id-map.json, never appears in safe-output-items.jsonl PR rows, and is never resolved when other safe-output bodies reference it as #aw_prN. The reference renders as literal text (e.g., #aw_pr1) in the issue/comment/discussion the agent posts.

Crucially this is not an agent-prompt mistake — it reproduces 100 % of the time even when the agent emits the call exactly as documented. create_issue works correctly, which masks the bug until someone tries to cross-reference a PR from a later safe-output.

Reproduction

A workflow that emits, in order:

  1. create_issue with temporary_id: "aw_issue1"
  2. create_pull_request with temporary_id: "aw_pr1" (e.g., a PR opened on a downstream repo)
  3. add_comment (or create_discussion, or another create_issue) whose body contains both #aw_issue1 and #aw_pr1

Expected: the comment body renders both #aw_issue1 → resolved issue reference and #aw_pr1 → resolved PR reference (qualified owner/repo#N for cross-repo cases, bare #N for same-repo).

Actual: #aw_issue1 resolves correctly. #aw_pr1 is left in the body as literal text — exactly as the agent emitted it.

Evidence from the workflow run that prompted this report

The safe-outputs job log contained:

Registered temporary ID: aw_issue1 -> <owner>/<repo>#<N>

…and no equivalent Registered temporary ID: line for any PR temp ID, even though the agent's safeoutputs.jsonl upload showed both create_pull_request rows with temporary_id: "aw_pr1" and temporary_id: "aw_pr2" set correctly on the calls.

temporary-id-map.json from the same run contained only the issue entry. The safe-output-items.jsonl PR rows looked like:

{"type":"create_pull_request","repo":"<owner>/<repo>","timestamp":"..."}

No number, no url, no temporaryId — even though the PRs themselves were created without error on the target repo.

Root cause

Two files disagree about the shape of the value create_pull_request returns to the orchestrator.

actions/setup/js/create_pull_request.cjs (≈ lines 2177-2184) returns:

return {
  success: true,
  pull_request_number,   // snake_case, prefixed
  pull_request_url,      // snake_case, prefixed
  branch_name,
  temporary_id,          // snake_case
  repo,
};

actions/setup/js/safe_output_handler_manager.cjs (≈ line 771) only registers temp IDs when:

if (result.temporaryId && result.repo && result.number) {
  registerTemporaryId(...);
}

The handler check uses camelCase field names (temporaryId, number) and the registrar expects an unprefixed number. None of those match the snake_case, pull_request_-prefixed names create_pull_request.cjs actually returns, so the if is always false for PRs and registration is skipped without any warning.

For contrast, actions/setup/js/create_issue.cjs does both of the following — either one alone would prevent the bug:

  1. It mutates the shared temporaryIdMap directly via the third handler argument (temporaryIdMap[temporaryId] = { ... }) at ≈ lines 1064-1065.
  2. Its return value at ≈ lines 1237-1241 uses the camelCase shape the manager check expects ({ temporaryId, repo, number }).

create_pull_request.cjs does neither.

Compounding the issue: the synthetic-update pass in safe_output_handler_manager.cjs (≈ line 1172) that retroactively patches #aw_* placeholders in already-emitted bodies only runs for create_issue, create_discussion, add_comment, and comment_memory. create_pull_request is not in the list, so PR bodies that contain forward #aw_* refs are never re-resolved either. (That is an orthogonal limitation, but worth fixing in the same PR.)

Suggested fixes

Either of these would resolve the registration bug; doing both would be ideal:

  1. Align the return shape with the registrar contract. Update create_pull_request.cjs to return:

    return {
      success: true,
      number: pull_request_number,
      url: pull_request_url,
      branch_name,
      temporaryId: temporary_id,
      repo,
    };

    (Keep pull_request_number / pull_request_url as aliases if any downstream consumer reads them, but make the canonical fields match what the manager expects.)

  2. Mutate the map directly, the same way create_issue.cjs does, so the registration path doesn't depend on the post-return shape match at all.

  3. Add create_pull_request to the synthetic-update list in safe_output_handler_manager.cjs so that bodies emitted before the PR call can resolve forward #aw_prN refs once the PR is created. (Today, even if (1)/(2) ship, a body posted before the PR call still won't resolve, because the synthetic-update pass excludes PRs.)

  4. Fail loudly when registration is skipped. When a handler returns a result that includes temporary_id / temporaryId but is missing the other fields needed for registration, log a warning rather than silently dropping the ID. This would have made the bug self-diagnosing.

Impact on downstream workflows

Any workflow that:

  • emits a create_pull_request, and
  • references that PR from a later safe-output body (e.g., posts a "Results delivered: see the linked PR #aw_pr1" summary comment on a discussion or issue)

…will render the placeholder as literal text, regardless of how the agent is prompted. The only workarounds today are:

  • Deterministic PR-search-by-branch URLs: https://github.com/<owner>/<repo>/pulls?q=is%3Apr+head%3A<branch> — requires the workflow to use predictable branch names and gives up the nicer auto-rendered PR card.
  • Embedding Closes #aw_issueN in the PR body so an issue created earlier in the same run sidebars the PR — only works when the workflow is happy for PR merge to auto-close the issue.
  • A post-step that re-edits the body — fragile, requires extra permissions, and the PR number isn't exposed in any cleanly machine-readable place from the safe_outputs job either.

All three are workarounds for what should be a one-line framework fix.

Why this took us a while to diagnose

The symptom looks identical to a prompt bug (agent forgot temporary_id), and our first two attempts at fixing it "downstream" — hardening the agent prompt to require temporary_id on every create_pull_request — appeared to land cleanly because nothing in gh-aw warned that the IDs were being dropped. It wasn't until we read both .cjs files side-by-side that the field-name mismatch became obvious. A single warning log on the silently-skipped registration would save the next team this trip.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions