Summary
The compiler emits a Checkout PR branch step whose if: explicitly names workflow_dispatch + aw_context, but the script that step runs — actions/setup/js/checkout_pr_branch.cjs — has no workflow_dispatch or aw_context handling at all. On a dispatch the step runs, logs, and succeeds without checking anything out, leaving the workspace on whatever ref the dispatch used.
The run is green, every job succeeds, and the agent silently reviews/operates on the wrong commit.
Sibling in shape (different mechanism, already closed): #47845 — "Cross-repo create_pull_request silently drops the PR (green run) when target-repo is a dynamic expression".
What the compiler emits
From a compiled .lock.yml (gh-aw v0.83.4, engine: copilot, workflow with on: { pull_request: …, workflow_dispatch: }):
- name: Checkout PR branch
id: checkout-pr
if: |
github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request'
uses: actions/github-script@…
with:
script: |
const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs');
await main();
The third disjunct is unambiguous: on workflow_dispatch, if aw_context.item_type == 'pull_request', check the PR out.
What the script does
actions/setup/js/checkout_pr_branch.cjs @ v0.83.4 (404 lines):
$ grep -c 'workflow_dispatch\|aw_context' checkout_pr_branch.cjs
0
179 async function main() {
180 const eventName = context.eventName;
…
184 let pullRequest = context.payload.pull_request;
185
186 // Handle issue_comment (and similar) events triggered on a PR
187 if (!pullRequest && context.payload.issue?.pull_request) {
…
195 if (!pullRequest) {
196 core.info("No pull request context available, skipping checkout");
workflow_dispatch has no context.payload.pull_request, so it falls straight to line 196.
Why it is silent
Line 196 is core.info, not core.setFailed. The step exits 0, the job stays green, and the only trace is one informational line in a step whose name asserts the opposite of what happened.
Because checkout_pr_branch.cjs is downloaded into ${runner.temp}/gh-aw/actions at runtime, it is not present in the .lock.yml either — so this cannot be caught by inspecting the compiled workflow. gh aw compile --strict reports 0 errors, 0 warnings.
Minimal reproduction
- A workflow with
on: { workflow_dispatch: } (gh-aw adds the aw_context input itself).
- Dispatch it with a ref that is not the PR's head:
$ gh workflow run my-workflow.lock.yml \
--ref some-other-branch \
-f aw_context='{"item_type":"pull_request","item_number":<PR>}'
- Observe in the
Checkout PR branch step:
No pull request context available, skipping checkout
- Step ✅, job ✅, run ✅ — and
git rev-parse HEAD in the workspace is some-other-branch, not the PR head.
Dispatching with --ref equal to the PR branch masks the bug completely: the tree is correct by coincidence. That is how it survived our first dispatch, which we recorded as a passing verification.
Observed
| Event |
--ref |
Step result |
Workspace HEAD |
pull_request |
n/a |
checked out |
PR head ✅ |
workflow_dispatch |
= PR head branch |
skipping checkout, ✅ |
PR head (by coincidence) |
workflow_dispatch |
≠ PR head branch |
skipping checkout, ✅ |
wrong commit ❌ |
Expected
Either of these would be fine; the current state is the one combination that is not:
- Honour the guard. In
main(), when context.eventName === 'workflow_dispatch', parse context.payload.inputs.aw_context and, if item_type === 'pull_request', use item_number to fetch and check out the PR ref — mirroring the existing issue?.pull_request branch just above it.
- Or fail loudly. If
workflow_dispatch checkout is deliberately unsupported, core.setFailed (or at minimum core.warning) when the if: matched but no PR context was found, and drop workflow_dispatch from the generated condition so the guard stops promising something the runtime does not deliver.
Option 1 seems closer to intent, given the compiler already goes to the trouble of parsing aw_context.item_type in the guard.
Workaround
Rather than adding our own checkout — which would land after the generated Restore agent config folders from base branch step and silently undo the protection that stops a PR from modifying the agent's own configuration — we compare git rev-parse HEAD against the PR head SHA and warn plus flag the run as untrusted when they differ. That also catches a pull_request run whose checkout failed for other reasons.
Environment
gh-aw v0.83.4, gh 2.96.0, ubuntu-latest
engine: { id: copilot }
Summary
The compiler emits a
Checkout PR branchstep whoseif:explicitly namesworkflow_dispatch+aw_context, but the script that step runs —actions/setup/js/checkout_pr_branch.cjs— has noworkflow_dispatchoraw_contexthandling at all. On a dispatch the step runs, logs, and succeeds without checking anything out, leaving the workspace on whatever ref the dispatch used.The run is green, every job succeeds, and the agent silently reviews/operates on the wrong commit.
Sibling in shape (different mechanism, already closed): #47845 — "Cross-repo create_pull_request silently drops the PR (green run) when target-repo is a dynamic expression".
What the compiler emits
From a compiled
.lock.yml(gh-aw v0.83.4,engine: copilot, workflow withon: { pull_request: …, workflow_dispatch: }):The third disjunct is unambiguous: on
workflow_dispatch, ifaw_context.item_type == 'pull_request', check the PR out.What the script does
actions/setup/js/checkout_pr_branch.cjs@v0.83.4(404 lines):workflow_dispatchhas nocontext.payload.pull_request, so it falls straight to line 196.Why it is silent
Line 196 is
core.info, notcore.setFailed. The step exits 0, the job stays green, and the only trace is one informational line in a step whose name asserts the opposite of what happened.Because
checkout_pr_branch.cjsis downloaded into${runner.temp}/gh-aw/actionsat runtime, it is not present in the.lock.ymleither — so this cannot be caught by inspecting the compiled workflow.gh aw compile --strictreports 0 errors, 0 warnings.Minimal reproduction
on: { workflow_dispatch: }(gh-aw adds theaw_contextinput itself).Checkout PR branchstep:git rev-parse HEADin the workspace issome-other-branch, not the PR head.Dispatching with
--refequal to the PR branch masks the bug completely: the tree is correct by coincidence. That is how it survived our first dispatch, which we recorded as a passing verification.Observed
--refpull_requestworkflow_dispatchskipping checkout, ✅workflow_dispatchskipping checkout, ✅Expected
Either of these would be fine; the current state is the one combination that is not:
main(), whencontext.eventName === 'workflow_dispatch', parsecontext.payload.inputs.aw_contextand, ifitem_type === 'pull_request', useitem_numberto fetch and check out the PR ref — mirroring the existingissue?.pull_requestbranch just above it.workflow_dispatchcheckout is deliberately unsupported,core.setFailed(or at minimumcore.warning) when theif:matched but no PR context was found, and dropworkflow_dispatchfrom the generated condition so the guard stops promising something the runtime does not deliver.Option 1 seems closer to intent, given the compiler already goes to the trouble of parsing
aw_context.item_typein the guard.Workaround
Rather than adding our own checkout — which would land after the generated
Restore agent config folders from base branchstep and silently undo the protection that stops a PR from modifying the agent's own configuration — we comparegit rev-parse HEADagainst the PR head SHA and warn plus flag the run as untrusted when they differ. That also catches apull_requestrun whose checkout failed for other reasons.Environment
gh-awv0.83.4,gh2.96.0,ubuntu-latestengine: { id: copilot }