diff --git a/actions/setup/js/apply_samples.cjs b/actions/setup/js/apply_samples.cjs index 10d2a91b1ab..8481fc2e60b 100644 --- a/actions/setup/js/apply_samples.cjs +++ b/actions/setup/js/apply_samples.cjs @@ -248,16 +248,30 @@ async function derivePrHeadRef(entry) { if (ref) return ref; } - // 3. Explicit pull_request_number on the sample arguments. - const argNumber = Number(entry.arguments.pull_request_number); - if (Number.isFinite(argNumber) && argNumber > 0) { - const ref = await fetchPullRequestHeadRef({ owner, repo, pullNumber: argNumber }); + // 3. PR number from sample arguments, workflow_dispatch inputs, or config target. + const pullNumber = + toPositivePullRequestNumber(entry.arguments.pull_request_number) || + toPositivePullRequestNumber(payload?.inputs?.pull_request_number) || + toPositivePullRequestNumber(payload?.client_payload?.pull_request_number) || + readConfiguredTargetPullRequestNumber(entry.tool); + if (pullNumber) { + const ref = await fetchPullRequestHeadRef({ owner, repo, pullNumber }); if (ref) return ref; } return null; } +/** + * Convert unknown value to a positive pull request number, or null. + * @param {any} value + * @returns {number|null} + */ +function toPositivePullRequestNumber(value) { + const n = Number(value); + return Number.isFinite(n) && n > 0 ? n : null; +} + /** * Read the configured `target-repo` for a given safe-output tool from the * safe-outputs config file (GH_AW_SAFE_OUTPUTS_CONFIG_PATH). Returns an empty @@ -291,6 +305,44 @@ function readConfiguredTargetRepo(tool) { return ""; } +/** + * Read configured `target` for a safe-output tool and coerce it into a PR number. + * Supports plain numeric values and `${ENV_VAR}` placeholders. + * @param {string} tool + * @returns {number|null} + */ +function readConfiguredTargetPullRequestNumber(tool) { + const configPath = process.env.GH_AW_SAFE_OUTPUTS_CONFIG_PATH; + if (!configPath || !configPath.trim()) { + return null; + } + + const toolKey = typeof tool === "string" ? tool.replace(/-/g, "_") : ""; + + 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])) : {}; + const toolConfig = toolKey && config && typeof config === "object" ? config[toolKey] : null; + const target = toolConfig && typeof toolConfig === "object" ? toolConfig.target : null; + + if (typeof target === "number") { + return toPositivePullRequestNumber(target); + } + if (typeof target === "string") { + const trimmed = target.trim(); + const envMatch = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(trimmed); + if (envMatch) { + return toPositivePullRequestNumber(process.env[envMatch[1]]); + } + return toPositivePullRequestNumber(trimmed); + } + } catch (err) { + core.debug(`apply_samples: could not read target from ${configPath}: ${getErrorMessage(err)}`); + } + return null; +} + /** * Resolve the on-disk working directory in which a sample's patch should be * staged (branch created + patch committed). diff --git a/actions/setup/js/apply_samples.test.cjs b/actions/setup/js/apply_samples.test.cjs index 3391abdd40d..6e28f472c20 100644 --- a/actions/setup/js/apply_samples.test.cjs +++ b/actions/setup/js/apply_samples.test.cjs @@ -504,6 +504,51 @@ describe("apply_samples.cjs preStagePatch (create_pull_request / push_to_pull_re } }); + it("derives push_to_pull_request_branch branch from workflow_dispatch inputs.pull_request_number", async () => { + const workspace = makeTempDir("gh-aw-prestage-push-dispatch-input-"); + initRepo(workspace, "main"); + + const headRef = "feat/dispatch-pr-number"; + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ head: { ref: headRef } }), + }); + + const eventPath = path.join(workspace, "event.json"); + fs.writeFileSync( + eventPath, + JSON.stringify({ + inputs: { pull_request_number: "123" }, + }) + ); + + const prevBase = process.env.GH_AW_CUSTOM_BASE_BRANCH; + const prevEvent = process.env.GITHUB_EVENT_PATH; + const prevRepo = process.env.GITHUB_REPOSITORY; + process.env.GH_AW_CUSTOM_BASE_BRANCH = "main"; + process.env.GITHUB_EVENT_PATH = eventPath; + process.env.GITHUB_REPOSITORY = "owner/repo"; + try { + const entry = { + tool: "push_to_pull_request_branch", + arguments: { message: "Push update" }, + sidecars: { patch: newFileDiff("dispatch-input.txt", "via workflow_dispatch input\n") }, + }; + await preStagePatch(entry, 0, workspace); + expect(git(["rev-parse", "--abbrev-ref", "HEAD"], workspace).trim()).toBe(headRef); + expect(fetchSpy).toHaveBeenCalledWith(expect.stringContaining("/repos/owner/repo/pulls/123"), expect.anything()); + } finally { + fetchSpy.mockRestore(); + if (prevBase === undefined) delete process.env.GH_AW_CUSTOM_BASE_BRANCH; + else process.env.GH_AW_CUSTOM_BASE_BRANCH = prevBase; + 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; + } + }); + it("derives push_to_pull_request_branch branch from explicit arguments.pull_request_number when no event payload exists", async () => { // Resolution path 3: no GITHUB_EVENT_PATH, but the sample entry carries an // explicit `arguments.pull_request_number`. The driver should hit the PR