From b70c5a82f36123d371212c733512c560ee4d77b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:55:36 +0000 Subject: [PATCH 1/2] Initial plan From 373f84986e349494f1349abb0462a27b6cdcc686 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:06:03 +0000 Subject: [PATCH 2/2] fix: enforce draft config as policy in create_pull_request, not as fallback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 10 +- actions/setup/js/create_pull_request.test.cjs | 125 ++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 0264adf97cd..5a45d2ff81f 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -609,8 +609,14 @@ async function main(config = {}) { .map(label => String(label).trim()) .filter(label => label); - // Use draft setting from message if provided, otherwise use config default - const draft = pullRequestItem.draft !== undefined ? pullRequestItem.draft : draftDefault; + // Configuration enforces draft as a policy, not a fallback (consistent with autoMerge/allowEmpty) + const draft = draftDefault; + if (pullRequestItem.draft !== undefined && pullRequestItem.draft !== draftDefault) { + core.warning( + `Agent requested draft: ${pullRequestItem.draft}, but configuration enforces draft: ${draftDefault}. ` + + `Configuration takes precedence for security. To change this, update safe-outputs.create-pull-request.draft in the workflow file.` + ); + } core.info(`Creating pull request with title: ${title}`); core.info(`Labels: ${JSON.stringify(labels)}`); diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index bba2c9df350..e6c08f517d5 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -7,6 +7,131 @@ import * as os from "os"; const require = createRequire(import.meta.url); +describe("create_pull_request - draft policy enforcement", () => { + let tempDir; + let originalEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + process.env.GH_AW_WORKFLOW_ID = "test-workflow"; + process.env.GITHUB_REPOSITORY = "test-owner/test-repo"; + process.env.GITHUB_BASE_REF = "main"; + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-draft-test-")); + + global.core = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), + startGroup: vi.fn(), + endGroup: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }, + }; + global.github = { + rest: { + pulls: { + create: vi.fn().mockResolvedValue({ data: { number: 1, html_url: "https://github.com/test" } }), + }, + repos: { + get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }), + }, + issues: { + addLabels: vi.fn().mockResolvedValue({}), + }, + }, + graphql: vi.fn(), + }; + global.context = { + eventName: "workflow_dispatch", + repo: { owner: "test-owner", repo: "test-repo" }, + payload: {}, + }; + global.exec = { + exec: vi.fn().mockResolvedValue(0), + getExecOutput: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }), + }; + + // Clear module cache so globals are picked up fresh + delete require.cache[require.resolve("./create_pull_request.cjs")]; + }); + + afterEach(() => { + for (const key of Object.keys(process.env)) { + if (!(key in originalEnv)) { + delete process.env[key]; + } + } + Object.assign(process.env, originalEnv); + + if (tempDir && fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + + delete global.core; + delete global.github; + delete global.context; + delete global.exec; + vi.clearAllMocks(); + }); + + /** Returns the `core.warning` calls related to draft config override attempts. */ + function getDraftOverrideWarnings() { + return global.core.warning.mock.calls.filter(args => String(args[0]).includes("Agent requested draft")); + } + + it("should enforce draft: false from config even when agent requests draft: true", async () => { + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ draft: "false", allow_empty: true }); + + const result = await handler({ title: "Test PR", body: "Test body", draft: true }, {}); + + expect(result.success).toBe(true); + expect(global.github.rest.pulls.create).toHaveBeenCalledWith(expect.objectContaining({ draft: false })); + }); + + it("should enforce draft: true from config even when agent requests draft: false", async () => { + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ draft: "true", allow_empty: true }); + + const result = await handler({ title: "Test PR", body: "Test body", draft: false }, {}); + + expect(result.success).toBe(true); + expect(global.github.rest.pulls.create).toHaveBeenCalledWith(expect.objectContaining({ draft: true })); + }); + + it("should log a warning when agent attempts to override draft config", async () => { + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ draft: "false", allow_empty: true }); + + await handler({ title: "Test PR", body: "Test body", draft: true }, {}); + + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("Agent requested draft: true, but configuration enforces draft: false")); + }); + + it("should not log a warning when agent draft matches config", async () => { + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ draft: "false", allow_empty: true }); + + await handler({ title: "Test PR", body: "Test body", draft: false }, {}); + + expect(getDraftOverrideWarnings()).toHaveLength(0); + }); + + it("should not log a warning when agent does not specify draft", async () => { + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ draft: "false", allow_empty: true }); + + await handler({ title: "Test PR", body: "Test body" }, {}); + + expect(getDraftOverrideWarnings()).toHaveLength(0); + }); +}); + describe("create_pull_request - fallback-as-issue configuration", () => { describe("configuration parsing", () => { it("should default fallback_as_issue to true when not specified", () => {