Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/agentic_commands.yml

Large diffs are not rendered by default.

1,830 changes: 1,830 additions & 0 deletions .github/workflows/smoke-checkout-pr-dispatch.lock.yml

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions .github/workflows/smoke-checkout-pr-dispatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
private: true
emoji: "🧪"
name: Smoke Checkout PR Dispatch
description: Integration test validating that workflow_dispatch events with aw_context.item_type == 'pull_request' correctly check out the PR branch
on:
slash_command:
name: smoke-checkout-pr-dispatch
strategy: centralized
events: [issues, issue_comment, pull_request, pull_request_comment]
workflow_dispatch:
pull_request:
types: [labeled]
names: ["smoke-checkout-pr-dispatch"]
status-comment: true
permissions:
contents: read
pull-requests: read
engine: copilot
strict: true
network:
allowed:
- defaults
imports:
- shared/otlp.md
tools:
bash:
- "git status"
- "git log *"
- "git branch *"
- "git remote *"
- "echo *"
safe-outputs:
allowed-domains: [default-safe-outputs]
add-comment:
hide-older-comments: true
max: 1
messages:
footer: "> 🧪 *checkout_pr_branch dispatch smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}"
run-started: "🧪 [{workflow_name}]({run_url}) is validating workflow_dispatch PR branch checkout..."
run-success: "✅ [{workflow_name}]({run_url}) successfully validated workflow_dispatch PR branch checkout."
run-failure: "[{workflow_name}]({run_url}) failed to validate workflow_dispatch PR branch checkout: {status}"
timeout-minutes: 10
features:
gh-aw-detection: false
---

# Smoke Test: workflow_dispatch + aw_context PR Branch Checkout

This workflow validates the `checkout_pr_branch.cjs` behaviour when triggered via `workflow_dispatch`
with an `aw_context` input whose `item_type` is `"pull_request"`. The setup step should detect the
context, fetch `refs/pull/<N>/head`, and check out the PR branch before the agent runs.

It also exercises the same checkout path when triggered directly by a pull-request event or
`slash_command`, ensuring both code paths are exercised.

## Context

- Event: `${{ github.event_name }}`
- `aw_context` input (if present): `${{ github.event.inputs.aw_context }}`

## Test Requirements

Run each check and mark as ✅ pass or ❌ fail:

1. **Git status**: Run `git status` and confirm the workspace is in a clean, initialised state.
2. **Branch check**: Run `git branch --show-current` and record the checked-out branch name.
3. **Remote check**: Run `git remote -v` and confirm a remote named `origin` is present.
4. **Log check**: Run `git log --oneline -3` and confirm at least one commit is visible.
5. **Not default branch**: When triggered via `workflow_dispatch` with a PR `aw_context`, or via
a PR event, the current branch **must not** be `main` — a different branch name confirms the
PR checkout ran successfully.

## Output

Add a comment summarising the checkout validation results:

- Event name and, if `${{ github.event_name }}` is `workflow_dispatch`, the `item_number` extracted
from `aw_context` (parse `${{ github.event.inputs.aw_context }}` to find it)
- Current branch name (from `git branch --show-current`)
- Last 3 commits (from `git log --oneline -3`)
- Whether the workspace is on a branch other than `main`
- Overall status: PASS or FAIL with a brief explanation
38 changes: 38 additions & 0 deletions actions/setup/js/checkout_pr_branch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
* - Also run in base repository context
* - Uses refs/pull/N/head to fetch PR branch
*
* 4. workflow_dispatch with aw_context:
* - When aw_context input contains item_type=="pull_request" and item_number,
* the PR number is extracted and the head is fetched via refs/pull/N/head
* - Mirrors the guard in the compiled workflow's if: condition
*
* NOTE: This handler operates within the PR context from the workflow event
* and does not support cross-repository operations or target-repo parameters.
* No allowlist validation (checkAllowedRepo/validateTargetRepo) is needed as
Expand Down Expand Up @@ -192,6 +197,39 @@ async function main() {
core.info(`Detected ${eventName} event on PR #${pullRequest.number}, will fetch PR ref`);
}

// Handle workflow_dispatch events with aw_context pointing to a PR
if (!pullRequest && eventName === "workflow_dispatch") {
const awContextStr = context.payload.inputs?.aw_context;
if (awContextStr) {

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.

[/diagnosing-bugs] awContext.item_number passes any truthy value — a string "abc" or float would silently proceed to git fetch refs/pull/abc/head and fail late with a confusing git error.

💡 Suggested fix
const prNumber = Number(awContext.item_number);
if (awContext.item_type === "pull_request" && Number.isInteger(prNumber) && prNumber > 0) {
  pullRequest = { number: prNumber, state: "open" };

This also ensures pullRequest.number is always a plain integer downstream, consistent with how the pull_request and issue_comment branches populate it.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the latest commit. The truthy check is replaced with proper integer validation:

const prNumber = Number(awContext.item_number);
if (awContext.item_type === "pull_request" && Number.isInteger(prNumber) && prNumber > 0) {
  pullRequest = { number: prNumber, state: "open" };

Three additional test cases were added covering item_number: "abc", item_number: 0, and item_number: 1.5 — all correctly skip checkout. 67/67 tests pass.

try {
const awContext = JSON.parse(awContextStr);
const prNumber = Number(awContext.item_number);
if (awContext.item_type === "pull_request" && Number.isInteger(prNumber) && prNumber > 0) {
if (awContext.repo) {
const currentRepo = `${context.repo.owner}/${context.repo.repo}`;
if (awContext.repo !== currentRepo) {
core.warning(`Cross-repository workflow_dispatch is not supported: aw_context.repo (${awContext.repo}) does not match current repository (${currentRepo}), skipping checkout`);
} else {
pullRequest = {
number: prNumber,
state: "open",
};
core.info(`Detected workflow_dispatch event for PR #${pullRequest.number} via aw_context, will fetch PR ref`);
}
} else {
pullRequest = {
number: prNumber,
state: "open",
};
core.info(`Detected workflow_dispatch event for PR #${pullRequest.number} via aw_context, will fetch PR ref`);
}
}
} catch (e) {
core.warning(`Failed to parse aw_context: ${getErrorMessage(e)}`);
}
}
}

if (!pullRequest) {
core.info("No pull request context available, skipping checkout");
core.setOutput("checkout_pr_success", "true");
Expand Down
140 changes: 140 additions & 0 deletions actions/setup/js/checkout_pr_branch.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,146 @@ If the pull request is still open, verify that:
});
});

describe("workflow_dispatch events with aw_context", () => {
beforeEach(() => {
mockContext.eventName = "workflow_dispatch";
mockContext.payload = {
repository: { fork: false },
inputs: {
aw_context: JSON.stringify({ item_type: "pull_request", item_number: 123 }),
},
};
});

it("should checkout PR using git fetch refs/pull when aw_context has item_type pull_request", async () => {
await runScript();

expect(mockCore.info).toHaveBeenCalledWith("Detected workflow_dispatch event for PR #123 via aw_context, will fetch PR ref");
expect(mockCore.info).toHaveBeenCalledWith("Event: workflow_dispatch");
expect(mockCore.info).toHaveBeenCalledWith("Pull Request #123");

// workflow_dispatch uses git fetch refs/pull + checkout (not the fast pull_request path)
expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", "origin", "+refs/pull/123/head:refs/remotes/origin/pr-head", "--depth=2"]);

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.

[/tdd] The happy-path test asserts that fetchPRDetails is called indirectly via the git fetch/checkout sequence, but there is no test asserting that fetchPRDetails is actually invoked with the correct PR number (123). If the implementation ever switches to a direct API call instead, this test would not catch the regression.

💡 Suggested additional assertion
expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith(
  expect.objectContaining({ pull_number: 123 })
);

(Adjust to whatever fetchPRDetails mock call is used elsewhere in the test file.) This pins the contract between the workflow_dispatch path and the shared checkout logic.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in the latest commit — the happy-path test now asserts the fetchPRDetails contract:

expect(mockGithub.rest.pulls.get).toHaveBeenCalledWith(
  expect.objectContaining({ pull_number: 123 })
);

This pins the contract between the workflow_dispatch path and the shared API-fetch/checkout logic. All 67 tests pass.

expect(mockExec.exec).toHaveBeenCalledWith("git", ["checkout", "-B", "feature-branch", "origin/pr-head"]);

// fetchPRDetails must be called with the correct PR number to resolve head ref / commit count
expect(mockGithub.rest.pulls.get).toHaveBeenCalledWith(expect.objectContaining({ pull_number: 123 }));

expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "true");
expect(mockCore.setFailed).not.toHaveBeenCalled();
});

it("should skip checkout when aw_context item_type is not pull_request", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({ item_type: "issue", item_number: 42 });

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
expect(mockCore.setFailed).not.toHaveBeenCalled();
});

it("should skip checkout when workflow_dispatch has no aw_context input", async () => {
mockContext.payload.inputs = {};

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should skip checkout when workflow_dispatch has no inputs at all", async () => {
mockContext.payload.inputs = undefined;

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should warn and skip checkout when aw_context is invalid JSON", async () => {
mockContext.payload.inputs.aw_context = "not-valid-json{";

await runScript();

expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to parse aw_context:"));
expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should skip checkout when aw_context pull_request has no item_number", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({ item_type: "pull_request" });

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should skip checkout when aw_context item_number is a non-numeric string", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({ item_type: "pull_request", item_number: "abc" });

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should skip checkout when aw_context item_number is zero", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({ item_type: "pull_request", item_number: 0 });

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should skip checkout when aw_context item_number is a non-integer float", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({ item_type: "pull_request", item_number: 1.5 });

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should checkout PR when aw_context repo matches current repository", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({
item_type: "pull_request",
item_number: 123,
repo: "test-owner/test-repo",
});

await runScript();

expect(mockCore.info).toHaveBeenCalledWith("Detected workflow_dispatch event for PR #123 via aw_context, will fetch PR ref");
expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", "origin", "+refs/pull/123/head:refs/remotes/origin/pr-head", "--depth=2"]);
expect(mockCore.warning).not.toHaveBeenCalled();
});

it("should warn and skip checkout when aw_context repo does not match current repository", async () => {
mockContext.payload.inputs.aw_context = JSON.stringify({
item_type: "pull_request",
item_number: 123,
repo: "other-owner/other-repo",
});

await runScript();

expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Cross-repository workflow_dispatch is not supported"));
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("other-owner/other-repo"));
expect(mockCore.info).toHaveBeenCalledWith("No pull request context available, skipping checkout");
expect(mockExec.exec).not.toHaveBeenCalled();
});

it("should set output to true on successful workflow_dispatch PR checkout", async () => {
await runScript();

expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "true");
expect(mockCore.setFailed).not.toHaveBeenCalled();
});
});

describe("different event types", () => {
it("should handle pull_request_target event", async () => {
mockContext.eventName = "pull_request_target";
Expand Down
10 changes: 5 additions & 5 deletions pkg/workflow/testdata/TestWasmGolden_AllEngines/copilot.golden
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "engine-copilot-test"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/workflow.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Generate agentic run info
Expand All @@ -71,8 +71,8 @@ jobs:
GH_AW_INFO_ENGINE_ID: "copilot"
GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'default' }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_AGENT_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AGENT_VERSION: "1.0.77"
GH_AW_INFO_WORKFLOW_NAME: "engine-copilot-test"
GH_AW_INFO_EXPERIMENTAL: "false"
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
Expand Down Expand Up @@ -367,7 +367,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "engine-copilot-test"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/workflow.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Checkout repository
Expand Down Expand Up @@ -714,7 +714,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "engine-copilot-test"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/workflow.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Check team membership for workflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "basic-copilot-test"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/basic-copilot.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Generate agentic run info
Expand All @@ -71,8 +71,8 @@ jobs:
GH_AW_INFO_ENGINE_ID: "copilot"
GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'default' }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_AGENT_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AGENT_VERSION: "1.0.77"
GH_AW_INFO_WORKFLOW_NAME: "basic-copilot-test"
GH_AW_INFO_EXPERIMENTAL: "false"
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
Expand Down Expand Up @@ -367,7 +367,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "basic-copilot-test"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/basic-copilot.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Checkout repository
Expand Down Expand Up @@ -714,7 +714,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "basic-copilot-test"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/basic-copilot.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Check team membership for workflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "Test Playwright CLI Mode"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/playwright-cli-mode.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Generate agentic run info
Expand All @@ -71,8 +71,8 @@ jobs:
GH_AW_INFO_ENGINE_ID: "copilot"
GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'default' }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_AGENT_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AGENT_VERSION: "1.0.77"
GH_AW_INFO_WORKFLOW_NAME: "Test Playwright CLI Mode"
GH_AW_INFO_EXPERIMENTAL: "false"
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
Expand Down Expand Up @@ -378,7 +378,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "Test Playwright CLI Mode"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/playwright-cli-mode.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Checkout repository
Expand Down Expand Up @@ -735,7 +735,7 @@ jobs:
env:
GH_AW_SETUP_WORKFLOW_NAME: "Test Playwright CLI Mode"
GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/playwright-cli-mode.lock.yml@${{ github.ref }}
GH_AW_INFO_VERSION: "1.0.75"
GH_AW_INFO_VERSION: "1.0.77"
GH_AW_INFO_AWF_VERSION: "vAWF_VERSION"
GH_AW_INFO_ENGINE_ID: "copilot"
- name: Check team membership for workflow
Expand Down
Loading
Loading