Skip to content

workflow_dispatch: "Checkout PR branch" succeeds without checking out — guard honours aw_context, checkout_pr_branch.cjs ignores it #49417

Description

@emexelem

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

  1. A workflow with on: { workflow_dispatch: } (gh-aw adds the aw_context input itself).
  2. 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>}'
  1. Observe in the Checkout PR branch step:
No pull request context available, skipping checkout
  1. 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:

  1. 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.
  2. 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 }

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions