diff --git a/.github/aw/github-agentic-workflows.md b/.github/aw/github-agentic-workflows.md index 13ddf594f4..0b3df484a9 100644 --- a/.github/aw/github-agentic-workflows.md +++ b/.github/aw/github-agentic-workflows.md @@ -392,11 +392,11 @@ The YAML frontmatter supports these fields: target-repo: "owner/repo" # Optional: cross-repository ``` When using `safe-outputs.close-pull-request`, the main job does **not** need `pull-requests: write` permission since PR closing is handled by a separate job with appropriate permissions. - - `add-labels:` - Safe label addition to issues or PRs. Labels will be created if they don't already exist in the repository. + - `add-labels:` - Safe label addition to issues or PRs ```yaml safe-outputs: add-labels: - allowed: [bug, enhancement, documentation] # Optional: restrict to specific labels (will be created if missing) + allowed: [bug, enhancement, documentation] # Optional: restrict to specific labels max: 3 # Optional: maximum number of labels (default: 3) target: "*" # Optional: "triggering" (default), "*" (any issue/PR), or number target-repo: "owner/repo" # Optional: cross-repository diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index e72950a39d..2705dd88bb 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -2516,7 +2516,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2638,6 +2638,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2721,7 +2738,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4607,7 +4624,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/ai-triage-campaign.lock.yml b/.github/workflows/ai-triage-campaign.lock.yml index ec6366789b..e32b14f62c 100644 --- a/.github/workflows/ai-triage-campaign.lock.yml +++ b/.github/workflows/ai-triage-campaign.lock.yml @@ -1497,7 +1497,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1619,6 +1619,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1702,7 +1719,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3514,7 +3531,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/archie.lock.yml b/.github/workflows/archie.lock.yml index 4f212b96e3..6b8a8bba6b 100644 --- a/.github/workflows/archie.lock.yml +++ b/.github/workflows/archie.lock.yml @@ -2984,7 +2984,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3106,6 +3106,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3189,7 +3206,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5175,7 +5192,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index c5fa07b779..3ac781f82a 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -1568,7 +1568,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1690,6 +1690,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1773,7 +1790,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3672,7 +3689,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index 51f6b4a5ab..8becba42b6 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -2381,7 +2381,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2503,6 +2503,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2586,7 +2603,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5230,7 +5247,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml index 514f62e961..cdeee9fa66 100644 --- a/.github/workflows/blog-auditor.lock.yml +++ b/.github/workflows/blog-auditor.lock.yml @@ -1884,7 +1884,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2006,6 +2006,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2089,7 +2106,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4292,7 +4309,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index 9fbb27add4..a2ac04ec88 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -2869,7 +2869,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2991,6 +2991,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3074,7 +3091,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4965,7 +4982,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml index 73bca91683..eeef619e55 100644 --- a/.github/workflows/breaking-change-checker.lock.yml +++ b/.github/workflows/breaking-change-checker.lock.yml @@ -1615,7 +1615,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1737,6 +1737,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1820,7 +1837,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3756,7 +3773,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 5a4959109c..f58be02c72 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -2488,7 +2488,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2610,6 +2610,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2693,7 +2710,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4642,7 +4659,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 46d5894999..ac870cfe96 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -2297,7 +2297,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2419,6 +2419,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2502,7 +2519,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4451,7 +4468,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index 3249129f56..7c23eee172 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -1616,7 +1616,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1738,6 +1738,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1821,7 +1838,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3753,7 +3770,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index c657caa88d..a104a8fbf6 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -1885,7 +1885,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2007,6 +2007,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2090,7 +2107,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4241,7 +4258,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 484767e533..ffc27e3377 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -3284,7 +3284,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3406,6 +3406,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3489,7 +3506,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5707,7 +5724,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -7475,6 +7507,7 @@ jobs: GH_AW_PR_LABELS: "automation,cloclo" GH_AW_PR_DRAFT: "true" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} @@ -7661,52 +7694,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -7727,7 +7775,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -7743,6 +7791,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -7952,16 +8002,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/close-old-discussions.lock.yml b/.github/workflows/close-old-discussions.lock.yml index 767fbe46c1..7b769a9581 100644 --- a/.github/workflows/close-old-discussions.lock.yml +++ b/.github/workflows/close-old-discussions.lock.yml @@ -1778,7 +1778,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1900,6 +1900,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1983,7 +2000,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3847,7 +3864,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml index cddcb68573..accd99cb6b 100644 --- a/.github/workflows/commit-changes-analyzer.lock.yml +++ b/.github/workflows/commit-changes-analyzer.lock.yml @@ -1842,7 +1842,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1964,6 +1964,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2047,7 +2064,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4172,7 +4189,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index 7f6c9627e7..3c2bb442d6 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -2193,7 +2193,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2315,6 +2315,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2398,7 +2415,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4917,7 +4934,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml index b55237256e..865761467c 100644 --- a/.github/workflows/copilot-pr-merged-report.lock.yml +++ b/.github/workflows/copilot-pr-merged-report.lock.yml @@ -1757,7 +1757,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1879,6 +1879,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1962,7 +1979,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5194,7 +5211,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index 472be5f200..0133954602 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -2407,7 +2407,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2529,6 +2529,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2612,7 +2629,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5293,7 +5310,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index ffb7e2162b..06d566dc71 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -1908,7 +1908,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2030,6 +2030,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2113,7 +2130,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4316,7 +4333,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 28cffc85bf..0cbd3e2c13 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -2923,7 +2923,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3045,6 +3045,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3128,7 +3145,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -6327,7 +6344,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 74504db959..b22e2f43d3 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -3079,7 +3079,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3201,6 +3201,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3284,7 +3301,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5309,7 +5326,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-assign-issue-to-user.lock.yml b/.github/workflows/daily-assign-issue-to-user.lock.yml index 317f503503..216585344a 100644 --- a/.github/workflows/daily-assign-issue-to-user.lock.yml +++ b/.github/workflows/daily-assign-issue-to-user.lock.yml @@ -2061,7 +2061,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2183,6 +2183,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2266,7 +2283,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3951,7 +3968,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index 984935c6a2..3cd458274c 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -2425,7 +2425,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2547,6 +2547,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2630,7 +2647,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5368,7 +5385,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml index bb49b18d76..3dea0685bb 100644 --- a/.github/workflows/daily-copilot-token-report.lock.yml +++ b/.github/workflows/daily-copilot-token-report.lock.yml @@ -2475,7 +2475,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2597,6 +2597,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2680,7 +2697,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5458,7 +5475,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 31a86a7e9f..53292ad9f5 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -1737,7 +1737,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1859,6 +1859,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1942,7 +1959,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3964,7 +3981,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -5732,6 +5764,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Daily Documentation Updater" GH_AW_TRACKER_ID: "daily-doc-updater" @@ -5916,52 +5949,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -5982,7 +6030,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -5998,6 +6046,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6207,16 +6257,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/daily-fact.lock.yml b/.github/workflows/daily-fact.lock.yml index 0d35b3688c..9731fdf849 100644 --- a/.github/workflows/daily-fact.lock.yml +++ b/.github/workflows/daily-fact.lock.yml @@ -2069,7 +2069,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2191,6 +2191,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2274,7 +2291,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4042,7 +4059,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 2e6bc86898..3b4e573f08 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -1766,7 +1766,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1888,6 +1888,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1971,7 +1988,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3996,7 +4013,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index 58b44528e3..fc0ea34bea 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -2177,7 +2177,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2299,6 +2299,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2382,7 +2399,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4741,7 +4758,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index f96562dee7..1d535c4c97 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -2570,7 +2570,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2692,6 +2692,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2775,7 +2792,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5580,7 +5597,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index a8817234f6..062da39e1b 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -1747,7 +1747,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1869,6 +1869,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1952,7 +1969,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3987,7 +4004,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index ae81bc3785..9362ee725f 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -1699,7 +1699,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1821,6 +1821,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1904,7 +1921,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3875,7 +3892,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index fd28bde900..035420bf2f 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -2494,7 +2494,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2616,6 +2616,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2699,7 +2716,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5217,7 +5234,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index 995f82d8c2..0732053a69 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -2332,7 +2332,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2454,6 +2454,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2537,7 +2554,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -6811,7 +6828,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml index a3ed1547e9..ab9ebb0f60 100644 --- a/.github/workflows/daily-repo-chronicle.lock.yml +++ b/.github/workflows/daily-repo-chronicle.lock.yml @@ -2179,7 +2179,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2301,6 +2301,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2384,7 +2401,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4891,7 +4908,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml index 52b49b7f0b..4788ba4487 100644 --- a/.github/workflows/daily-team-status.lock.yml +++ b/.github/workflows/daily-team-status.lock.yml @@ -1543,7 +1543,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1665,6 +1665,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1748,7 +1765,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3515,7 +3532,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index 3c799501bf..0a559becac 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -1608,7 +1608,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1730,6 +1730,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1813,7 +1830,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3679,7 +3696,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6166,6 +6198,7 @@ jobs: GH_AW_PR_LABELS: "dependencies,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Daily Workflow Updater" GH_AW_TRACKER_ID: "daily-workflow-updater" @@ -6350,52 +6383,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6416,7 +6464,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6432,6 +6480,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6641,16 +6691,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 68f384af56..497e2d91b4 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -2051,7 +2051,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2173,6 +2173,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2256,7 +2273,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4458,7 +4475,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index 01b80ccdd8..605e485fcf 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -1919,7 +1919,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2041,6 +2041,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2124,7 +2141,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4286,7 +4303,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index 9a67b19ede..afbf8c6f17 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -2147,7 +2147,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2269,6 +2269,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2352,7 +2369,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4233,7 +4250,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 4a93dd40cf..dc2b09f75d 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -18,14 +18,14 @@ # gh aw compile # For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md # -# Create a poem about GitHub and save it to repo-memory +# Create an empty pull request for agent to push changes to # # Original Frontmatter: # ```yaml # on: # workflow_dispatch: # name: Dev -# description: Create a poem about GitHub and save it to repo-memory +# description: Create an empty pull request for agent to push changes to # timeout-minutes: 5 # strict: false # engine: copilot @@ -39,7 +39,8 @@ # imports: # - shared/gh.md # safe-outputs: -# create-issue: +# create-pull-request: +# allow-empty: true # staged: true # steps: # - name: Download issues data @@ -59,16 +60,17 @@ # activation["activation"] # agent["agent"] # conclusion["conclusion"] -# create_issue["create_issue"] +# create_pull_request["create_pull_request"] # detection["detection"] # activation --> agent # activation --> conclusion +# activation --> create_pull_request # agent --> conclusion -# agent --> create_issue +# agent --> create_pull_request # agent --> detection -# create_issue --> conclusion +# create_pull_request --> conclusion # detection --> conclusion -# detection --> create_issue +# detection --> create_pull_request # ``` # # Original Prompt: @@ -90,8 +92,11 @@ # # # -# Read the last pull request using `githubissues-gh` tool -# and create an issue with the summary. +# Create an empty pull request that prepares a branch for future changes. +# The pull request should have: +# - Title: "Feature: Prepare branch for agent updates" +# - Body: "This is an empty pull request created to prepare a feature branch that an agent can push changes to later." +# - Branch name: "feature/agent-updates" # ``` # # Pinned GitHub Actions: @@ -339,39 +344,32 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /tmp/gh-aw/safeoutputs/config.json << 'EOF' - {"create_issue":{"max":1},"missing_tool":{"max":0},"noop":{"max":1}} + {"create_pull_request":{"allow_empty":true},"missing_tool":{"max":0},"noop":{"max":1}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' [ { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created.", + "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created.", "inputSchema": { "additionalProperties": false, "properties": { "body": { - "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", + "type": "string" + }, + "branch": { + "description": "Source branch name containing the changes. If omitted, uses the current working branch.", "type": "string" }, "labels": { - "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", "items": { "type": "string" }, "type": "array" }, - "parent": { - "description": "Parent issue number for creating sub-issues. Can be a real issue number (e.g., 42) or a temporary_id (e.g., 'aw_abc123def456') from a previously created issue in the same workflow run.", - "type": [ - "number", - "string" - ] - }, - "temporary_id": { - "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", - "type": "string" - }, "title": { - "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", "type": "string" } }, @@ -381,7 +379,7 @@ jobs: ], "type": "object" }, - "name": "create_issue" + "name": "create_pull_request" }, { "description": "Report that a tool or capability needed to complete the task is not available. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", @@ -430,7 +428,7 @@ jobs: EOF cat > /tmp/gh-aw/safeoutputs/validation.json << 'EOF' { - "create_issue": { + "create_pull_request": { "defaultMax": 1, "fields": { "body": { @@ -439,22 +437,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, "labels": { "type": "array", "itemType": "string", "itemSanitize": true, "itemMaxLength": 128 }, - "parent": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "temporary_id": { - "type": "string" - }, "title": { "required": true, "type": "string", @@ -1461,7 +1455,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1583,6 +1577,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1666,7 +1677,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3292,8 +3303,11 @@ jobs: - Read the last pull request using `githubissues-gh` tool - and create an issue with the summary. + Create an empty pull request that prepares a branch for future changes. + The pull request should have: + - Title: "Feature: Prepare branch for agent updates" + - Body: "This is an empty pull request created to prepare a feature branch that an agent can push changes to later." + - Branch name: "feature/agent-updates" PROMPT_EOF - name: Append XPIA security instructions to prompt @@ -3357,7 +3371,7 @@ jobs: To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - **Available tools**: create_issue, missing_tool, noop + **Available tools**: create_pull_request, missing_tool, noop **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. @@ -4463,7 +4477,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6370,12 +6399,19 @@ jobs: if (typeof module === "undefined" || require.main === module) { main(); } + - name: Upload git patch + if: always() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + with: + name: aw.patch + path: /tmp/gh-aw/aw.patch + if-no-files-found: ignore conclusion: needs: - activation - agent - - create_issue + - create_pull_request - detection if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim @@ -6625,8 +6661,8 @@ jobs: GH_AW_WORKFLOW_NAME: "Dev" GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} - GH_AW_SAFE_OUTPUT_JOBS: "{\"create_issue\":\"issue_url\"}" - GH_AW_OUTPUT_CREATE_ISSUE_ISSUE_URL: ${{ needs.create_issue.outputs.issue_url }} + GH_AW_SAFE_OUTPUT_JOBS: "{\"create_pull_request\":\"pull_request_url\"}" + GH_AW_OUTPUT_CREATE_PULL_REQUEST_PULL_REQUEST_URL: ${{ needs.create_pull_request.outputs.pull_request_url }} with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -6881,23 +6917,50 @@ jobs: core.setFailed(error instanceof Error ? error.message : String(error)); }); - create_issue: + create_pull_request: needs: + - activation - agent - detection if: > - (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_issue'))) && + (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))) && (needs.detection.outputs.success == 'true') runs-on: ubuntu-slim permissions: - contents: read + contents: write issues: write + pull-requests: write timeout-minutes: 10 outputs: - issue_number: ${{ steps.create_issue.outputs.issue_number }} - issue_url: ${{ steps.create_issue.outputs.issue_url }} - temporary_id_map: ${{ steps.create_issue.outputs.temporary_id_map }} + branch_name: ${{ steps.create_pull_request.outputs.branch_name }} + fallback_used: ${{ steps.create_pull_request.outputs.fallback_used }} + issue_number: ${{ steps.create_pull_request.outputs.issue_number }} + issue_url: ${{ steps.create_pull_request.outputs.issue_url }} + pull_request_number: ${{ steps.create_pull_request.outputs.pull_request_number }} + pull_request_url: ${{ steps.create_pull_request.outputs.pull_request_url }} steps: + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: aw.patch + path: /tmp/gh-aw/ + - name: Checkout repository + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + fetch-depth: 0 + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 @@ -6909,138 +6972,130 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs/ find "/tmp/gh-aw/safeoutputs/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Create Output Issue - id: create_issue + - name: Create Pull Request + id: create_pull_request uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_ID: "agent" + GH_AW_BASE_BRANCH: ${{ github.ref_name }} + GH_AW_PR_DRAFT: "true" + GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "true" + GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Dev" GH_AW_ENGINE_ID: "copilot" GH_AW_SAFE_OUTPUTS_STAGED: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - function sanitizeLabelContent(content) { - if (!content || typeof content !== "string") { - return ""; - } - let sanitized = content.trim(); - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); - sanitized = sanitized.replace( - /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\`` - ); - sanitized = sanitized.replace(/[<>&'"]/g, ""); - return sanitized.trim(); - } const fs = require("fs"); const crypto = require("crypto"); - const MAX_LOG_CONTENT_LENGTH = 10000; - function truncateForLogging(content) { - if (content.length <= MAX_LOG_CONTENT_LENGTH) { - return content; - } - return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`; - } - function loadAgentOutput() { - const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; - if (!agentOutputFile) { - core.info("No GH_AW_AGENT_OUTPUT environment variable found"); - return { success: false }; - } - let outputContent; - try { - outputContent = fs.readFileSync(agentOutputFile, "utf8"); - } catch (error) { - const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; - core.error(errorMessage); - return { success: false, error: errorMessage }; - } - if (outputContent.trim() === "") { - core.info("Agent output content is empty"); - return { success: false }; - } - core.info(`Agent output content length: ${outputContent.length}`); - let validatedOutput; - try { - validatedOutput = JSON.parse(outputContent); - } catch (error) { - const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; - core.error(errorMessage); - core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`); - return { success: false, error: errorMessage }; - } - if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { - core.info("No valid items found in agent output"); - core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`); - return { success: false }; + async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") { + const itemLabel = itemType === "issue" ? "issue" : "pull request"; + const linkMessage = + itemType === "issue" + ? `\n\n✅ Issue created: [#${itemNumber}](${itemUrl})` + : `\n\n✅ Pull request created: [#${itemNumber}](${itemUrl})`; + await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel); + } + async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) { + const shortSha = commitSha.substring(0, 7); + const message = `\n\n✅ Commit pushed: [\`${shortSha}\`](${commitUrl})`; + await updateActivationCommentWithMessage(github, context, core, message, "commit"); + } + async function updateActivationCommentWithMessage(github, context, core, message, label = "") { + const commentId = process.env.GH_AW_COMMENT_ID; + const commentRepo = process.env.GH_AW_COMMENT_REPO; + if (!commentId) { + core.info("No activation comment to update (GH_AW_COMMENT_ID not set)"); + return; } - return { success: true, items: validatedOutput.items }; - } - async function generateStagedPreview(options) { - const { title, description, items, renderItem } = options; - let summaryContent = `## 🎭 Staged Mode: ${title} Preview\n\n`; - summaryContent += `${description}\n\n`; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - summaryContent += renderItem(item, i); - summaryContent += "---\n\n"; + core.info(`Updating activation comment ${commentId}`); + let repoOwner = context.repo.owner; + let repoName = context.repo.repo; + if (commentRepo) { + const parts = commentRepo.split("/"); + if (parts.length === 2) { + repoOwner = parts[0]; + repoName = parts[1]; + } else { + core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`); + } } + core.info(`Updating comment in ${repoOwner}/${repoName}`); + const isDiscussionComment = commentId.startsWith("DC_"); try { - await core.summary.addRaw(summaryContent).write(); - core.info(summaryContent); - core.info(`📝 ${title} preview written to step summary`); + if (isDiscussionComment) { + const currentComment = await github.graphql( + ` + query($commentId: ID!) { + node(id: $commentId) { + ... on DiscussionComment { + body + } + } + }`, + { commentId: commentId } + ); + if (!currentComment?.node?.body) { + core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible"); + return; + } + const currentBody = currentComment.node.body; + const updatedBody = currentBody + message; + const result = await github.graphql( + ` + mutation($commentId: ID!, $body: String!) { + updateDiscussionComment(input: { commentId: $commentId, body: $body }) { + comment { + id + url + } + } + }`, + { commentId: commentId, body: updatedBody } + ); + const comment = result.updateDiscussionComment.comment; + const successMessage = label + ? `Successfully updated discussion comment with ${label} link` + : "Successfully updated discussion comment"; + core.info(successMessage); + core.info(`Comment ID: ${comment.id}`); + core.info(`Comment URL: ${comment.url}`); + } else { + const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + headers: { + Accept: "application/vnd.github+json", + }, + }); + if (!currentComment?.data?.body) { + core.warning("Unable to fetch current comment body, comment may have been deleted"); + return; + } + const currentBody = currentComment.data.body; + const updatedBody = currentBody + message; + const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { + owner: repoOwner, + repo: repoName, + comment_id: parseInt(commentId, 10), + body: updatedBody, + headers: { + Accept: "application/vnd.github+json", + }, + }); + const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment"; + core.info(successMessage); + core.info(`Comment ID: ${response.data.id}`); + core.info(`Comment URL: ${response.data.html_url}`); + } } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); + core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`); } } - function generateXMLMarker(workflowName, runUrl) { - const engineId = process.env.GH_AW_ENGINE_ID || ""; - const engineVersion = process.env.GH_AW_ENGINE_VERSION || ""; - const engineModel = process.env.GH_AW_ENGINE_MODEL || ""; - const trackerId = process.env.GH_AW_TRACKER_ID || ""; - const parts = []; - parts.push(`agentic-workflow: ${workflowName}`); - if (trackerId) { - parts.push(`tracker-id: ${trackerId}`); - } - if (engineId) { - parts.push(`engine: ${engineId}`); - } - if (engineVersion) { - parts.push(`version: ${engineVersion}`); - } - if (engineModel) { - parts.push(`model: ${engineModel}`); - } - parts.push(`run: ${runUrl}`); - return ``; - } - function generateFooter( - workflowName, - runUrl, - workflowSource, - workflowSourceURL, - triggeringIssueNumber, - triggeringPRNumber, - triggeringDiscussionNumber - ) { - let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`; - if (triggeringIssueNumber) { - footer += ` for #${triggeringIssueNumber}`; - } else if (triggeringPRNumber) { - footer += ` for #${triggeringPRNumber}`; - } else if (triggeringDiscussionNumber) { - footer += ` for discussion #${triggeringDiscussionNumber}`; - } - if (workflowSource && workflowSourceURL) { - footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`; - } - footer += "\n\n" + generateXMLMarker(workflowName, runUrl); - footer += "\n"; - return footer; - } function getTrackerID(format) { const trackerID = process.env.GH_AW_TRACKER_ID || ""; if (trackerID) { @@ -7049,130 +7104,6 @@ jobs: } return ""; } - const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi; - function generateTemporaryId() { - return "aw_" + crypto.randomBytes(6).toString("hex"); - } - function isTemporaryId(value) { - if (typeof value === "string") { - return /^aw_[0-9a-f]{12}$/i.test(value); - } - return false; - } - function normalizeTemporaryId(tempId) { - return String(tempId).toLowerCase(); - } - function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) { - return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { - const resolved = tempIdMap.get(normalizeTemporaryId(tempId)); - if (resolved !== undefined) { - if (currentRepo && resolved.repo === currentRepo) { - return `#${resolved.number}`; - } - return `${resolved.repo}#${resolved.number}`; - } - return match; - }); - } - function replaceTemporaryIdReferencesLegacy(text, tempIdMap) { - return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { - const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId)); - if (issueNumber !== undefined) { - return `#${issueNumber}`; - } - return match; - }); - } - function loadTemporaryIdMap() { - const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP; - if (!mapJson || mapJson === "{}") { - return new Map(); - } - try { - const mapObject = JSON.parse(mapJson); - const result = new Map(); - for (const [key, value] of Object.entries(mapObject)) { - const normalizedKey = normalizeTemporaryId(key); - if (typeof value === "number") { - const contextRepo = `${context.repo.owner}/${context.repo.repo}`; - result.set(normalizedKey, { repo: contextRepo, number: value }); - } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) { - result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) }); - } - } - return result; - } catch (error) { - if (typeof core !== "undefined") { - core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`); - } - return new Map(); - } - } - function resolveIssueNumber(value, temporaryIdMap) { - if (value === undefined || value === null) { - return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" }; - } - const valueStr = String(value); - if (isTemporaryId(valueStr)) { - const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr)); - if (resolvedPair !== undefined) { - return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null }; - } - return { - resolved: null, - wasTemporaryId: true, - errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`, - }; - } - const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10); - if (isNaN(issueNumber) || issueNumber <= 0) { - return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` }; - } - const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : ""; - return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null }; - } - function serializeTemporaryIdMap(tempIdMap) { - const obj = Object.fromEntries(tempIdMap); - return JSON.stringify(obj); - } - function parseAllowedRepos() { - const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS; - const set = new Set(); - if (allowedReposEnv) { - allowedReposEnv - .split(",") - .map(repo => repo.trim()) - .filter(repo => repo) - .forEach(repo => set.add(repo)); - } - return set; - } - function getDefaultTargetRepo() { - const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG; - if (targetRepoSlug) { - return targetRepoSlug; - } - return `${context.repo.owner}/${context.repo.repo}`; - } - function validateRepo(repo, defaultRepo, allowedRepos) { - if (repo === defaultRepo) { - return { valid: true, error: null }; - } - if (allowedRepos.has(repo)) { - return { valid: true, error: null }; - } - return { - valid: false, - error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`, - }; - } - function parseRepoSlug(repoSlug) { - const parts = repoSlug.split("/"); - if (parts.length !== 2 || !parts[0] || !parts[1]) { - return null; - } - return { owner: parts[0], repo: parts[1] }; - } function addExpirationComment(bodyLines, envVarName, entityType) { const expiresEnv = process.env[envVarName]; if (expiresEnv) { @@ -7186,305 +7117,488 @@ jobs: } } } + function generatePatchPreview(patchContent) { + if (!patchContent || !patchContent.trim()) { + return ""; + } + const lines = patchContent.split("\n"); + const maxLines = 500; + const maxChars = 2000; + let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n"); + const lineTruncated = lines.length > maxLines; + const charTruncated = preview.length > maxChars; + if (charTruncated) { + preview = preview.slice(0, maxChars); + } + const truncated = lineTruncated || charTruncated; + const summary = truncated + ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)` + : `Show patch (${lines.length} lines)`; + return `\n\n
${summary}\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n
`; + } async function main() { + core.setOutput("pull_request_number", ""); + core.setOutput("pull_request_url", ""); core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); - core.setOutput("temporary_id_map", "{}"); - core.setOutput("issues_to_assign_copilot", ""); + core.setOutput("branch_name", ""); + core.setOutput("fallback_used", ""); const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; - const result = loadAgentOutput(); - if (!result.success) { + const workflowId = process.env.GH_AW_WORKFLOW_ID; + if (!workflowId) { + throw new Error("GH_AW_WORKFLOW_ID environment variable is required"); + } + const baseBranch = process.env.GH_AW_BASE_BRANCH; + if (!baseBranch) { + throw new Error("GH_AW_BASE_BRANCH environment variable is required"); + } + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT || ""; + let outputContent = ""; + if (agentOutputFile.trim() !== "") { + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + core.setFailed(`Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`); + return; + } + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + } + const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; + if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); + return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } + } + } + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } + if (patchContent.includes("Failed to generate patch")) { + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); + return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } + } + } + if (!isEmpty) { + const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); + const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); + const patchSizeKb = Math.ceil(patchSizeBytes / 1024); + core.info(`Patch size: ${patchSizeKb} KB (maximum allowed: ${maxSizeKb} KB)`); + if (patchSizeKb > maxSizeKb) { + const message = `Patch size (${patchSizeKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ❌ Patch size exceeded\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch size error)"); + return; + } + throw new Error(message); + } + core.info("Patch size validation passed"); + } + if (isEmpty && !isStaged && !allowEmpty) { + const message = "Patch file is empty - no changes to apply (noop operation)"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to push - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } + } + core.info(`Agent output content length: ${outputContent.length}`); + if (!isEmpty) { + core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); + } else { + core.info("Patch file is empty - processing noop operation"); + } + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + core.setFailed(`Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`); return; } - const createIssueItems = result.items.filter(item => item.type === "create_issue"); - if (createIssueItems.length === 0) { - core.info("No create-issue items found in agent output"); + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.warning("No valid items found in agent output"); return; } - core.info(`Found ${createIssueItems.length} create-issue item(s)`); - const allowedRepos = parseAllowedRepos(); - const defaultTargetRepo = getDefaultTargetRepo(); - core.info(`Default target repo: ${defaultTargetRepo}`); - if (allowedRepos.size > 0) { - core.info(`Allowed repos: ${Array.from(allowedRepos).join(", ")}`); + const pullRequestItem = validatedOutput.items.find( item => item.type === "create_pull_request"); + if (!pullRequestItem) { + core.warning("No create-pull-request item found in agent output"); + return; } + core.info(`Found create-pull-request item: title="${pullRequestItem.title}", bodyLength=${pullRequestItem.body.length}`); if (isStaged) { - await generateStagedPreview({ - title: "Create Issues", - description: "The following issues would be created if staged mode was disabled:", - items: createIssueItems, - renderItem: (item, index) => { - let content = `### Issue ${index + 1}\n`; - content += `**Title:** ${item.title || "No title provided"}\n\n`; - if (item.temporary_id) { - content += `**Temporary ID:** ${item.temporary_id}\n\n`; - } - if (item.repo) { - content += `**Repository:** ${item.repo}\n\n`; - } - if (item.body) { - content += `**Body:**\n${item.body}\n\n`; - } - if (item.labels && item.labels.length > 0) { - content += `**Labels:** ${item.labels.join(", ")}\n\n`; - } - if (item.parent) { - content += `**Parent:** ${item.parent}\n\n`; - } - return content; - }, - }); + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Title:** ${pullRequestItem.title || "No title provided"}\n\n`; + summaryContent += `**Branch:** ${pullRequestItem.branch || "auto-generated"}\n\n`; + summaryContent += `**Base:** ${baseBranch}\n\n`; + if (pullRequestItem.body) { + summaryContent += `**Body:**\n${pullRequestItem.body}\n\n`; + } + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchStats = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + if (patchStats.trim()) { + summaryContent += `**Changes:** Patch file exists with ${patchStats.split("\n").length} lines\n\n`; + summaryContent += `
Show patch preview\n\n\`\`\`diff\n${patchStats.slice(0, 2000)}${patchStats.length > 2000 ? "\n... (truncated)" : ""}\n\`\`\`\n\n
\n\n`; + } else { + summaryContent += `**Changes:** No changes (empty patch)\n\n`; + } + } + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary"); return; } - const parentIssueNumber = context.payload?.issue?.number; - const temporaryIdMap = new Map(); - const triggeringIssueNumber = - context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; - const triggeringPRNumber = - context.payload?.pull_request?.number || (context.payload?.issue?.pull_request ? context.payload.issue.number : undefined); - const triggeringDiscussionNumber = context.payload?.discussion?.number; - const labelsEnv = process.env.GH_AW_ISSUE_LABELS; - let envLabels = labelsEnv + let title = pullRequestItem.title.trim(); + let bodyLines = pullRequestItem.body.split("\n"); + let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; + if (!title) { + title = "Agent Output"; + } + const titlePrefix = process.env.GH_AW_PR_TITLE_PREFIX; + if (titlePrefix && !title.startsWith(titlePrefix)) { + title = titlePrefix + title; + } + const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; + const runId = context.runId; + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository + ? `${context.payload.repository.html_url}/actions/runs/${runId}` + : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + const trackerIDComment = getTrackerID("markdown"); + if (trackerIDComment) { + bodyLines.push(trackerIDComment); + } + addExpirationComment(bodyLines, "GH_AW_PR_EXPIRES", "Pull Request"); + bodyLines.push(``, ``, `> AI generated by [${workflowName}](${runUrl})`, ""); + const body = bodyLines.join("\n").trim(); + const labelsEnv = process.env.GH_AW_PR_LABELS; + const labels = labelsEnv ? labelsEnv .split(",") - .map(label => label.trim()) - .filter(label => label) + .map( label => label.trim()) + .filter( label => label) : []; - const createdIssues = []; - for (let i = 0; i < createIssueItems.length; i++) { - const createIssueItem = createIssueItems[i]; - const itemRepo = createIssueItem.repo ? String(createIssueItem.repo).trim() : defaultTargetRepo; - const repoValidation = validateRepo(itemRepo, defaultTargetRepo, allowedRepos); - if (!repoValidation.valid) { - core.warning(`Skipping issue: ${repoValidation.error}`); - continue; + const draftEnv = process.env.GH_AW_PR_DRAFT; + const draft = draftEnv ? draftEnv.toLowerCase() === "true" : true; + core.info(`Creating pull request with title: ${title}`); + core.info(`Labels: ${JSON.stringify(labels)}`); + core.info(`Draft: ${draft}`); + core.info(`Body length: ${body.length}`); + const randomHex = crypto.randomBytes(8).toString("hex"); + if (!branchName) { + core.info("No branch name provided in JSONL, generating unique branch name"); + branchName = `${workflowId}-${randomHex}`; + } else { + branchName = `${branchName}-${randomHex}`; + core.info(`Using branch name from JSONL with added salt: ${branchName}`); + } + core.info(`Generated branch name: ${branchName}`); + core.info(`Base branch: ${baseBranch}`); + core.info(`Fetching latest changes and checking out base branch: ${baseBranch}`); + await exec.exec("git fetch origin"); + await exec.exec(`git checkout ${baseBranch}`); + core.info(`Branch should not exist locally, creating new branch from base: ${branchName}`); + await exec.exec(`git checkout -b ${branchName}`); + core.info(`Created new branch from base: ${branchName}`); + if (!isEmpty) { + core.info("Applying patch..."); + const patchLines = patchContent.split("\n"); + const previewLineCount = Math.min(500, patchLines.length); + core.info(`Patch preview (first ${previewLineCount} of ${patchLines.length} lines):`); + for (let i = 0; i < previewLineCount; i++) { + core.info(patchLines[i]); } - const repoParts = parseRepoSlug(itemRepo); - if (!repoParts) { - core.warning(`Skipping issue: Invalid repository format '${itemRepo}'. Expected 'owner/repo'.`); - continue; + try { + await exec.exec("git am /tmp/gh-aw/aw.patch"); + core.info("Patch applied successfully"); + } catch (patchError) { + core.error(`Failed to apply patch: ${patchError instanceof Error ? patchError.message : String(patchError)}`); + try { + core.info("Investigating patch failure..."); + const statusResult = await exec.getExecOutput("git", ["status"]); + core.info("Git status output:"); + core.info(statusResult.stdout); + const patchResult = await exec.getExecOutput("git", ["am", "--show-current-patch=diff"]); + core.info("Failed patch content:"); + core.info(patchResult.stdout); + } catch (investigateError) { + core.warning( + `Failed to investigate patch failure: ${investigateError instanceof Error ? investigateError.message : String(investigateError)}` + ); + } + core.setFailed("Failed to apply patch"); + return; } - const temporaryId = createIssueItem.temporary_id || generateTemporaryId(); - core.info( - `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}, temporaryId=${temporaryId}, repo=${itemRepo}` - ); - core.info(`Debug: createIssueItem.parent = ${JSON.stringify(createIssueItem.parent)}`); - core.info(`Debug: parentIssueNumber from context = ${JSON.stringify(parentIssueNumber)}`); - let effectiveParentIssueNumber; - let effectiveParentRepo = itemRepo; - if (createIssueItem.parent !== undefined) { - if (isTemporaryId(createIssueItem.parent)) { - const resolvedParent = temporaryIdMap.get(normalizeTemporaryId(createIssueItem.parent)); - if (resolvedParent !== undefined) { - effectiveParentIssueNumber = resolvedParent.number; - effectiveParentRepo = resolvedParent.repo; - core.info(`Resolved parent temporary ID '${createIssueItem.parent}' to ${effectiveParentRepo}#${effectiveParentIssueNumber}`); - } else { - core.warning( - `Parent temporary ID '${createIssueItem.parent}' not found in map. Ensure parent issue is created before sub-issues.` - ); - effectiveParentIssueNumber = undefined; + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; } - } else { - effectiveParentIssueNumber = parseInt(String(createIssueItem.parent), 10); - if (isNaN(effectiveParentIssueNumber)) { - core.warning(`Invalid parent value: ${createIssueItem.parent}`); - effectiveParentIssueNumber = undefined; + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Changes pushed to branch"); + } catch (pushError) { + core.error(`Git push failed: ${pushError instanceof Error ? pushError.message : String(pushError)}`); + core.warning("Git push operation failed - creating fallback issue instead of pull request"); + const runId = context.runId; + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository + ? `${context.payload.repository.html_url}/actions/runs/${runId}` + : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + let patchPreview = ""; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + patchPreview = generatePatchPreview(patchContent); + } + const fallbackBody = `${body} + --- + > [!NOTE] + > This was originally intended as a pull request, but the git push operation failed. + > + > **Workflow Run:** [View run details and download patch artifact](${runUrl}) + > + > The patch file is available as an artifact (\`aw.patch\`) in the workflow run linked above. + To apply the patch locally: + \`\`\`sh + # Download the artifact from the workflow run ${runUrl} + # (Use GitHub MCP tools if gh CLI is not available) + gh run download ${runId} -n aw.patch + # Apply the patch + git am aw.patch + \`\`\` + ${patchPreview}`; + try { + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: fallbackBody, + labels: labels, + }); + core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); + await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); + core.setOutput("issue_number", issue.number); + core.setOutput("issue_url", issue.html_url); + core.setOutput("branch_name", branchName); + core.setOutput("fallback_used", "true"); + core.setOutput("push_failed", "true"); + await core.summary + .addRaw( + ` + ## Push Failure Fallback + - **Push Error:** ${pushError instanceof Error ? pushError.message : String(pushError)} + - **Fallback Issue:** [#${issue.number}](${issue.html_url}) + - **Patch Artifact:** Available in workflow run artifacts + - **Note:** Push failed, created issue as fallback + ` + ) + .write(); + return; + } catch (issueError) { + core.setFailed( + `Failed to push and failed to create fallback issue. Push error: ${pushError instanceof Error ? pushError.message : String(pushError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}` + ); + return; + } + } + } else { + core.info("Skipping patch application (empty patch)"); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); + return; } } else { - const contextRepo = `${context.repo.owner}/${context.repo.repo}`; - if (itemRepo === contextRepo) { - effectiveParentIssueNumber = parentIssueNumber; + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; } } - core.info( - `Debug: effectiveParentIssueNumber = ${JSON.stringify(effectiveParentIssueNumber)}, effectiveParentRepo = ${effectiveParentRepo}` - ); - if (effectiveParentIssueNumber && createIssueItem.parent !== undefined) { - core.info(`Using explicit parent issue number from item: ${effectiveParentRepo}#${effectiveParentIssueNumber}`); - } - let labels = [...envLabels]; - if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) { - labels = [...labels, ...createIssueItem.labels]; - } - labels = labels - .filter(label => !!label) - .map(label => String(label).trim()) - .filter(label => label) - .map(label => sanitizeLabelContent(label)) - .filter(label => label) - .map(label => (label.length > 64 ? label.substring(0, 64) : label)) - .filter((label, index, arr) => arr.indexOf(label) === index); - let title = createIssueItem.title ? createIssueItem.title.trim() : ""; - let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); - let bodyLines = processedBody.split("\n"); - if (!title) { - title = createIssueItem.body || "Agent Output"; - } - const titlePrefix = process.env.GH_AW_ISSUE_TITLE_PREFIX; - if (titlePrefix && !title.startsWith(titlePrefix)) { - title = titlePrefix + title; - } - if (effectiveParentIssueNumber) { - core.info("Detected issue context, parent issue " + effectiveParentRepo + "#" + effectiveParentIssueNumber); - if (effectiveParentRepo === itemRepo) { - bodyLines.push(`Related to #${effectiveParentIssueNumber}`); - } else { - bodyLines.push(`Related to ${effectiveParentRepo}#${effectiveParentIssueNumber}`); - } + } + try { + const { data: pullRequest } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + head: branchName, + base: baseBranch, + draft: draft, + }); + core.info(`Created pull request #${pullRequest.number}: ${pullRequest.html_url}`); + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullRequest.number, + labels: labels, + }); + core.info(`Added labels to pull request: ${JSON.stringify(labels)}`); } - const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow"; - const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE || ""; - const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL || ""; - const runId = context.runId; + core.setOutput("pull_request_number", pullRequest.number); + core.setOutput("pull_request_url", pullRequest.html_url); + core.setOutput("branch_name", branchName); + await updateActivationComment(github, context, core, pullRequest.html_url, pullRequest.number); + await core.summary + .addRaw( + ` + ## Pull Request + - **Pull Request**: [#${pullRequest.number}](${pullRequest.html_url}) + - **Branch**: \`${branchName}\` + - **Base Branch**: \`${baseBranch}\` + ` + ) + .write(); + } catch (prError) { + core.warning(`Failed to create pull request: ${prError instanceof Error ? prError.message : String(prError)}`); + core.info("Falling back to creating an issue instead"); const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; - const runUrl = context.payload.repository - ? `${context.payload.repository.html_url}/actions/runs/${runId}` - : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - const trackerIDComment = getTrackerID("markdown"); - if (trackerIDComment) { - bodyLines.push(trackerIDComment); - } - addExpirationComment(bodyLines, "GH_AW_ISSUE_EXPIRES", "Issue"); - bodyLines.push( - ``, - ``, - generateFooter( - workflowName, - runUrl, - workflowSource, - workflowSourceURL, - triggeringIssueNumber, - triggeringPRNumber, - triggeringDiscussionNumber - ).trimEnd(), - "" - ); - const body = bodyLines.join("\n").trim(); - core.info(`Creating issue in ${itemRepo} with title: ${title}`); - core.info(`Labels: ${labels}`); - core.info(`Body length: ${body.length}`); + const branchUrl = context.payload.repository + ? `${context.payload.repository.html_url}/tree/${branchName}` + : `${githubServer}/${context.repo.owner}/${context.repo.repo}/tree/${branchName}`; + let patchPreview = ""; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + patchPreview = generatePatchPreview(patchContent); + } + const fallbackBody = `${body} + --- + **Note:** This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch [\`${branchName}\`](${branchUrl}). + **Original error:** ${prError instanceof Error ? prError.message : String(prError)} + You can manually create a pull request from the branch if needed.${patchPreview}`; try { const { data: issue } = await github.rest.issues.create({ - owner: repoParts.owner, - repo: repoParts.repo, + owner: context.repo.owner, + repo: context.repo.repo, title: title, - body: body, + body: fallbackBody, labels: labels, }); - core.info(`Created issue ${itemRepo}#${issue.number}: ${issue.html_url}`); - createdIssues.push({ ...issue, _repo: itemRepo }); - temporaryIdMap.set(normalizeTemporaryId(temporaryId), { repo: itemRepo, number: issue.number }); - core.info(`Stored temporary ID mapping: ${temporaryId} -> ${itemRepo}#${issue.number}`); - core.info(`Debug: About to check if sub-issue linking is needed. effectiveParentIssueNumber = ${effectiveParentIssueNumber}`); - if (effectiveParentIssueNumber && effectiveParentRepo === itemRepo) { - core.info(`Attempting to link issue #${issue.number} as sub-issue of #${effectiveParentIssueNumber}`); - try { - core.info(`Fetching node ID for parent issue #${effectiveParentIssueNumber}...`); - const getIssueNodeIdQuery = ` - query($owner: String!, $repo: String!, $issueNumber: Int!) { - repository(owner: $owner, name: $repo) { - issue(number: $issueNumber) { - id - } - } - } - `; - const parentResult = await github.graphql(getIssueNodeIdQuery, { - owner: repoParts.owner, - repo: repoParts.repo, - issueNumber: effectiveParentIssueNumber, - }); - const parentNodeId = parentResult.repository.issue.id; - core.info(`Parent issue node ID: ${parentNodeId}`); - core.info(`Fetching node ID for child issue #${issue.number}...`); - const childResult = await github.graphql(getIssueNodeIdQuery, { - owner: repoParts.owner, - repo: repoParts.repo, - issueNumber: issue.number, - }); - const childNodeId = childResult.repository.issue.id; - core.info(`Child issue node ID: ${childNodeId}`); - core.info(`Executing addSubIssue mutation...`); - const addSubIssueMutation = ` - mutation($issueId: ID!, $subIssueId: ID!) { - addSubIssue(input: { - issueId: $issueId, - subIssueId: $subIssueId - }) { - subIssue { - id - number - } - } - } - `; - await github.graphql(addSubIssueMutation, { - issueId: parentNodeId, - subIssueId: childNodeId, - }); - core.info("✓ Successfully linked issue #" + issue.number + " as sub-issue of #" + effectiveParentIssueNumber); - } catch (error) { - core.info(`Warning: Could not link sub-issue to parent: ${error instanceof Error ? error.message : String(error)}`); - core.info(`Error details: ${error instanceof Error ? error.stack : String(error)}`); - try { - core.info(`Attempting fallback: adding comment to parent issue #${effectiveParentIssueNumber}...`); - await github.rest.issues.createComment({ - owner: repoParts.owner, - repo: repoParts.repo, - issue_number: effectiveParentIssueNumber, - body: `Created related issue: #${issue.number}`, - }); - core.info("✓ Added comment to parent issue #" + effectiveParentIssueNumber + " (sub-issue linking not available)"); - } catch (commentError) { - core.info( - `Warning: Could not add comment to parent issue: ${commentError instanceof Error ? commentError.message : String(commentError)}` - ); - } - } - } else if (effectiveParentIssueNumber && effectiveParentRepo !== itemRepo) { - core.info(`Skipping sub-issue linking: parent is in different repository (${effectiveParentRepo})`); - } else { - core.info(`Debug: No parent issue number set, skipping sub-issue linking`); - } - if (i === createIssueItems.length - 1) { - core.setOutput("issue_number", issue.number); - core.setOutput("issue_url", issue.html_url); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("Issues has been disabled in this repository")) { - core.info(`⚠ Cannot create issue "${title}" in ${itemRepo}: Issues are disabled for this repository`); - core.info("Consider enabling issues in repository settings if you want to create issues automatically"); - continue; - } - core.error(`✗ Failed to create issue "${title}" in ${itemRepo}: ${errorMessage}`); - throw error; - } - } - if (createdIssues.length > 0) { - let summaryContent = "\n\n## GitHub Issues\n"; - for (const issue of createdIssues) { - const repoLabel = issue._repo !== defaultTargetRepo ? ` (${issue._repo})` : ""; - summaryContent += `- Issue #${issue.number}${repoLabel}: [${issue.title}](${issue.html_url})\n`; + core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); + await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); + core.setOutput("issue_number", issue.number); + core.setOutput("issue_url", issue.html_url); + core.setOutput("branch_name", branchName); + core.setOutput("fallback_used", "true"); + await core.summary + .addRaw( + ` + ## Fallback Issue Created + - **Issue**: [#${issue.number}](${issue.html_url}) + - **Branch**: [\`${branchName}\`](${branchUrl}) + - **Base Branch**: \`${baseBranch}\` + - **Note**: Pull request creation failed, created issue as fallback + ` + ) + .write(); + } catch (issueError) { + core.setFailed( + `Failed to create both pull request and fallback issue. PR error: ${prError instanceof Error ? prError.message : String(prError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}` + ); + return; } - await core.summary.addRaw(summaryContent).write(); - } - const tempIdMapOutput = serializeTemporaryIdMap(temporaryIdMap); - core.setOutput("temporary_id_map", tempIdMapOutput); - core.info(`Temporary ID map: ${tempIdMapOutput}`); - const assignCopilot = process.env.GH_AW_ASSIGN_COPILOT === "true"; - if (assignCopilot && createdIssues.length > 0) { - const issuesToAssign = createdIssues.map(issue => `${issue._repo}:${issue.number}`).join(","); - core.setOutput("issues_to_assign_copilot", issuesToAssign); - core.info(`Issues to assign copilot: ${issuesToAssign}`); } - core.info(`Successfully created ${createdIssues.length} issue(s)`); } - (async () => { - await main(); - })(); + await main(); detection: needs: agent @@ -7524,7 +7638,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: WORKFLOW_NAME: "Dev" - WORKFLOW_DESCRIPTION: "Create a poem about GitHub and save it to repo-memory" + WORKFLOW_DESCRIPTION: "Create an empty pull request for agent to push changes to" with: script: | const fs = require('fs'); diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index ab15ccc1ca..7ea30ca9f0 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -2,7 +2,7 @@ on: workflow_dispatch: name: Dev -description: Create a poem about GitHub and save it to repo-memory +description: Create an empty pull request for agent to push changes to timeout-minutes: 5 strict: false engine: copilot @@ -16,7 +16,8 @@ tools: imports: - shared/gh.md safe-outputs: - create-issue: + create-pull-request: + allow-empty: true staged: true steps: - name: Download issues data @@ -26,5 +27,8 @@ steps: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} --- -Read the last pull request using `githubissues-gh` tool -and create an issue with the summary. +Create an empty pull request that prepares a branch for future changes. +The pull request should have: +- Title: "Feature: Prepare branch for agent updates" +- Body: "This is an empty pull request created to prepare a feature branch that an agent can push changes to later." +- Branch name: "feature/agent-updates" diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 73d7055863..67f138a5d3 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -2313,7 +2313,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2435,6 +2435,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2518,7 +2535,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5119,7 +5136,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -7584,6 +7616,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Developer Documentation Consolidator" GH_AW_ENGINE_ID: "claude" @@ -7767,52 +7800,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -7833,7 +7881,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -7849,6 +7897,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -8058,16 +8108,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index 5d6f48da2f..327af2c8af 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -1593,7 +1593,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1715,6 +1715,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1798,7 +1815,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3626,7 +3643,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6110,6 +6142,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Dictation Prompt Generator" GH_AW_ENGINE_ID: "copilot" @@ -6293,52 +6326,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6359,7 +6407,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6375,6 +6423,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6584,16 +6634,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index ba03d08c1b..a67ce4f2a9 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -1624,7 +1624,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1746,6 +1746,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1829,7 +1846,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3765,7 +3782,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index d01983f9ae..b01a075077 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -1672,7 +1672,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1794,6 +1794,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1877,7 +1894,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3832,7 +3849,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index 5851be64f2..790d98b757 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -1641,7 +1641,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1763,6 +1763,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1846,7 +1863,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3680,7 +3697,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/github-mcp-structural-analysis.lock.yml b/.github/workflows/github-mcp-structural-analysis.lock.yml index 88869957a9..e4c061fd26 100644 --- a/.github/workflows/github-mcp-structural-analysis.lock.yml +++ b/.github/workflows/github-mcp-structural-analysis.lock.yml @@ -2279,7 +2279,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2401,6 +2401,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2484,7 +2501,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5045,7 +5062,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index dc7a6b597b..8c79cdd041 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -2164,7 +2164,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2286,6 +2286,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2369,7 +2386,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4822,7 +4839,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -7287,6 +7319,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "GitHub MCP Remote Server Tools Report Generator" GH_AW_ENGINE_ID: "claude" @@ -7470,52 +7503,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -7536,7 +7584,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -7552,6 +7600,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -7761,16 +7811,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 1f61d09d4b..7acb2fe6d8 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -2102,7 +2102,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2224,6 +2224,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2307,7 +2324,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4784,7 +4801,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -7275,6 +7307,7 @@ jobs: GH_AW_PR_LABELS: "documentation,glossary" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Glossary Maintainer" GH_AW_ENGINE_ID: "copilot" @@ -7458,52 +7491,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -7524,7 +7572,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -7540,6 +7588,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -7749,16 +7799,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/go-fan.lock.yml b/.github/workflows/go-fan.lock.yml index ff1fa768a3..f2328831ec 100644 --- a/.github/workflows/go-fan.lock.yml +++ b/.github/workflows/go-fan.lock.yml @@ -1954,7 +1954,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2076,6 +2076,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2159,7 +2176,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4394,7 +4411,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index 1b6ab1fe97..9a7dbdd854 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -1864,7 +1864,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1986,6 +1986,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2069,7 +2086,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4127,7 +4144,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -5892,6 +5924,7 @@ jobs: GH_AW_PR_LABELS: "enhancement,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Go Logger Enhancement" GH_AW_ENGINE_ID: "claude" @@ -6075,52 +6108,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6141,7 +6189,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6157,6 +6205,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6366,16 +6416,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 8498b0e125..8cf6877a98 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -1728,7 +1728,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1850,6 +1850,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1933,7 +1950,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3878,7 +3895,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index 3364baf180..75c60191e3 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -2996,7 +2996,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3118,6 +3118,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3201,7 +3218,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5114,7 +5131,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index 8c93021139..a01387e54c 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -1733,7 +1733,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1855,6 +1855,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1938,7 +1955,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3892,7 +3909,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -5657,6 +5689,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation,instructions" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Instructions Janitor" GH_AW_ENGINE_ID: "claude" @@ -5840,52 +5873,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -5906,7 +5954,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -5922,6 +5970,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6131,16 +6181,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index 4e6a81de2f..0dfed9b07e 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -1718,7 +1718,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1840,6 +1840,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1923,7 +1940,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3841,7 +3858,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 1d5114c9df..d34120c9ae 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -2726,7 +2726,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2848,6 +2848,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2931,7 +2948,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4690,7 +4707,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index 94276e082a..058b74d3bf 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -2281,7 +2281,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2403,6 +2403,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2486,7 +2503,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4390,7 +4407,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml index 1e635fd68d..0e7607ff94 100644 --- a/.github/workflows/issue-triage-agent.lock.yml +++ b/.github/workflows/issue-triage-agent.lock.yml @@ -1914,7 +1914,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2036,6 +2036,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2119,7 +2136,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3858,7 +3875,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/layout-spec-maintainer.lock.yml b/.github/workflows/layout-spec-maintainer.lock.yml index 9010cecbe3..51e49ad077 100644 --- a/.github/workflows/layout-spec-maintainer.lock.yml +++ b/.github/workflows/layout-spec-maintainer.lock.yml @@ -1731,7 +1731,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1853,6 +1853,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1936,7 +1953,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3917,7 +3934,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6404,6 +6436,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Layout Specification Maintainer" GH_AW_TRACKER_ID: "layout-spec-maintainer" @@ -6588,52 +6621,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6654,7 +6702,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6670,6 +6718,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6879,16 +6929,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index 09d5aad77a..a9084f42ff 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -1959,7 +1959,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2081,6 +2081,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2164,7 +2181,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4405,7 +4422,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index de5d3d8cf8..20ba1e37ff 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -1799,7 +1799,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1921,6 +1921,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2004,7 +2021,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4295,7 +4312,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index 0df0973dfe..9674c50ddb 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -2146,7 +2146,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2268,6 +2268,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2351,7 +2368,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4459,7 +4476,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index c8c25c18fb..0a26b9804c 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -1382,7 +1382,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1504,6 +1504,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1587,7 +1604,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3361,7 +3378,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/org-health-report.lock.yml b/.github/workflows/org-health-report.lock.yml index 52ed3cf85d..d523b4c0e1 100644 --- a/.github/workflows/org-health-report.lock.yml +++ b/.github/workflows/org-health-report.lock.yml @@ -2388,7 +2388,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2510,6 +2510,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2593,7 +2610,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5156,7 +5173,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index b0b6ff9ded..a586fb5b97 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -2981,7 +2981,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3103,6 +3103,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3186,7 +3203,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5139,7 +5156,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 0a6ee1d923..8d590d7851 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -2449,7 +2449,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2571,6 +2571,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2654,7 +2671,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4595,7 +4612,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 5050c8e9a5..29b8e38296 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -4110,7 +4110,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -4232,6 +4232,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -4315,7 +4332,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -6191,7 +6208,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -10658,6 +10690,7 @@ jobs: GH_AW_PR_LABELS: "poetry,automation,creative-writing" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} @@ -10846,52 +10879,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -10912,7 +10960,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -10928,6 +10976,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -11137,16 +11187,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/portfolio-analyst.lock.yml b/.github/workflows/portfolio-analyst.lock.yml index 7f36b1186e..18695e839d 100644 --- a/.github/workflows/portfolio-analyst.lock.yml +++ b/.github/workflows/portfolio-analyst.lock.yml @@ -1968,7 +1968,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2090,6 +2090,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2173,7 +2190,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4507,7 +4524,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index 11a02c55ba..5ea733937e 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -3032,7 +3032,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3154,6 +3154,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3237,7 +3254,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5459,7 +5476,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index a3c7ea4bd2..53f3e17b9d 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -2644,7 +2644,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2766,6 +2766,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2849,7 +2866,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5680,7 +5697,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index 240d69eab7..77bfd73240 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -2480,7 +2480,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2602,6 +2602,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2685,7 +2702,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5523,7 +5540,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index fcb8e104b1..bd0fdc5cfd 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -3299,7 +3299,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3421,6 +3421,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3504,7 +3521,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5721,7 +5738,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -8215,6 +8247,7 @@ jobs: GH_AW_PR_LABELS: "automation,workflow-optimization" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "ignore" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} @@ -8401,52 +8434,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -8467,7 +8515,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -8483,6 +8531,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -8692,16 +8742,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 5ffd229b0d..9e6a69ffe8 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1735,7 +1735,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1857,6 +1857,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1940,7 +1957,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3819,7 +3836,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index 6667a16276..9f15d6eb32 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -1622,7 +1622,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1744,6 +1744,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1827,7 +1844,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3700,7 +3717,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml index b625ccd937..5a45fe6e6c 100644 --- a/.github/workflows/repository-quality-improver.lock.yml +++ b/.github/workflows/repository-quality-improver.lock.yml @@ -2077,7 +2077,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2199,6 +2199,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2282,7 +2299,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4737,7 +4754,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index 37efa982dd..7dffa957e3 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -1543,7 +1543,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1665,6 +1665,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1748,7 +1765,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3614,7 +3631,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml index 2d4610a57b..d7b1206c31 100644 --- a/.github/workflows/safe-output-health.lock.yml +++ b/.github/workflows/safe-output-health.lock.yml @@ -2084,7 +2084,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2206,6 +2206,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2289,7 +2306,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4702,7 +4719,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index 0c0d869048..740b540b26 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -1969,7 +1969,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2091,6 +2091,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2174,7 +2191,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4350,7 +4367,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 9e879395f3..89ac03249f 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -3285,7 +3285,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3407,6 +3407,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3490,7 +3507,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5755,7 +5772,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index 11147835a8..259f6c0bf2 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -1711,7 +1711,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1833,6 +1833,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1916,7 +1933,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3900,7 +3917,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -5665,6 +5697,7 @@ jobs: GH_AW_PR_LABELS: "security,automated-fix" GH_AW_PR_DRAFT: "true" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Security Fix PR" GH_AW_ENGINE_ID: "claude" @@ -5848,52 +5881,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -5914,7 +5962,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -5930,6 +5978,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6139,16 +6189,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 47e8031070..e12f6b830c 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -2118,7 +2118,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2240,6 +2240,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2323,7 +2340,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4738,7 +4755,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 5ec14f2c1e..e265bfe644 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -3396,7 +3396,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3518,6 +3518,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3601,7 +3618,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5636,7 +5653,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index a7015d5d7c..720f193e68 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -3188,7 +3188,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3310,6 +3310,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3393,7 +3410,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5206,7 +5223,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index 60aef8dc59..feb046af6e 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -3270,7 +3270,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3392,6 +3392,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3475,7 +3492,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -6627,7 +6644,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml index f21b5f58a9..e60e85eada 100644 --- a/.github/workflows/smoke-copilot-playwright.lock.yml +++ b/.github/workflows/smoke-copilot-playwright.lock.yml @@ -3261,7 +3261,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3383,6 +3383,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3466,7 +3483,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -6607,7 +6624,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-copilot-safe-inputs.lock.yml b/.github/workflows/smoke-copilot-safe-inputs.lock.yml index c413f6b53f..31536ef2ff 100644 --- a/.github/workflows/smoke-copilot-safe-inputs.lock.yml +++ b/.github/workflows/smoke-copilot-safe-inputs.lock.yml @@ -3166,7 +3166,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3288,6 +3288,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3371,7 +3388,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -6332,7 +6349,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index ef89f79ebe..54028893b2 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -3153,7 +3153,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3275,6 +3275,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3358,7 +3375,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5157,7 +5174,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index b79f208809..b81eb88e08 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -3005,7 +3005,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3127,6 +3127,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3210,7 +3227,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5379,7 +5396,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/smoke-srt.lock.yml b/.github/workflows/smoke-srt.lock.yml index 212143c00b..be18eca3f2 100644 --- a/.github/workflows/smoke-srt.lock.yml +++ b/.github/workflows/smoke-srt.lock.yml @@ -1416,7 +1416,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1538,6 +1538,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1621,7 +1638,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3510,7 +3527,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/spec-kit-execute.lock.yml b/.github/workflows/spec-kit-execute.lock.yml index 963d31625b..4a7222cc59 100644 --- a/.github/workflows/spec-kit-execute.lock.yml +++ b/.github/workflows/spec-kit-execute.lock.yml @@ -1884,7 +1884,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2006,6 +2006,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2089,7 +2106,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4228,7 +4245,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6732,6 +6764,7 @@ jobs: GH_AW_PR_LABELS: "spec-kit,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Spec-Kit Execute" GH_AW_TRACKER_ID: "spec-kit-execute" @@ -6916,52 +6949,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6982,7 +7030,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6998,6 +7046,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -7207,16 +7257,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/spec-kit-executor.lock.yml b/.github/workflows/spec-kit-executor.lock.yml index 9b2c7cc611..158c21af5d 100644 --- a/.github/workflows/spec-kit-executor.lock.yml +++ b/.github/workflows/spec-kit-executor.lock.yml @@ -1730,7 +1730,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1852,6 +1852,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1935,7 +1952,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3918,7 +3935,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6422,6 +6454,7 @@ jobs: GH_AW_PR_LABELS: "spec-kit,automation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Spec Kit Executor" GH_AW_TRACKER_ID: "spec-kit-executor" @@ -6606,52 +6639,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6672,7 +6720,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6688,6 +6736,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6897,16 +6947,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/speckit-dispatcher.lock.yml b/.github/workflows/speckit-dispatcher.lock.yml index aa56216215..e4f77124b7 100644 --- a/.github/workflows/speckit-dispatcher.lock.yml +++ b/.github/workflows/speckit-dispatcher.lock.yml @@ -3293,7 +3293,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3415,6 +3415,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3498,7 +3515,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5634,7 +5651,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 40c827628d..c6c54865dc 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -2478,7 +2478,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2600,6 +2600,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2683,7 +2700,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5392,7 +5409,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml index 99cb339d12..98367cfdac 100644 --- a/.github/workflows/static-analysis-report.lock.yml +++ b/.github/workflows/static-analysis-report.lock.yml @@ -1993,7 +1993,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2115,6 +2115,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2198,7 +2215,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4441,7 +4458,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 69c81f767b..74e9032d64 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -1732,7 +1732,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1854,6 +1854,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1937,7 +1954,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3915,7 +3932,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index a281e4a797..d8b3b1d815 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -2590,7 +2590,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2712,6 +2712,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2795,7 +2812,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4972,7 +4989,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -7474,6 +7506,7 @@ jobs: GH_AW_PR_LABELS: "documentation" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_WORKFLOW_NAME: "Technical Doc Writer" GH_AW_ENGINE_ID: "copilot" @@ -7658,52 +7691,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -7724,7 +7772,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -7740,6 +7788,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -7949,16 +7999,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/test-discussion-expires.lock.yml b/.github/workflows/test-discussion-expires.lock.yml index 9f18c5754c..77c4e3ff0f 100644 --- a/.github/workflows/test-discussion-expires.lock.yml +++ b/.github/workflows/test-discussion-expires.lock.yml @@ -1420,7 +1420,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1542,6 +1542,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1625,7 +1642,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3299,7 +3316,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/test-python-safe-input.lock.yml b/.github/workflows/test-python-safe-input.lock.yml index 58ec7e2865..5dc3bbdcb7 100644 --- a/.github/workflows/test-python-safe-input.lock.yml +++ b/.github/workflows/test-python-safe-input.lock.yml @@ -1518,7 +1518,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1640,6 +1640,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1723,7 +1740,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4910,7 +4927,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 8452b43a15..772857cb1e 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -2025,7 +2025,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2147,6 +2147,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2230,7 +2247,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4032,7 +4049,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -6518,6 +6550,7 @@ jobs: GH_AW_PR_LABELS: "automation,maintenance" GH_AW_PR_DRAFT: "false" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} @@ -6703,52 +6736,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -6769,7 +6817,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -6785,6 +6833,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -6994,16 +7044,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml index 93869e1e6d..145ce0e4fb 100644 --- a/.github/workflows/typist.lock.yml +++ b/.github/workflows/typist.lock.yml @@ -2102,7 +2102,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2224,6 +2224,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2307,7 +2324,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4768,7 +4785,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index e1be373962..6e698ce46a 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -3047,7 +3047,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -3169,6 +3169,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -3252,7 +3269,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -5501,7 +5518,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output @@ -7277,6 +7309,7 @@ jobs: GH_AW_PR_LABELS: "documentation,automation" GH_AW_PR_DRAFT: "true" GH_AW_PR_IF_NO_CHANGES: "warn" + GH_AW_PR_ALLOW_EMPTY: "false" GH_AW_MAX_PATCH_SIZE: 1024 GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} @@ -7463,52 +7496,67 @@ jobs: core.info("Agent output content is empty"); } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); const patchSizeBytes = Buffer.byteLength(patchContent, "utf8"); @@ -7529,7 +7577,7 @@ jobs: } core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { case "error": @@ -7545,6 +7593,8 @@ jobs: core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -7754,16 +7804,44 @@ jobs: } } else { core.info("Skipping patch application (empty patch)"); - const message = "No changes to apply - noop operation completed successfully"; - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - return; - case "warn": - default: - core.warning(message); + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + try { + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + const message = "No changes to apply - noop operation completed successfully"; + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + return; + case "warn": + default: + core.warning(message); + return; + } } } try { diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index e3c7055a11..1c4418a923 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -1729,7 +1729,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -1851,6 +1851,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -1934,7 +1951,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -3956,7 +3973,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml index c782da4d34..845f814d49 100644 --- a/.github/workflows/weekly-issue-summary.lock.yml +++ b/.github/workflows/weekly-issue-summary.lock.yml @@ -2081,7 +2081,7 @@ jobs: const { getCurrentBranch } = require("./get_current_branch.cjs"); const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); - function createHandlers(server, appendSafeOutput) { + function createHandlers(server, appendSafeOutput, config = {}) { const defaultHandler = type => args => { const entry = { ...(args || {}), type }; let largeContent = null; @@ -2203,6 +2203,23 @@ jobs: } entry.branch = detectedBranch; } + const allowEmpty = config.create_pull_request?.allow_empty === true; + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); if (!patchResult.success) { @@ -2286,7 +2303,7 @@ jobs: const server = createServer(SERVER_INFO, { logDir: MCP_LOG_DIR }); const { config: safeOutputsConfig, outputFile, tools: ALL_TOOLS } = bootstrapSafeOutputsServer(server); const appendSafeOutput = createAppendFunction(outputFile); - const handlers = createHandlers(server, appendSafeOutput); + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; const toolsWithHandlers = attachHandlers(ALL_TOOLS, handlers); server.debug(` output file: ${outputFile}`); @@ -4748,7 +4765,22 @@ jobs: const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + let allowEmptyPR = false; + if (safeOutputsConfig) { + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); - name: Upload sanitized agent output diff --git a/pkg/cli/templates/github-agentic-workflows.md b/pkg/cli/templates/github-agentic-workflows.md index 13ddf594f4..0b3df484a9 100644 --- a/pkg/cli/templates/github-agentic-workflows.md +++ b/pkg/cli/templates/github-agentic-workflows.md @@ -392,11 +392,11 @@ The YAML frontmatter supports these fields: target-repo: "owner/repo" # Optional: cross-repository ``` When using `safe-outputs.close-pull-request`, the main job does **not** need `pull-requests: write` permission since PR closing is handled by a separate job with appropriate permissions. - - `add-labels:` - Safe label addition to issues or PRs. Labels will be created if they don't already exist in the repository. + - `add-labels:` - Safe label addition to issues or PRs ```yaml safe-outputs: add-labels: - allowed: [bug, enhancement, documentation] # Optional: restrict to specific labels (will be created if missing) + allowed: [bug, enhancement, documentation] # Optional: restrict to specific labels max: 3 # Optional: maximum number of labels (default: 3) target: "*" # Optional: "triggering" (default), "*" (any issue/PR), or number target-repo: "owner/repo" # Optional: cross-repository diff --git a/pkg/parser/schedule_parser.go b/pkg/parser/schedule_parser.go index d0b713d9a8..542a20d80e 100644 --- a/pkg/parser/schedule_parser.go +++ b/pkg/parser/schedule_parser.go @@ -105,15 +105,15 @@ func (p *ScheduleParser) parseInterval() (string, error) { if len(p.tokens) == 2 || (len(p.tokens) > 2 && p.tokens[2] != "minutes" && p.tokens[2] != "hours" && p.tokens[2] != "minute" && p.tokens[2] != "hour") { // Try to parse as short duration format: "every 2h", "every 30m", "every 1d" durationStr := p.tokens[1] - + // Check if it matches the pattern: number followed by unit letter (h, m, d, w, mo) durationPattern := regexp.MustCompile(`^(\d+)([hdwm]|mo)$`) matches := durationPattern.FindStringSubmatch(durationStr) - + if matches != nil { interval, _ := strconv.Atoi(matches[1]) unit := matches[2] - + // Check for conflicting "at time" clause if len(p.tokens) > 2 { for i := 2; i < len(p.tokens); i++ { @@ -122,7 +122,7 @@ func (p *ScheduleParser) parseInterval() (string, error) { } } } - + // Validate minimum duration of 5 minutes totalMinutes := 0 switch unit { @@ -137,11 +137,11 @@ func (p *ScheduleParser) parseInterval() (string, error) { case "mo": totalMinutes = interval * 30 * 24 * 60 // Approximate month as 30 days } - + if totalMinutes < 5 { return "", fmt.Errorf("minimum schedule interval is 5 minutes, got %d minute(s)", totalMinutes) } - + switch unit { case "m": // every Nm -> */N * * * * @@ -221,7 +221,7 @@ func (p *ScheduleParser) parseInterval() (string, error) { case "hours": totalMinutes = interval * 60 } - + if totalMinutes < 5 { return "", fmt.Errorf("minimum schedule interval is 5 minutes, got %d minute(s)", totalMinutes) } @@ -332,7 +332,7 @@ func (p *ScheduleParser) extractTime(startPos int) (string, error) { } timeStr := p.tokens[startPos] - + // Check if there's a UTC offset in the next token if startPos+1 < len(p.tokens) { nextToken := strings.ToLower(p.tokens[startPos+1]) @@ -341,7 +341,7 @@ func (p *ScheduleParser) extractTime(startPos int) (string, error) { timeStr = timeStr + " " + p.tokens[startPos+1] } } - + return timeStr, nil } @@ -352,11 +352,11 @@ func parseTime(timeStr string) (minute string, hour string) { parts := strings.Split(timeStr, " ") var utcOffset int = 0 var baseTime string - + if len(parts) == 2 && strings.HasPrefix(strings.ToLower(parts[1]), "utc") { baseTime = parts[0] offsetStr := strings.ToLower(parts[1]) - + // Parse UTC offset (e.g., utc+9, utc-5, utc+09:00, utc-05:30) if len(offsetStr) > 3 { offsetPart := offsetStr[3:] // Skip "utc" @@ -367,7 +367,7 @@ func parseTime(timeStr string) (minute string, hour string) { sign = -1 offsetPart = offsetPart[1:] } - + // Check if it's HH:MM format if strings.Contains(offsetPart, ":") { offsetParts := strings.Split(offsetPart, ":") @@ -389,9 +389,9 @@ func parseTime(timeStr string) (minute string, hour string) { } else { baseTime = timeStr } - + var baseMinute, baseHour int - + switch baseTime { case "midnight": baseMinute, baseHour = 0, 0 @@ -404,7 +404,7 @@ func parseTime(timeStr string) (minute string, hour string) { isPM := strings.HasSuffix(lowerTime, "pm") // Remove am/pm suffix hourStr := lowerTime[:len(lowerTime)-2] - + hourNum, err := strconv.Atoi(hourStr) if err == nil && hourNum >= 1 && hourNum <= 12 { // Convert 12-hour to 24-hour format @@ -447,11 +447,11 @@ func parseTime(timeStr string) (minute string, hour string) { } } } - + // Apply UTC offset (convert from local time to UTC) // If utc+9, we subtract 9 hours to get UTC time totalMinutes := baseHour*60 + baseMinute - utcOffset - + // Handle wrap-around (keep within 0-1439 minutes, which is 0:00-23:59) for totalMinutes < 0 { totalMinutes += 24 * 60 @@ -459,10 +459,10 @@ func parseTime(timeStr string) (minute string, hour string) { for totalMinutes >= 24*60 { totalMinutes -= 24 * 60 } - + finalHour := totalMinutes / 60 finalMinute := totalMinutes % 60 - + return strconv.Itoa(finalMinute), strconv.Itoa(finalHour) } diff --git a/pkg/parser/schedule_parser_fuzz_test.go b/pkg/parser/schedule_parser_fuzz_test.go index d584d489eb..0c193616cc 100644 --- a/pkg/parser/schedule_parser_fuzz_test.go +++ b/pkg/parser/schedule_parser_fuzz_test.go @@ -18,26 +18,26 @@ import ( // 7. Minimum duration validation works correctly func FuzzScheduleParser(f *testing.F) { // Seed corpus with valid schedule expressions - + // Daily schedules f.Add("daily") f.Add("daily at 02:00") f.Add("daily at midnight") f.Add("daily at noon") f.Add("daily at 09:30") - + // Weekly schedules f.Add("weekly on monday") f.Add("weekly on monday at 06:30") f.Add("weekly on friday at 17:00") f.Add("weekly on sunday at midnight") - + // Monthly schedules f.Add("monthly on 1") f.Add("monthly on 15") f.Add("monthly on 15 at 09:00") f.Add("monthly on 31 at noon") - + // Interval schedules (long format) f.Add("every 10 minutes") f.Add("every 5 minutes") @@ -46,7 +46,7 @@ func FuzzScheduleParser(f *testing.F) { f.Add("every 2 hours") f.Add("every 6 hours") f.Add("every 12 hours") - + // Interval schedules (short duration format) f.Add("every 5m") f.Add("every 10m") @@ -60,7 +60,7 @@ func FuzzScheduleParser(f *testing.F) { f.Add("every 2w") f.Add("every 1mo") f.Add("every 2mo") - + // UTC offset schedules f.Add("daily at 02:00 utc+9") f.Add("daily at 14:00 utc-5") @@ -68,7 +68,7 @@ func FuzzScheduleParser(f *testing.F) { f.Add("weekly on monday at 08:00 utc+1") f.Add("monthly on 15 at 12:00 utc-8") f.Add("daily at 00:00 utc+0") - + // AM/PM time formats f.Add("daily at 3pm") f.Add("daily at 1am") @@ -87,26 +87,26 @@ func FuzzScheduleParser(f *testing.F) { f.Add("weekly on friday at 6pm utc-7") f.Add("monthly on 15 at 10am utc+2") f.Add("monthly on 1 at 7pm utc-3") - + // Valid cron expressions (passthrough) f.Add("0 0 * * *") f.Add("*/5 * * * *") f.Add("0 */2 * * *") f.Add("30 6 * * 1") f.Add("0 9 15 * *") - + // Case variations f.Add("DAILY") f.Add("Weekly On Monday") f.Add("MONTHLY ON 15") - + // Invalid schedules (should error gracefully) - + // Empty and whitespace f.Add("") f.Add(" ") f.Add("\t\n") - + // Below minimum duration f.Add("every 1m") f.Add("every 2m") @@ -114,54 +114,54 @@ func FuzzScheduleParser(f *testing.F) { f.Add("every 4m") f.Add("every 1 minute") f.Add("every 2 minutes") - + // Invalid interval with time conflict f.Add("every 10 minutes at 06:00") f.Add("every 2h at noon") - + // Invalid interval units f.Add("every 10 days") f.Add("every 2 weeks") f.Add("every 1 month") - + // Invalid numbers f.Add("every abc minutes") f.Add("every -5 minutes") f.Add("every 0 minutes") f.Add("every 1000000 hours") - + // Invalid weekly schedules f.Add("weekly monday") f.Add("weekly on funday") f.Add("weekly on 123") - + // Invalid monthly schedules f.Add("monthly 15") f.Add("monthly on abc") f.Add("monthly on 0") f.Add("monthly on 32") f.Add("monthly on -1") - + // Invalid time formats f.Add("daily at 25:00") f.Add("daily at 12:60") f.Add("daily at 12:30:45") f.Add("daily at abc") f.Add("daily at 12") - + // Invalid UTC offsets f.Add("daily at 12:00 utc+25") f.Add("daily at 12:00 utc-15") f.Add("daily at 12:00 utc+99:99") f.Add("daily at 12:00 utc") f.Add("daily at 12:00 utc+abc") - + // Unsupported schedule types f.Add("hourly") f.Add("yearly on 12/25") f.Add("biweekly") f.Add("quarterly") - + // Malformed expressions f.Add("daily at") f.Add("weekly on") @@ -169,13 +169,13 @@ func FuzzScheduleParser(f *testing.F) { f.Add("every") f.Add("every 10") f.Add("at 12:00") - + // Very long strings longString := strings.Repeat("a", 10000) f.Add(longString) f.Add("daily at " + longString) f.Add("every " + longString + " minutes") - + // Special characters f.Add("daily\x00at\x0012:00") f.Add("daily at 12:00\n\r") @@ -184,56 +184,56 @@ func FuzzScheduleParser(f *testing.F) { f.Add("daily at 12:00") f.Add("daily at 12:00$(whoami)") f.Add("daily at 12:00`id`") - + // Unicode characters f.Add("daily at 午前12時") f.Add("每日 at 12:00") f.Add("weekly on lundi") f.Add("毎週月曜日 at 12:00") - + // Multiple spaces and tabs f.Add("daily at 12:00") f.Add("daily\tat\t12:00") f.Add("weekly on monday") f.Add("every 10 minutes") - + // Mixed valid/invalid patterns f.Add("daily at 12:00 weekly on monday") f.Add("every 5 minutes every 10 minutes") f.Add("daily daily") - + // Edge case numbers f.Add("every 2147483647 minutes") f.Add("monthly on 2147483647") f.Add("every -2147483648 hours") - + // Complex UTC offsets f.Add("daily at 12:00 utc+12:30") f.Add("daily at 12:00 utc-11:30") f.Add("daily at 12:00 utc+14") f.Add("daily at 12:00 utc-12") - + // Duplicate keywords f.Add("daily daily at 12:00") f.Add("weekly on monday on tuesday") f.Add("every every 10 minutes") - + // Cron-like but invalid f.Add("0 0 * *") f.Add("0 0 * * * *") f.Add("* * * * * *") f.Add("@daily") f.Add("@weekly") - + // Mixed case with invalid syntax f.Add("DaIlY aT 12:00") f.Add("WEEKLY ON MONDAY AT 12:00") - + // Run the fuzzer f.Fuzz(func(t *testing.T, input string) { // The parser should never panic, even on malformed input cron, original, err := ParseSchedule(input) - + // Basic sanity checks: // 1. Results should be consistent if err != nil { @@ -245,24 +245,24 @@ func FuzzScheduleParser(f *testing.F) { t.Errorf("ParseSchedule returned non-empty original '%s' with error: %v", original, err) } } - + // 2. If successful, cron should not be empty if err == nil && cron == "" { t.Errorf("ParseSchedule succeeded but returned empty cron for input: %q", input) } - + // 3. If error is returned, it should have a meaningful message if err != nil { if err.Error() == "" { t.Errorf("ParseSchedule returned error with empty message for input: %q", input) } - + // Error should not be generic if err.Error() == "error" { t.Errorf("ParseSchedule returned generic 'error' message for input: %q", input) } } - + // 4. Validate cron expression format if successful if err == nil && cron != "" { // Cron should have 5 fields separated by spaces @@ -270,7 +270,7 @@ func FuzzScheduleParser(f *testing.F) { if len(fields) != 5 { t.Errorf("ParseSchedule returned invalid cron format with %d fields (expected 5): %q for input: %q", len(fields), cron, input) } - + // Each field should not be empty for i, field := range fields { if field == "" { @@ -278,7 +278,7 @@ func FuzzScheduleParser(f *testing.F) { } } } - + // 5. If original is returned, it should match the input (after trimming) if original != "" && err == nil { // Original should be the input (for human-friendly formats) @@ -287,20 +287,20 @@ func FuzzScheduleParser(f *testing.F) { t.Errorf("ParseSchedule returned original '%s' that doesn't match input '%s'", original, input) } } - + // 6. Check for known invalid patterns that should error if shouldError(input) && err == nil { // This is informational - the fuzzer might find edge cases // where our simple check is wrong _ = err } - + // 7. Check for known valid patterns that should succeed if looksValid(input) && err != nil { // This is informational - the input might have subtle issues _ = err } - + // 8. Validate minimum duration for interval schedules if strings.HasPrefix(strings.ToLower(strings.TrimSpace(input)), "every") { if err == nil { @@ -320,17 +320,17 @@ func FuzzScheduleParser(f *testing.F) { // shouldError returns true if the input contains obvious invalid patterns func shouldError(input string) bool { input = strings.TrimSpace(input) - + // Empty input should error if input == "" { return true } - + // Control characters should likely error if strings.ContainsAny(input, "\x00\x01\x02\x03") { return true } - + // Minimum duration violations if strings.HasPrefix(strings.ToLower(input), "every 1m") || strings.HasPrefix(strings.ToLower(input), "every 2m") || @@ -342,32 +342,32 @@ func shouldError(input string) bool { strings.HasPrefix(strings.ToLower(input), "every 4 minute") { return true } - + // Known unsupported types if strings.HasPrefix(strings.ToLower(input), "yearly") || strings.HasPrefix(strings.ToLower(input), "hourly") { return true } - + return false } // looksValid returns true if the input looks like it might be valid func looksValid(input string) bool { input = strings.TrimSpace(strings.ToLower(input)) - + // Empty is not valid if input == "" { return false } - + // Check if it's a cron expression (5 fields) fields := strings.Fields(input) if len(fields) == 5 { // Could be a valid cron expression return true } - + // Check for valid patterns validPrefixes := []string{ "daily", @@ -386,12 +386,12 @@ func looksValid(input string) bool { "every 1 hour", "every 2 hours", } - + for _, prefix := range validPrefixes { if strings.HasPrefix(input, prefix) { return true } } - + return false } diff --git a/pkg/parser/schedule_parser_test.go b/pkg/parser/schedule_parser_test.go index 441da57434..049bcd8d5d 100644 --- a/pkg/parser/schedule_parser_test.go +++ b/pkg/parser/schedule_parser_test.go @@ -557,8 +557,8 @@ func TestParseTime(t *testing.T) { // AM/PM formats {"1am", "0", "1"}, {"3pm", "0", "15"}, - {"12am", "0", "0"}, // midnight - {"12pm", "0", "12"}, // noon + {"12am", "0", "0"}, // midnight + {"12pm", "0", "12"}, // noon {"11pm", "0", "23"}, {"6am", "0", "6"}, {"9am", "0", "9"}, @@ -569,8 +569,8 @@ func TestParseTime(t *testing.T) { {"25:00", "0", "0"}, {"12:60", "0", "0"}, {"12", "0", "0"}, - {"13pm", "0", "0"}, // invalid hour for 12-hour format - {"0am", "0", "0"}, // invalid hour for 12-hour format + {"13pm", "0", "0"}, // invalid hour for 12-hour format + {"0am", "0", "0"}, // invalid hour for 12-hour format } for _, tt := range tests { diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 49bcb62eeb..15d501d8a9 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3552,6 +3552,10 @@ "enum": ["warn", "error", "ignore"], "description": "Behavior when no changes to push: 'warn' (default - log warning but succeed), 'error' (fail the action), or 'ignore' (silent success)" }, + "allow-empty": { + "type": "boolean", + "description": "When true, allows creating a pull request without any initial changes or git patch. This is useful for preparing a feature branch that an agent can push changes to later. The branch will be created from the base branch without applying any patch. Defaults to false." + }, "target-repo": { "type": "string", "description": "Target repository in format 'owner/repo' for cross-repository pull request creation. Takes precedence over trial target repo settings." diff --git a/pkg/workflow/create_pull_request.go b/pkg/workflow/create_pull_request.go index b25ab32048..911df2d534 100644 --- a/pkg/workflow/create_pull_request.go +++ b/pkg/workflow/create_pull_request.go @@ -18,6 +18,7 @@ type CreatePullRequestsConfig struct { Reviewers []string `yaml:"reviewers,omitempty"` // List of users/bots to assign as reviewers to the pull request Draft *bool `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil) and explicitly false IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn" (default), "error", or "ignore" + AllowEmpty bool `yaml:"allow-empty,omitempty"` // Allow creating PR without patch file or with empty patch (useful for preparing feature branches) TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository pull requests Expires int `yaml:"expires,omitempty"` // Days until the pull request expires and should be automatically closed (only for same-repo PRs) } @@ -77,6 +78,9 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa } customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_IF_NO_CHANGES: %q\n", ifNoChanges)) + // Pass the allow-empty configuration + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_ALLOW_EMPTY: %q\n", fmt.Sprintf("%t", data.SafeOutputs.CreatePullRequests.AllowEmpty))) + // Pass the maximum patch size configuration maxPatchSize := 1024 // Default value if data.SafeOutputs != nil && data.SafeOutputs.MaximumPatchSize > 0 { @@ -185,6 +189,13 @@ func (c *Compiler) parsePullRequestsConfig(outputMap map[string]any) *CreatePull } } + // Parse allow-empty + if allowEmpty, exists := configMap["allow-empty"]; exists { + if allowEmptyBool, ok := allowEmpty.(bool); ok { + pullRequestsConfig.AllowEmpty = allowEmptyBool + } + } + // Parse target-repo using shared helper with validation targetRepoSlug, isInvalid := parseTargetRepoWithValidation(configMap) if isInvalid { diff --git a/pkg/workflow/create_pull_request_allow_empty_test.go b/pkg/workflow/create_pull_request_allow_empty_test.go new file mode 100644 index 0000000000..33e298b256 --- /dev/null +++ b/pkg/workflow/create_pull_request_allow_empty_test.go @@ -0,0 +1,65 @@ +package workflow + +import ( + "strings" + "testing" +) + +func TestCreatePullRequestJobWithAllowEmpty(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test with allow-empty configured + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + AllowEmpty: true, + }, + }, + } + + job, err := c.buildCreateOutputPullRequestJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create pull request job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that GH_AW_PR_ALLOW_EMPTY environment variable is set + if !strings.Contains(stepsContent, `GH_AW_PR_ALLOW_EMPTY: "true"`) { + t.Error("Expected GH_AW_PR_ALLOW_EMPTY environment variable to be set to true") + } + + // Check that the JavaScript code includes allow-empty logic + if !strings.Contains(stepsContent, "allowEmpty") { + t.Error("Expected JavaScript code to include allowEmpty variable") + } +} + +func TestCreatePullRequestJobWithoutAllowEmpty(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test without allow-empty (default should be false) + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{}, + }, + } + + job, err := c.buildCreateOutputPullRequestJob(workflowData, "main_job") + if err != nil { + t.Fatalf("Unexpected error building create pull request job: %v", err) + } + + // Convert steps to a single string for testing + stepsContent := strings.Join(job.Steps, "") + + // Check that GH_AW_PR_ALLOW_EMPTY environment variable is set to false + if !strings.Contains(stepsContent, `GH_AW_PR_ALLOW_EMPTY: "false"`) { + t.Error("Expected GH_AW_PR_ALLOW_EMPTY environment variable to be set to false by default") + } +} diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index 95e74c6610..9d23cc25ad 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -336,6 +336,27 @@ async function main() { const patchPath = "/tmp/gh-aw/aw.patch"; const hasPatch = fs.existsSync(patchPath); core.info(`Patch file ${hasPatch ? "exists" : "does not exist"} at: ${patchPath}`); - core.setOutput("has_patch", hasPatch ? "true" : "false"); + + // Check if allow-empty is enabled for create_pull_request (reuse already loaded config) + let allowEmptyPR = false; + if (safeOutputsConfig) { + // Check if create-pull-request has allow-empty enabled + if ( + safeOutputsConfig["create-pull-request"]?.["allow-empty"] === true || + safeOutputsConfig["create_pull_request"]?.["allow_empty"] === true + ) { + allowEmptyPR = true; + core.info(`allow-empty is enabled for create-pull-request`); + } + } + + // If allow-empty is enabled for create_pull_request and there's no patch, that's OK + // Set has_patch to true so the create_pull_request job will run + if (allowEmptyPR && !hasPatch && outputTypes.includes("create_pull_request")) { + core.info(`allow-empty is enabled and no patch exists - will create empty PR`); + core.setOutput("has_patch", "true"); + } else { + core.setOutput("has_patch", hasPatch ? "true" : "false"); + } } await main(); diff --git a/pkg/workflow/js/create_pull_request.cjs b/pkg/workflow/js/create_pull_request.cjs index 3f4fcf58b5..c4d2f286d9 100644 --- a/pkg/workflow/js/create_pull_request.cjs +++ b/pkg/workflow/js/create_pull_request.cjs @@ -82,71 +82,89 @@ async function main() { } const ifNoChanges = process.env.GH_AW_PR_IF_NO_CHANGES || "warn"; + const allowEmpty = (process.env.GH_AW_PR_ALLOW_EMPTY || "false").toLowerCase() === "true"; // Check if patch file exists and has valid content if (!fs.existsSync("/tmp/gh-aw/aw.patch")) { - const message = "No patch file found - cannot create pull request without changes"; - - // If in staged mode, still show preview - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ No patch file found\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - - // Write to step summary - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (no patch file)"); - return; - } + // If allow-empty is enabled, we can proceed without a patch file + if (allowEmpty) { + core.info("No patch file found, but allow-empty is enabled - will create empty PR"); + } else { + const message = "No patch file found - cannot create pull request without changes"; - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - // Silent success - no console output - return; - case "warn": - default: - core.warning(message); + // If in staged mode, still show preview + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ No patch file found\n\n`; + summaryContent += `**Message:** ${message}\n\n`; + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (no patch file)"); return; + } + + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + // Silent success - no console output + return; + case "warn": + default: + core.warning(message); + return; + } } } - const patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + let patchContent = ""; + let isEmpty = true; + + if (fs.existsSync("/tmp/gh-aw/aw.patch")) { + patchContent = fs.readFileSync("/tmp/gh-aw/aw.patch", "utf8"); + isEmpty = !patchContent || !patchContent.trim(); + } // Check for actual error conditions (but allow empty patches as valid noop) if (patchContent.includes("Failed to generate patch")) { - const message = "Patch file contains error message - cannot create pull request without changes"; - - // If in staged mode, still show preview - if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; - summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; - summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; - summaryContent += `**Message:** ${message}\n\n`; - - // Write to step summary - await core.summary.addRaw(summaryContent).write(); - core.info("📝 Pull request creation preview written to step summary (patch error)"); - return; - } + // If allow-empty is enabled, ignore patch errors and proceed + if (allowEmpty) { + core.info("Patch file contains error, but allow-empty is enabled - will create empty PR"); + patchContent = ""; + isEmpty = true; + } else { + const message = "Patch file contains error message - cannot create pull request without changes"; + + // If in staged mode, still show preview + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file contains error\n\n`; + summaryContent += `**Message:** ${message}\n\n`; - switch (ifNoChanges) { - case "error": - throw new Error(message); - case "ignore": - // Silent success - no console output - return; - case "warn": - default: - core.warning(message); + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (patch error)"); return; + } + + switch (ifNoChanges) { + case "error": + throw new Error(message); + case "ignore": + // Silent success - no console output + return; + case "warn": + default: + core.warning(message); + return; + } } } // Validate patch size (unless empty) - const isEmpty = !patchContent || !patchContent.trim(); if (!isEmpty) { // Get maximum patch size from environment (default: 1MB = 1024 KB) const maxSizeKb = parseInt(process.env.GH_AW_MAX_PATCH_SIZE || "1024", 10); @@ -176,7 +194,8 @@ async function main() { core.info("Patch size validation passed"); } - if (isEmpty && !isStaged) { + + if (isEmpty && !isStaged && !allowEmpty) { const message = "Patch file is empty - no changes to apply (noop operation)"; switch (ifNoChanges) { @@ -195,6 +214,8 @@ async function main() { core.info(`Agent output content length: ${outputContent.length}`); if (!isEmpty) { core.info("Patch content validation passed"); + } else if (allowEmpty) { + core.info("Patch file is empty - processing empty PR creation (allow-empty is enabled)"); } else { core.info("Patch file is empty - processing noop operation"); } @@ -487,19 +508,53 @@ ${patchPreview}`; } else { core.info("Skipping patch application (empty patch)"); - // For empty patches, handle if-no-changes configuration - const message = "No changes to apply - noop operation completed successfully"; + // For empty patches with allow-empty, we still need to push the branch + if (allowEmpty) { + core.info("allow-empty is enabled - will create branch and push without changes"); + // Push the branch without any changes + try { + // Check if remote branch already exists (optional precheck) + let remoteBranchExists = false; + try { + const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`); + if (stdout.trim()) { + remoteBranchExists = true; + } + } catch (checkError) { + core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`); + } - switch (ifNoChanges) { - case "error": - throw new Error("No changes to apply - failing as configured by if-no-changes: error"); - case "ignore": - // Silent success - no console output - return; - case "warn": - default: - core.warning(message); + if (remoteBranchExists) { + core.warning(`Remote branch ${branchName} already exists - appending random suffix`); + const extraHex = crypto.randomBytes(4).toString("hex"); + const oldBranch = branchName; + branchName = `${branchName}-${extraHex}`; + // Rename local branch + await exec.exec(`git branch -m ${oldBranch} ${branchName}`); + core.info(`Renamed branch to ${branchName}`); + } + + await exec.exec(`git push origin ${branchName}`); + core.info("Empty branch pushed successfully"); + } catch (pushError) { + core.setFailed(`Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`); return; + } + } else { + // For empty patches without allow-empty, handle if-no-changes configuration + const message = "No changes to apply - noop operation completed successfully"; + + switch (ifNoChanges) { + case "error": + throw new Error("No changes to apply - failing as configured by if-no-changes: error"); + case "ignore": + // Silent success - no console output + return; + case "warn": + default: + core.warning(message); + return; + } } } diff --git a/pkg/workflow/js/safe_outputs_handlers.cjs b/pkg/workflow/js/safe_outputs_handlers.cjs index 517de6e7a2..b7fb027ffd 100644 --- a/pkg/workflow/js/safe_outputs_handlers.cjs +++ b/pkg/workflow/js/safe_outputs_handlers.cjs @@ -15,9 +15,10 @@ const { generateGitPatch } = require("./generate_git_patch.cjs"); * Create handlers for safe output tools * @param {Object} server - The MCP server instance for logging * @param {Function} appendSafeOutput - Function to append entries to the output file + * @param {Object} [config] - Optional configuration object with safe output settings * @returns {Object} An object containing all handler functions */ -function createHandlers(server, appendSafeOutput) { +function createHandlers(server, appendSafeOutput, config = {}) { /** * Default handler for safe output tools * @param {string} type - The tool type @@ -185,7 +186,7 @@ function createHandlers(server, appendSafeOutput) { /** * Handler for create_pull_request tool * Resolves the current branch if branch is not provided or is the base branch - * Generates git patch for the changes + * Generates git patch for the changes (unless allow-empty is true) */ const createPullRequestHandler = args => { const entry = { ...args, type: "create_pull_request" }; @@ -205,6 +206,27 @@ function createHandlers(server, appendSafeOutput) { entry.branch = detectedBranch; } + // Check if allow-empty is enabled in configuration + const allowEmpty = config.create_pull_request?.allow_empty === true; + + if (allowEmpty) { + server.debug(`allow-empty is enabled for create_pull_request - skipping patch generation`); + // Append the safe output entry without generating a patch + appendSafeOutput(entry); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + message: "Pull request prepared (allow-empty mode - no patch generated)", + branch: entry.branch, + }), + }, + ], + }; + } + // Generate git patch server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`); const patchResult = generateGitPatch(entry.branch); diff --git a/pkg/workflow/js/safe_outputs_mcp_server.cjs b/pkg/workflow/js/safe_outputs_mcp_server.cjs index 2d0db87bd4..0abd29c73a 100644 --- a/pkg/workflow/js/safe_outputs_mcp_server.cjs +++ b/pkg/workflow/js/safe_outputs_mcp_server.cjs @@ -38,8 +38,8 @@ function startSafeOutputsServer(options = {}) { // Create append function const appendSafeOutput = createAppendFunction(outputFile); - // Create handlers - const handlers = createHandlers(server, appendSafeOutput); + // Create handlers with configuration + const handlers = createHandlers(server, appendSafeOutput, safeOutputsConfig); const { defaultHandler } = handlers; // Attach handlers to tools diff --git a/pkg/workflow/safe_outputs.go b/pkg/workflow/safe_outputs.go index 189a0f0f60..f6ad9bcae8 100644 --- a/pkg/workflow/safe_outputs.go +++ b/pkg/workflow/safe_outputs.go @@ -1003,6 +1003,10 @@ func generateSafeOutputsConfig(data *WorkflowData) string { if len(data.SafeOutputs.CreatePullRequests.AllowedLabels) > 0 { prConfig["allowed_labels"] = data.SafeOutputs.CreatePullRequests.AllowedLabels } + // Pass allow_empty flag to MCP server so it can skip patch generation + if data.SafeOutputs.CreatePullRequests.AllowEmpty { + prConfig["allow_empty"] = true + } safeOutputsConfig["create_pull_request"] = prConfig } if data.SafeOutputs.CreatePullRequestReviewComments != nil {