Skip to content

Handle workflow_dispatch PR targeting in deterministic safe-output sample replay - #48398

Merged
pelikhan merged 1 commit into
mainfrom
copilot/investigate-failure-in-gh-aw-test
Jul 27, 2026
Merged

Handle workflow_dispatch PR targeting in deterministic safe-output sample replay#48398
pelikhan merged 1 commit into
mainfrom
copilot/investigate-failure-in-gh-aw-test

Conversation

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The gh-aw-test run failed in Replay safe-outputs samples (deterministic) when replaying push_to_pull_request_branch samples from workflow_dispatch. In that path, replay could not derive the PR head branch if the sample omitted arguments.pull_request_number, even though the workflow/config already carried PR target information.

  • PR-branch derivation fallback expansion

    • Updated apply_samples.cjs branch derivation for push_to_pull_request_branch to resolve PR number from multiple sources, in order:
      1. arguments.pull_request_number
      2. event.inputs.pull_request_number (workflow_dispatch)
      3. event.client_payload.pull_request_number (repository_dispatch)
      4. safe-outputs config target (including ${ENV_VAR} placeholder resolution)
  • Config-target numeric resolution

    • Added parsing/coercion logic for safe-outputs target into a positive PR number.
    • Supports numeric literals and env-backed placeholders so replay can use the same target source already used by workflow config generation.
  • Regression coverage for dispatch path

    • Added a focused test case proving preStagePatch derives the PR head ref from workflow_dispatch input when sample arguments omit PR number.
const pullNumber =
  toPositivePullRequestNumber(entry.arguments.pull_request_number) ||
  toPositivePullRequestNumber(payload?.inputs?.pull_request_number) ||
  toPositivePullRequestNumber(payload?.client_payload?.pull_request_number) ||
  readConfiguredTargetPullRequestNumber(entry.tool);

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 9.88 AIC · ⌖ 7.74 AIC · ⊞ 7.1K ·
Comment /souschef to run again


pr-sous-chef run: https://github.com/github/gh-aw/actions/runs/30293408015

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 28.1 AIC · ⌖ 8.01 AIC · ⊞ 7.1K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 15.6 AIC · ⌖ 7.45 AIC · ⊞ 7.1K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 31.8 AIC · ⌖ 8.29 AIC · ⊞ 7.1K ·
Comment /souschef to run again


Run: https://github.com/github/gh-aw/actions/runs/30302221322

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13 AIC · ⌖ 6.16 AIC · ⊞ 7.1K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Fix deterministic samples replay for workflow_dispatch PR targets Handle workflow_dispatch PR targeting in deterministic safe-output sample replay Jul 27, 2026
Copilot AI requested a review from pelikhan July 27, 2026 15:46
@pelikhan
pelikhan marked this pull request as ready for review July 27, 2026 16:54
Copilot AI review requested due to automatic review settings July 27, 2026 16:54

Copilot AI 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.

Pull request overview

Expands deterministic safe-output replay to derive pull request branches from dispatch payloads and configured targets.

Changes:

  • Adds PR-number fallback resolution for dispatch events and config targets.
  • Adds workflow-dispatch regression coverage.
  • Env-backed config targets remain unresolved because replay does not forward their environment variables.
Show a summary per file
File Description
actions/setup/js/apply_samples.cjs Expands PR-number derivation and target parsing.
actions/setup/js/apply_samples.test.cjs Tests workflow-dispatch input derivation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment on lines +334 to +336
const envMatch = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(trimmed);
if (envMatch) {
return toPositivePullRequestNumber(process.env[envMatch[1]]);
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #48398 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions github-actions Bot 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.

The fallback chain for deriving pull_request_number is well-structured and correctly ordered. toPositivePullRequestNumber handles all edge cases (non-numeric, NaN, zero, negative). The config-target path gracefully returns null on missing env vars or parse errors rather than crashing replay. Test coverage for the workflow_dispatch path is focused and correctly validates the fetch URL. No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 13.8 AIC · ⌖ 5.58 AIC · ⊞ 5K

@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 70/100 — Acceptable

Analyzed 1 test: 1 design, 0 implementation, 0 violation(s).

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 0, JS: 1)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 0 (0%)
Duplicate clusters 0
Inflation No (0.8:1 test:prod)
🚨 Violations 0
Test File Classification Coverage
workflow_dispatch PR targeting apply_samples.test.cjs:507–550 behavioral_contract Happy path (event payload → branch)
✅ Quality Notes

Strengths:

  • High-value test: 100% of new tests are behavioral contracts (tests public API guarantees)
  • Real I/O: Uses actual subprocess git operations + API mocking at correct boundary
  • Targeted: Directly tests the bug fix (issue apply_samples.cjs ignores target-repo when fetching PR for push_to_pull_request_branch samples in siderepo workflows #41292 — workflow_dispatch PR targeting)
  • Proper cleanup: Environment variables restored in finally block
  • Security checks: Validates fetch call to exact PR endpoint (prevents cross-repo mistakes)
  • Fits pattern: Naturally extends existing event-payload test suite (pull_request, issue_comment, now workflow_dispatch)

Observations:

  • Happy-path only: Test covers success case; error scenarios (missing PR, fetch failure, etc.) untested
  • No edge cases: Does not validate boundary conditions or malformed inputs
  • Line ratio healthy: 45 test lines vs 56 production lines (0.8:1) — well-proportioned, not inflated

Context:
This PR adds one focused test to a mature 10-test suite covering the preStagePatch function. Related error-path tests already exist (issue #489: "fails fast when no PR context"; similar patterns for issue_comment). The new test validates a new event-source variant while leaning on existing adjacent tests for error coverage.

Verdict

Passed. 0% implementation tests (threshold: 30%). Test is a high-value behavioral contract with no violations.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 26.3 AIC · ⌖ 7.63 AIC · ⊞ 8.1K ·
Comment /review to run again

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 70/100. 0% implementation tests (threshold: 30%). High-value behavioral contract test with no violations.

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — COMMENT with three suggestions; no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Duplicated config-parsing logic: readConfiguredTargetPullRequestNumber and readConfiguredTargetRepo both inline the same Object.fromEntries key-normalisation block — extracting a shared helper would reduce maintenance risk.
  • Silent failure on bad env-var placeholder: when a ${ENV_VAR} target resolves to a non-integer, the function returns null without logging anything, making misconfiguration invisible in CI.
  • Test coverage gap: the new fallback chain (priority order + config-target path + invalid values) is not yet exercised — the single regression test only covers the workflow_dispatch happy path.

Positive Highlights

  • toPositivePullRequestNumber is a clean, well-named utility — reusing it consistently across all four sources is exactly the right call.
  • ✅ Fallback chain is clearly documented in the JSDoc and in the PR description.
  • ✅ The regression test is well-structured (Arrange / Act / Assert, explicit env-var cleanup in finally).
  • ✅ Root cause properly addressed: the fix expands the resolution strategy rather than patching a single missing branch.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 36.2 AIC · ⌖ 4.98 AIC · ⊞ 6.7K
Comment /matt to run again

try {
const raw = fs.readFileSync(configPath, "utf8");
const parsed = JSON.parse(raw);
const config = parsed && typeof parsed === "object" ? Object.fromEntries(Object.entries(parsed).map(([k, v]) => [String(k).replace(/-/g, "_"), v])) : {};

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.

[/codebase-design] Duplicated config-parsing logic — readConfiguredTargetPullRequestNumber repeats the same Object.fromEntries key-normalisation block already in readConfiguredTargetRepo. This dense one-liner is harder to maintain and test in isolation.

💡 Suggested refactor

Extract a shared helper:

function parseNormalisedConfig(configPath) {
  const raw = fs.readFileSync(configPath, "utf8");
  const parsed = JSON.parse(raw);
  if (!parsed || typeof parsed !== "object") return {};
  return Object.fromEntries(
    Object.entries(parsed).map(([k, v]) => [String(k).replace(/-/g, "_"), v])
  );
}

Both readers call parseNormalisedConfig(configPath) — cuts duplication and makes each function a single-purpose.

@copilot please address this.

const trimmed = target.trim();
const envMatch = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(trimmed);
if (envMatch) {
return toPositivePullRequestNumber(process.env[envMatch[1]]);

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] ${ENV_VAR} placeholders that expand to an empty string or non-numeric value silently return null and skip all remaining fallbacks — there is no warning logged. If a CI operator misconfigures the env var they will see a silent no-op with no indication of why the branch derivation failed.

💡 Suggestion

Add a core.debug or core.warning when the env var is set but doesn't resolve to a positive integer:

if (envMatch) {
  const resolved = toPositivePullRequestNumber(process.env[envMatch[1]]);
  if (!resolved) {
    core.debug(`apply_samples: env var ${envMatch[1]} did not resolve to a PR number (value: ${process.env[envMatch[1]]})`);
  }
  return resolved;
}

This makes misconfiguration visible in the Actions debug log without changing behaviour.

@copilot please address this.

if (prevEvent === undefined) delete process.env.GITHUB_EVENT_PATH;
else process.env.GITHUB_EVENT_PATH = prevEvent;
if (prevRepo === undefined) delete process.env.GITHUB_REPOSITORY;
else process.env.GITHUB_REPOSITORY = prevRepo;

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 test covers only the happy path (valid numeric string "123"). There are no tests for the new fallback priority order or edge cases in readConfiguredTargetPullRequestNumber.

💡 Missing coverage to consider
  1. Priority order: arguments.pull_request_number should win over inputs.pull_request_number — a test with both present would confirm this.
  2. Config-target path: A test where neither arguments nor event inputs carry a PR number, but the safe-outputs config target does (both numeric literal and ${ENV_VAR} form).
  3. Invalid / zero values: pull_request_number: "0", "abc" and "" should all return null and not cause an unhandled branch.

These are the exact edge cases where the original bug could silently regress.

@copilot please address this.

@github-actions github-actions Bot 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.

Changes needed before merge

Three issues require fixes; one additional concern is worth addressing.

Blocking issues
  1. toPositivePullRequestNumber accepts floats (line 272) — "1.5" or 1.9 passes the guard and reaches the GitHub API as an invalid PR number. Add Number.isInteger(n).

  2. client_payload.pull_request_number trusted without bounds (line 258) — client_payload is fully caller-controlled in repository_dispatch. Placing it at the same priority tier as sample arguments and workflow_dispatch inputs creates a potential misdirection path. Either document the trust assumption explicitly or deprioritize it below the config-target fallback.

  3. Config file re-read on every call (line 329) — fs.readFileSync + JSON.parse runs synchronously per invocation. Cache at module scope.

Non-blocking
  • Silent debug swallow of parse errors (line 347) — use core.warning when the file exists but fails to parse, so misconfigured setups are visible in the action log.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 · 32.8 AIC · ⌖ 5.02 AIC · ⊞ 5.7K
Comment /review to run again

*/
function toPositivePullRequestNumber(value) {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? n : null;

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.

toPositivePullRequestNumber accepts non-integer floats: Number("1.5") is 1.5, which passes the isFinite && > 0 guard and gets forwarded as a PR number — an invalid value that will cause the GitHub API call to fail.

💡 Suggested fix
function toPositivePullRequestNumber(value) {
  const n = Number(value);
  return Number.isFinite(n) && Number.isInteger(n) && n > 0 ? n : null;
}

Rejects "1.5", 1.9, etc. before they reach the GitHub API.

toPositivePullRequestNumber(payload?.client_payload?.pull_request_number) ||
readConfiguredTargetPullRequestNumber(entry.tool);
if (pullNumber) {
const ref = await fetchPullRequestHeadRef({ owner, repo, pullNumber });

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.

client_payload.pull_request_number is user-controlled and accepted without authorization checks: repository_dispatch events let any actor with write access (or a workflow that forwards external input) set client_payload freely. Accepting it here as a trusted PR number source can steer the replay to a different PR branch than intended.

💡 Details

payload?.client_payload?.pull_request_number is on the same priority level as the explicit sample arguments and workflow_dispatch inputs. Unlike workflow_dispatch.inputs, client_payload is entirely caller-controlled and there is no structural guarantee it comes from a trusted workflow step. If an attacker can send a repository_dispatch, they could redirect sample replay to an arbitrary PR head ref.

Consider documenting the trust boundary clearly, or move the client_payload fallback behind the config-target fallback so it is the last resort — or require an explicit opt-in flag before trusting it.

const toolConfig = toolKey && config && typeof config === "object" ? config[toolKey] : null;
const target = toolConfig && typeof toolConfig === "object" ? toolConfig.target : null;

if (typeof target === "number") {

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.

Config file is re-read and re-parsed on every call: fs.readFileSync + JSON.parse runs synchronously each time readConfiguredTargetPullRequestNumber is called. In a batch run over many entries this will hammer the filesystem for no reason.

💡 Suggested fix

Cache the parsed result at module scope, similar to readConfiguredTargetRepo:

let _configCache = undefined; // undefined = not yet loaded; null = failed/missing

function _loadConfig() {
  if (_configCache !== undefined) return _configCache;
  const configPath = process.env.GH_AW_SAFE_OUTPUTS_CONFIG_PATH;
  if (!configPath?.trim()) return (_configCache = null);
  try {
    const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
    _configCache = parsed && typeof parsed === "object" ? parsed : null;
  } catch {
    _configCache = null;
  }
  return _configCache;
}

Then readConfiguredTargetPullRequestNumber just calls _loadConfig() instead of re-reading the file.

}

/**
* Resolve the on-disk working directory in which a sample's patch should be

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.

Config parse errors are silently swallowed at debug level: a malformed or truncated GH_AW_SAFE_OUTPUTS_CONFIG_PATH file causes readConfiguredTargetPullRequestNumber to return null with only a core.debug log — indistinguishable at runtime from "config not set". Operators will have no idea their config is broken.

💡 Suggested fix

Emit at least core.warning (not core.debug) for JSON.parse failures so the problem surfaces in the action log without blocking execution:

} catch (err) {
  core.warning(`apply_samples: could not read target from ${configPath}: ${getErrorMessage(err)}`);
}

Keep debug for the "file not found" / env-var-not-set path, but use warning when the file exists and is unparseable.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the fix! This PR looks great and ready for review.

  • Clearly identified and fixed the bug — PR targeting now correctly falls back through multiple sources (arguments.pull_request_number, event.inputs.pull_request_number, event.client_payload.pull_request_number, config target), which handles workflow_dispatch dispatch events properly.
  • Added strong test coverage — The regression test in apply_samples.test.cjs validates that preStagePatch correctly derives PR head refs from workflow_dispatch inputs when sample arguments omit the PR number.
  • Provided excellent context — The PR body is clear, well-structured, and includes a concise code example.

This follows the project's agentic development process. 🤖

Generated by ✅ Contribution Check · sonnet46 · 63.8 AIC · ⌖ 7.55 AIC · ⊞ 6.2K ·

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category bug
Risk low
Score 72/100 (impact:30, urgency:25, quality:17)
Action auto_merge

Rationale: Fixes deterministic replay failure for workflow_dispatch+push_to_pull_request_branch. 2-file change, CI fully passing. Low risk, high precision fix.

Generated by 🔧 PR Triage Agent · sonnet46 · 54.1 AIC · ⌖ 8.18 AIC · ⊞ 5.7K ·

@pelikhan
pelikhan merged commit f427a5e into main Jul 27, 2026
59 of 61 checks passed
@pelikhan
pelikhan deleted the copilot/investigate-failure-in-gh-aw-test branch July 27, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants