diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index bc9c59e2a4f..73b8f358d13 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -12,7 +12,7 @@ const { replaceTemporaryIdReferences, loadTemporaryIdMapFromResolved, resolveRep const { getTrackerID } = require("./get_tracker_id.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); -const { resolveTarget } = require("./safe_output_helpers.cjs"); +const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { getMissingInfoSections } = require("./missing_messages_helper.cjs"); @@ -312,7 +312,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); // Check if append-only-comments is enabled in messages config const messagesConfig = getMessages(); diff --git a/actions/setup/js/add_labels.cjs b/actions/setup/js/add_labels.cjs index 06fab518bf6..13d65a64233 100644 --- a/actions/setup/js/add_labels.cjs +++ b/actions/setup/js/add_labels.cjs @@ -13,6 +13,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveRepoIssueTarget, loadTemporaryIdMapFromResolved } = require("./temporary_id.cjs"); const { MAX_LABELS } = require("./constants.cjs"); @@ -31,7 +32,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Add labels configuration: max=${maxCount}`); if (allowedLabels.length > 0) { diff --git a/actions/setup/js/add_reviewer.cjs b/actions/setup/js/add_reviewer.cjs index 982a75ab87e..e26c8b6d68b 100644 --- a/actions/setup/js/add_reviewer.cjs +++ b/actions/setup/js/add_reviewer.cjs @@ -9,6 +9,7 @@ const { processItems } = require("./safe_output_processor.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { getPullRequestNumber } = require("./pr_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { COPILOT_REVIEWER_BOT } = require("./constants.cjs"); @@ -24,7 +25,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Add reviewer configuration: max=${maxCount}`); if (allowedReviewers.length > 0) { diff --git a/actions/setup/js/assign_milestone.cjs b/actions/setup/js/assign_milestone.cjs index f35178b07a9..db6ce579772 100644 --- a/actions/setup/js/assign_milestone.cjs +++ b/actions/setup/js/assign_milestone.cjs @@ -7,6 +7,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { loadTemporaryIdMapFromResolved, resolveRepoIssueTarget } = require("./temporary_id.cjs"); @@ -25,7 +26,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Assign milestone configuration: max=${maxCount}`); if (allowedMilestones.length > 0) { diff --git a/actions/setup/js/assign_to_user.cjs b/actions/setup/js/assign_to_user.cjs index 55475ce6c65..9ad6c89e167 100644 --- a/actions/setup/js/assign_to_user.cjs +++ b/actions/setup/js/assign_to_user.cjs @@ -8,7 +8,7 @@ const { processItems } = require("./safe_output_processor.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); -const { resolveIssueNumber, extractAssignees } = require("./safe_output_helpers.cjs"); +const { resolveIssueNumber, extractAssignees, isStagedMode } = require("./safe_output_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); @@ -31,7 +31,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Assign to user configuration: max=${maxCount}, unassign_first=${unassignFirst}`); if (allowedAssignees.length > 0) { diff --git a/actions/setup/js/autofix_code_scanning_alert.cjs b/actions/setup/js/autofix_code_scanning_alert.cjs index 880237bc2e4..7dd5249c92a 100644 --- a/actions/setup/js/autofix_code_scanning_alert.cjs +++ b/actions/setup/js/autofix_code_scanning_alert.cjs @@ -3,6 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -19,7 +20,7 @@ const HANDLER_TYPE = "autofix_code_scanning_alert"; async function main(config = {}) { // Extract configuration const maxCount = config.max || 10; - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Add code scanning autofix configuration: max=${maxCount}`); if (isStaged) logStagedPreviewInfo("no changes will be written"); diff --git a/actions/setup/js/call_workflow.cjs b/actions/setup/js/call_workflow.cjs index 3cb20a15ef4..8d550dd6ce0 100644 --- a/actions/setup/js/call_workflow.cjs +++ b/actions/setup/js/call_workflow.cjs @@ -9,6 +9,8 @@ const HANDLER_TYPE = "call_workflow"; const { getErrorMessage } = require("./error_helpers.cjs"); +const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * Main handler factory for call_workflow. @@ -31,6 +33,7 @@ async function main(config = {}) { // Track how many items we've processed for max limit let processedCount = 0; + const isStaged = isStagedMode(config); /** * Message handler function that processes a single call_workflow message. @@ -85,6 +88,17 @@ async function main(config = {}) { const inputs = message.inputs && typeof message.inputs === "object" ? message.inputs : {}; const payloadJson = JSON.stringify(inputs); + // If in staged mode, preview the workflow call without executing it + if (isStaged) { + logStagedPreviewInfo(`Would call workflow: ${workflowName} with payload: ${payloadJson}`); + return { + success: true, + staged: true, + workflow_name: workflowName, + payload: payloadJson, + }; + } + // Set the step outputs that the conditional `uses:` jobs check core.setOutput("call_workflow_name", workflowName); core.setOutput("call_workflow_payload", payloadJson); diff --git a/actions/setup/js/close_discussion.cjs b/actions/setup/js/close_discussion.cjs index fa83e0f39bc..6c4664d5a7f 100644 --- a/actions/setup/js/close_discussion.cjs +++ b/actions/setup/js/close_discussion.cjs @@ -8,6 +8,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { ERR_NOT_FOUND } = require("./error_codes.cjs"); @@ -163,7 +164,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Close discussion configuration: max=${maxCount}`); if (requiredLabels.length > 0) { diff --git a/actions/setup/js/close_entity_helpers.cjs b/actions/setup/js/close_entity_helpers.cjs index 5591e1675ec..5de4a8e9184 100644 --- a/actions/setup/js/close_entity_helpers.cjs +++ b/actions/setup/js/close_entity_helpers.cjs @@ -8,6 +8,7 @@ const { getRepositoryUrl } = require("./get_repository_url.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * @typedef {'issue' | 'pull_request'} EntityType @@ -212,7 +213,7 @@ function escapeMarkdownTitle(title) { */ async function processCloseEntityItems(config, callbacks, handlerConfig = {}) { // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(handlerConfig); const result = loadAgentOutput(); if (!result.success) { diff --git a/actions/setup/js/close_issue.cjs b/actions/setup/js/close_issue.cjs index e400286801a..4c765d057c2 100644 --- a/actions/setup/js/close_issue.cjs +++ b/actions/setup/js/close_issue.cjs @@ -9,6 +9,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { ERR_NOT_FOUND } = require("./error_codes.cjs"); @@ -91,7 +92,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Close issue configuration: max=${maxCount}, state_reason=${configStateReason}`); if (requiredLabels.length > 0) { diff --git a/actions/setup/js/close_pull_request.cjs b/actions/setup/js/close_pull_request.cjs index 6309fe4835c..2483993cfb6 100644 --- a/actions/setup/js/close_pull_request.cjs +++ b/actions/setup/js/close_pull_request.cjs @@ -6,6 +6,7 @@ const { getTrackerID } = require("./get_tracker_id.cjs"); const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { ERR_NOT_FOUND } = require("./error_codes.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); @@ -90,7 +91,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode (either globally or per-handler config) - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true" || config.staged === true; + const isStaged = isStagedMode(config); core.info(`Close pull request configuration: max=${maxCount}`); if (requiredLabels.length > 0) { diff --git a/actions/setup/js/create_code_scanning_alert.cjs b/actions/setup/js/create_code_scanning_alert.cjs index fd3e7771600..63b52ad6a0c 100644 --- a/actions/setup/js/create_code_scanning_alert.cjs +++ b/actions/setup/js/create_code_scanning_alert.cjs @@ -6,6 +6,8 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); +const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const fs = require("fs"); const path = require("path"); @@ -29,6 +31,7 @@ async function main(config = {}) { // Track how many items we've processed for max limit let processedCount = 0; + const isStaged = isStagedMode(config); // Collect valid findings across all messages const validFindings = []; @@ -235,6 +238,17 @@ async function main(config = {}) { core.info(`Added security finding ${validFindings.length}: ${finding.severity} in ${finding.file}:${finding.line}`); + // If in staged mode, preview the finding without writing the SARIF file + if (isStaged) { + logStagedPreviewInfo(`Would create code scanning alert: ${finding.severity} in ${finding.file}:${finding.line} - ${finding.message}`); + return { + success: true, + staged: true, + finding: finding, + findingsCount: validFindings.length, + }; + } + // Generate/update SARIF file after each finding try { generateSarifFile(); diff --git a/actions/setup/js/create_discussion.cjs b/actions/setup/js/create_discussion.cjs index 9cae18d34bd..dad7439f262 100644 --- a/actions/setup/js/create_discussion.cjs +++ b/actions/setup/js/create_discussion.cjs @@ -20,6 +20,7 @@ const { generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKey const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { closeOlderDiscussions: closeOlderDiscussionsFunc } = require("./close_older_discussions.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); @@ -318,7 +319,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); // Parse labels from config const labelsConfig = config.labels || []; diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 593ba448731..5aa8862b65a 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -43,6 +43,7 @@ const { closeOlderIssues } = require("./close_older_issues.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); const { MAX_LABELS, MAX_ASSIGNEES } = require("./constants.cjs"); @@ -218,7 +219,7 @@ async function main(config = {}) { const assignCopilot = process.env.GH_AW_ASSIGN_COPILOT === "true"; // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Default target repo: ${defaultTargetRepo}`); if (allowedRepos.size > 0) { diff --git a/actions/setup/js/create_pr_review_comment.cjs b/actions/setup/js/create_pr_review_comment.cjs index 401a058b4c2..8384ec1d7e5 100644 --- a/actions/setup/js/create_pr_review_comment.cjs +++ b/actions/setup/js/create_pr_review_comment.cjs @@ -46,6 +46,11 @@ async function main(config = {}) { core.info(`Allowed repos: ${Array.from(allowedRepos).join(", ")}`); } + // Propagate per-handler staged flag to the shared PR review buffer + if (config.staged === true) { + buffer.setStaged(true); + } + // Track how many items we've processed for max limit let processedCount = 0; diff --git a/actions/setup/js/create_project.cjs b/actions/setup/js/create_project.cjs index 749ca6243c5..5b3803e05be 100644 --- a/actions/setup/js/create_project.cjs +++ b/actions/setup/js/create_project.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { normalizeTemporaryId, isTemporaryId, generateTemporaryId, getOrGenerateTemporaryId } = require("./temporary_id.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { ERR_CONFIG, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); /** @@ -298,7 +299,7 @@ async function main(config = {}, githubClient = null) { const configuredViews = Array.isArray(config.views) ? config.views : []; // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); // Use the provided github client, or fall back to the global github object // The global github object is available when running via github-script action diff --git a/actions/setup/js/create_project_status_update.cjs b/actions/setup/js/create_project_status_update.cjs index b241020411e..10305296bbc 100644 --- a/actions/setup/js/create_project_status_update.cjs +++ b/actions/setup/js/create_project_status_update.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { isTemporaryId, normalizeTemporaryId } = require("./temporary_id.cjs"); const { ERR_CONFIG, ERR_NOT_FOUND, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); @@ -288,7 +289,7 @@ async function main(config = {}, githubClient = null) { const maxCount = config.max || 10; // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); // Use the provided github client, or fall back to the global github object // @ts-ignore - global.github is set by setupGlobals() from github-script context diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 1b04c751a61..981db2d394b 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -27,6 +27,7 @@ const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); const { checkFileProtection } = require("./manifest_file_helpers.cjs"); const { renderTemplateFromFile } = require("./messages_core.cjs"); const { COPILOT_REVIEWER_BOT, FAQ_CREATE_PR_PERMISSIONS_URL } = require("./constants.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -160,7 +161,7 @@ async function main(config = {}) { const triggeringIssueNumber = typeof context !== "undefined" && context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Base branch: ${configBaseBranch || "(dynamic - resolved per target repo)"}`); core.info(`Default target repo: ${defaultTargetRepo}`); diff --git a/actions/setup/js/dispatch_workflow.cjs b/actions/setup/js/dispatch_workflow.cjs index 757a257506a..4e4137c4f6b 100644 --- a/actions/setup/js/dispatch_workflow.cjs +++ b/actions/setup/js/dispatch_workflow.cjs @@ -11,6 +11,8 @@ const HANDLER_TYPE = "dispatch_workflow"; const { getErrorMessage } = require("./error_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveTargetRepoConfig, parseRepoSlug, validateTargetRepo } = require("./repo_helpers.cjs"); +const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * Main handler factory for dispatch_workflow @@ -73,6 +75,7 @@ async function main(config = {}) { // Track how many items we've processed for max limit let processedCount = 0; let lastDispatchTime = 0; + const isStaged = isStagedMode(config); // Helper function to get the default branch of the dispatch target repository const getDefaultBranchRef = async () => { @@ -197,6 +200,17 @@ async function main(config = {}) { const workflowFile = `${workflowName}${extension}`; core.info(`Dispatching workflow: ${workflowFile}`); + // If in staged mode, preview the dispatch without executing it + if (isStaged) { + logStagedPreviewInfo(`Would dispatch workflow: ${workflowFile} in ${resolvedRepoSlug} with ref: ${ref}`); + return { + success: true, + staged: true, + workflow_name: workflowName, + inputs: inputs, + }; + } + // Dispatch the workflow using the resolved file. // Request return_run_details for newer GitHub API support; fall back without it // for older GitHub Enterprise Server deployments that don't support the parameter. diff --git a/actions/setup/js/hide_comment.cjs b/actions/setup/js/hide_comment.cjs index b98edb077c9..e93e280ee21 100644 --- a/actions/setup/js/hide_comment.cjs +++ b/actions/setup/js/hide_comment.cjs @@ -7,6 +7,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); /** @@ -52,7 +53,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Hide comment configuration: max=${maxCount}`); if (allowedReasons.length > 0) { diff --git a/actions/setup/js/link_sub_issue.cjs b/actions/setup/js/link_sub_issue.cjs index 5fd129b3a58..439e8bf5b7a 100644 --- a/actions/setup/js/link_sub_issue.cjs +++ b/actions/setup/js/link_sub_issue.cjs @@ -4,6 +4,7 @@ const { loadTemporaryIdMapFromResolved, resolveRepoIssueTarget } = require("./temporary_id.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); /** @@ -22,7 +23,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); if (parentRequiredLabels.length > 0) { core.info(`Parent required labels: ${JSON.stringify(parentRequiredLabels)}`); diff --git a/actions/setup/js/mark_pull_request_as_ready_for_review.cjs b/actions/setup/js/mark_pull_request_as_ready_for_review.cjs index 0ac07e03c29..74f5c3aff91 100644 --- a/actions/setup/js/mark_pull_request_as_ready_for_review.cjs +++ b/actions/setup/js/mark_pull_request_as_ready_for_review.cjs @@ -9,6 +9,7 @@ const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { ERR_NOT_FOUND } = require("./error_codes.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); @@ -88,7 +89,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Mark pull request as ready for review configuration: max=${maxCount}`); diff --git a/actions/setup/js/noop.cjs b/actions/setup/js/noop.cjs index 6bcba542d2f..25918613bc8 100644 --- a/actions/setup/js/noop.cjs +++ b/actions/setup/js/noop.cjs @@ -2,6 +2,7 @@ /// const { loadAgentOutput } = require("./load_agent_output.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * Main function to handle noop safe output @@ -10,7 +11,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); */ async function main() { // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(); const result = loadAgentOutput(); if (!result.success) { diff --git a/actions/setup/js/pr_review_buffer.cjs b/actions/setup/js/pr_review_buffer.cjs index e208d705c63..a848407fc2d 100644 --- a/actions/setup/js/pr_review_buffer.cjs +++ b/actions/setup/js/pr_review_buffer.cjs @@ -21,6 +21,7 @@ const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); /** * @typedef {Object} BufferedComment @@ -68,6 +69,8 @@ function createReviewBuffer() { /** @type {string} Footer mode: "always" (default), "none", or "if-body" */ let footerMode = "always"; + /** @type {boolean} Staged mode: when true, preview review without submitting (set via setStaged(), reset on buffer clear) */ + let stagedMode = false; /** * Add a validated comment to the buffer. * Rejects comments targeting a different repo/PR than the first comment. @@ -157,6 +160,18 @@ function createReviewBuffer() { } } + /** + * Set staged mode for the review buffer. + * When staged, submitReview() will preview the review without actually submitting. + * @param {boolean} value - Whether staged mode is enabled + */ + function setStaged(value) { + stagedMode = value; + if (value) { + core.info("PR review buffer staged mode enabled"); + } + } + /** * Check if there are buffered comments to submit. * @returns {boolean} @@ -261,7 +276,7 @@ function createReviewBuffer() { core.info(`Submitting PR review on ${repo}#${pullRequestNumber}: event=${event}, comments=${comments.length}, bodyLength=${body.length}`); // If in staged mode, preview the review without submitting - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode({ staged: stagedMode }); if (isStaged) { let summaryContent = "## 🎭 Staged Mode: PR Review Preview\n\n"; summaryContent += "The following PR review would be submitted if staged mode was disabled:\n\n"; @@ -396,6 +411,7 @@ function createReviewBuffer() { reviewContext = null; footerContext = null; footerMode = "always"; + stagedMode = false; } return { @@ -406,6 +422,7 @@ function createReviewBuffer() { setFooterContext, setFooterMode, setIncludeFooter: setFooterMode, // Backward compatibility alias + setStaged, hasBufferedComments, hasReviewMetadata, getBufferedCount, diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 292cea76b76..3d0a4db8932 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -4,6 +4,7 @@ /** @type {typeof import("fs")} */ const fs = require("fs"); const { generateStagedPreview } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); const { updateActivationCommentWithCommit, updateActivationComment } = require("./update_activation_comment.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); @@ -56,7 +57,7 @@ async function main(config = {}) { const configBaseBranch = config.base_branch || null; // Check if we're in staged mode (either globally or per-handler config) - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true" || config.staged === true; + const isStaged = isStagedMode(config); core.info(`Target: ${target}`); if (configBaseBranch) { diff --git a/actions/setup/js/remove_labels.cjs b/actions/setup/js/remove_labels.cjs index 6dc7cc518ab..e6a9a4f1259 100644 --- a/actions/setup/js/remove_labels.cjs +++ b/actions/setup/js/remove_labels.cjs @@ -12,6 +12,7 @@ const { validateLabels } = require("./safe_output_validator.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveRepoIssueTarget, loadTemporaryIdMapFromResolved } = require("./temporary_id.cjs"); @@ -29,7 +30,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Remove labels configuration: max=${maxCount}`); if (allowedLabels.length > 0) { diff --git a/actions/setup/js/reply_to_pr_review_comment.cjs b/actions/setup/js/reply_to_pr_review_comment.cjs index ef668e34f3c..25daf5768fc 100644 --- a/actions/setup/js/reply_to_pr_review_comment.cjs +++ b/actions/setup/js/reply_to_pr_review_comment.cjs @@ -11,6 +11,7 @@ const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { getPRNumber } = require("./update_context_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); @@ -34,7 +35,7 @@ async function main(config = {}) { const maxCount = config.max || 10; const replyTarget = config.target || "triggering"; const includeFooter = parseBoolTemplatable(config.footer, true); - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); const githubClient = await createAuthenticatedGitHubClient(config); diff --git a/actions/setup/js/resolve_pr_review_thread.cjs b/actions/setup/js/resolve_pr_review_thread.cjs index de2544d40f0..0f388a3b2df 100644 --- a/actions/setup/js/resolve_pr_review_thread.cjs +++ b/actions/setup/js/resolve_pr_review_thread.cjs @@ -8,6 +8,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { getPRNumber } = require("./update_context_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveTargetRepoConfig, validateTargetRepo } = require("./repo_helpers.cjs"); @@ -103,7 +104,7 @@ async function main(config = {}) { const triggeringPRNumber = getPRNumber(context.payload); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Resolve PR review thread configuration: max=${maxCount}, target=${resolveTarget}, triggeringPR=${triggeringPRNumber || "none"}`); core.info(`Default target repo: ${defaultTargetRepo}`); diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index 41e9d48a656..16684f2b42e 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -20,7 +20,7 @@ const { getIssuesToAssignCopilot } = require("./create_issue.cjs"); const { createReviewBuffer } = require("./pr_review_buffer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { createManifestLogger, ensureManifestExists, extractCreatedItemFromResult } = require("./safe_output_manifest.cjs"); -const { loadCustomSafeOutputJobTypes, loadCustomSafeOutputScriptHandlers, loadCustomSafeOutputActionHandlers } = require("./safe_output_helpers.cjs"); +const { loadCustomSafeOutputJobTypes, loadCustomSafeOutputScriptHandlers, loadCustomSafeOutputActionHandlers, isStagedMode } = require("./safe_output_helpers.cjs"); const { emitSafeOutputActionOutputs } = require("./safe_outputs_action_outputs.cjs"); const nodePath = require("path"); @@ -960,7 +960,7 @@ async function processSyntheticUpdates(github, context, trackedOutputs, temporar async function main() { // Detect staged mode before try/finally so it's accessible in the finally block. // In staged mode (🎭 Staged Mode Preview) no real items are created in GitHub so no manifest should be emitted. - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(); try { core.info("Safe Output Handler Manager starting..."); diff --git a/actions/setup/js/safe_output_helpers.cjs b/actions/setup/js/safe_output_helpers.cjs index 47967926ee8..42dbbb1fb94 100644 --- a/actions/setup/js/safe_output_helpers.cjs +++ b/actions/setup/js/safe_output_helpers.cjs @@ -393,6 +393,18 @@ function loadCustomSafeOutputActionHandlers() { } } +/** + * Returns true when the current execution is in staged mode. + * Staged mode is active when either the global GH_AW_SAFE_OUTPUTS_STAGED + * environment variable is "true" or when the per-handler config has staged: true. + * Use this helper in all handlers to ensure consistent staged mode detection. + * @param {Object} [config] - Handler configuration object (may have staged: true) + * @returns {boolean} + */ +function isStagedMode(config) { + return process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true" || (config != null && config.staged === true); +} + module.exports = { parseAllowedItems, parseMaxCount, @@ -404,4 +416,5 @@ module.exports = { extractAssignees, matchesBlockedPattern, isUsernameBlocked, + isStagedMode, }; diff --git a/actions/setup/js/set_issue_type.cjs b/actions/setup/js/set_issue_type.cjs index db0ac3ee85b..3e802f1059e 100644 --- a/actions/setup/js/set_issue_type.cjs +++ b/actions/setup/js/set_issue_type.cjs @@ -8,6 +8,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { loadTemporaryIdMapFromResolved, resolveRepoIssueTarget } = require("./temporary_id.cjs"); @@ -99,7 +100,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Set issue type configuration: max=${maxCount}`); if (allowedTypes.length > 0) { diff --git a/actions/setup/js/submit_pr_review.cjs b/actions/setup/js/submit_pr_review.cjs index 70eaa1cca32..547e632b7f9 100644 --- a/actions/setup/js/submit_pr_review.cjs +++ b/actions/setup/js/submit_pr_review.cjs @@ -46,6 +46,11 @@ async function main(config = {}) { core.info(`Allowed repos: ${Array.from(allowedRepos).join(", ")}`); } + // Propagate per-handler staged flag to the shared PR review buffer + if (config.staged === true) { + buffer.setStaged(true); + } + let processedCount = 0; /** diff --git a/actions/setup/js/unassign_from_user.cjs b/actions/setup/js/unassign_from_user.cjs index 0e92fdf83dc..de4a6c9fb2c 100644 --- a/actions/setup/js/unassign_from_user.cjs +++ b/actions/setup/js/unassign_from_user.cjs @@ -8,7 +8,7 @@ const { processItems } = require("./safe_output_processor.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); -const { resolveIssueNumber, extractAssignees } = require("./safe_output_helpers.cjs"); +const { resolveIssueNumber, extractAssignees, isStagedMode } = require("./safe_output_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); @@ -31,7 +31,7 @@ async function main(config = {}) { const githubClient = await createAuthenticatedGitHubClient(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); core.info(`Unassign from user configuration: max=${maxCount}`); if (allowedAssignees.length > 0) { diff --git a/actions/setup/js/update_handler_factory.cjs b/actions/setup/js/update_handler_factory.cjs index 70065137c54..25cc21f54f7 100644 --- a/actions/setup/js/update_handler_factory.cjs +++ b/actions/setup/js/update_handler_factory.cjs @@ -6,7 +6,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); -const { resolveTarget } = require("./safe_output_helpers.cjs"); +const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); @@ -113,7 +113,7 @@ function createUpdateHandlerFactory(handlerConfig) { const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); // Build configuration log message const configParts = [`max=${maxCount}`, `target=${updateTarget}`]; diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 34a467aaf53..1bd8090e35e 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { loadTemporaryIdMapFromResolved, resolveIssueNumber, isTemporaryId, normalizeTemporaryId } = require("./temporary_id.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); const { parseRepoSlug, resolveTargetRepoConfig, isRepoAllowed } = require("./repo_helpers.cjs"); @@ -1237,7 +1238,7 @@ async function main(config = {}, githubClient = null) { const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); if (configuredViews.length > 0) { core.info(`Found ${configuredViews.length} configured view(s) in frontmatter`); diff --git a/actions/setup/js/update_release.cjs b/actions/setup/js/update_release.cjs index c63ccc10aff..3e32193ff90 100644 --- a/actions/setup/js/update_release.cjs +++ b/actions/setup/js/update_release.cjs @@ -11,6 +11,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { updateBody } = require("./update_pr_description_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { isStagedMode } = require("./safe_output_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); @@ -28,7 +29,7 @@ const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); */ async function main(config = {}) { // Check if we're in staged mode - const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; + const isStaged = isStagedMode(config); const workflowName = process.env.GH_AW_WORKFLOW_NAME || "GitHub Agentic Workflow"; const includeFooter = parseBoolTemplatable(config.footer, true); const githubClient = await createAuthenticatedGitHubClient(config); diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index 0cd2db0da76..2d9edc6fed9 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -152,6 +152,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("close_older_key", c.CloseOlderKey). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "add_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -167,6 +168,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). Build() }, "create_discussion": func(cfg *SafeOutputsConfig) map[string]any { @@ -189,6 +191,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "close_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -204,6 +207,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("state_reason", c.StateReason). + AddIfTrue("staged", c.Staged). Build() }, "close_discussion": func(cfg *SafeOutputsConfig) map[string]any { @@ -218,6 +222,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfTrue("staged", c.Staged). Build() }, "add_labels": func(cfg *SafeOutputsConfig) map[string]any { @@ -233,6 +238,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() // If config is empty, it means add_labels was explicitly configured with no options // (null config), which means "allow any labels". Return non-nil empty map to @@ -256,6 +262,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "add_reviewer": func(cfg *SafeOutputsConfig) map[string]any { @@ -270,6 +277,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "assign_milestone": func(cfg *SafeOutputsConfig) map[string]any { @@ -284,6 +292,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "mark_pull_request_as_ready_for_review": func(cfg *SafeOutputsConfig) map[string]any { @@ -299,6 +308,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "create_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { @@ -312,6 +322,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "create_agent_session": func(cfg *SafeOutputsConfig) map[string]any { @@ -324,6 +335,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("base", c.Base). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfTrue("staged", c.Staged). Build() }, "update_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -349,6 +361,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). Build() }, "update_discussion": func(cfg *SafeOutputsConfig) map[string]any { @@ -375,6 +388,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). Build() }, "link_sub_issue": func(cfg *SafeOutputsConfig) map[string]any { @@ -391,6 +405,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "update_release": func(cfg *SafeOutputsConfig) map[string]any { @@ -402,6 +417,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddTemplatableInt("max", c.Max). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). Build() }, "create_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -416,6 +432,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "submit_pull_request_review": func(cfg *SafeOutputsConfig) map[string]any { @@ -430,6 +447,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). AddStringPtr("footer", getEffectiveFooterString(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). Build() }, "reply_to_pull_request_review_comment": func(cfg *SafeOutputsConfig) map[string]any { @@ -444,6 +462,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("footer", getEffectiveFooterForTemplatable(c.Footer, cfg.Footer)). + AddIfTrue("staged", c.Staged). Build() }, "resolve_pull_request_review_thread": func(cfg *SafeOutputsConfig) map[string]any { @@ -457,6 +476,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "create_pull_request": func(cfg *SafeOutputsConfig) map[string]any { @@ -490,7 +510,8 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("protected_path_prefixes", getProtectedPathPrefixes()). AddStringSlice("allowed_files", c.AllowedFiles). AddStringSlice("excluded_files", c.ExcludedFiles). - AddIfTrue("preserve_branch_name", c.PreserveBranchName) + AddIfTrue("preserve_branch_name", c.PreserveBranchName). + AddIfTrue("staged", c.Staged) return builder.Build() }, "push_to_pull_request_branch": func(cfg *SafeOutputsConfig) map[string]any { @@ -536,6 +557,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "close_pull_request": func(cfg *SafeOutputsConfig) map[string]any { @@ -565,6 +587,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "dispatch_workflow": func(cfg *SafeOutputsConfig) map[string]any { @@ -584,6 +607,7 @@ var handlerRegistry = map[string]handlerBuilder{ builder.AddIfNotEmpty("target-ref", c.TargetRef) builder.AddIfNotEmpty("github-token", c.GitHubToken) + builder.AddIfTrue("staged", c.Staged) return builder.Build() }, "call_workflow": func(cfg *SafeOutputsConfig) map[string]any { @@ -600,6 +624,7 @@ var handlerRegistry = map[string]handlerBuilder{ builder.AddDefault("workflow_files", c.WorkflowFiles) } + builder.AddIfTrue("staged", c.Staged) return builder.Build() }, "missing_tool": func(cfg *SafeOutputsConfig) map[string]any { @@ -610,6 +635,7 @@ var handlerRegistry = map[string]handlerBuilder{ return newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "missing_data": func(cfg *SafeOutputsConfig) map[string]any { @@ -620,6 +646,7 @@ var handlerRegistry = map[string]handlerBuilder{ return newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "noop": func(cfg *SafeOutputsConfig) map[string]any { @@ -630,6 +657,7 @@ var handlerRegistry = map[string]handlerBuilder{ return newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddStringPtr("report-as-issue", c.ReportAsIssue). + AddIfTrue("staged", c.Staged). Build() }, "assign_to_agent": func(cfg *SafeOutputsConfig) map[string]any { @@ -652,6 +680,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed-pull-request-repos", c.AllowedPullRequestRepos). AddIfNotEmpty("base-branch", c.BaseBranch). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "upload_asset": func(cfg *SafeOutputsConfig) map[string]any { @@ -665,6 +694,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfPositive("max-size", c.MaxSizeKB). AddStringSlice("allowed-exts", c.AllowedExts). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "autofix_code_scanning_alert": func(cfg *SafeOutputsConfig) map[string]any { @@ -675,6 +705,7 @@ var handlerRegistry = map[string]handlerBuilder{ return newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, // Note: create_project, update_project and create_project_status_update are handled by the unified handler, @@ -695,6 +726,7 @@ var handlerRegistry = map[string]handlerBuilder{ if len(c.FieldDefinitions) > 0 { builder.AddDefault("field_definitions", c.FieldDefinitions) } + builder.AddIfTrue("staged", c.Staged) return builder.Build() }, "update_project": func(cfg *SafeOutputsConfig) map[string]any { @@ -714,6 +746,7 @@ var handlerRegistry = map[string]handlerBuilder{ if len(c.FieldDefinitions) > 0 { builder.AddDefault("field_definitions", c.FieldDefinitions) } + builder.AddIfTrue("staged", c.Staged) return builder.Build() }, "assign_to_user": func(cfg *SafeOutputsConfig) map[string]any { @@ -730,6 +763,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("unassign_first", c.UnassignFirst). + AddIfTrue("staged", c.Staged). Build() }, "unassign_from_user": func(cfg *SafeOutputsConfig) map[string]any { @@ -745,6 +779,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() }, "create_project_status_update": func(cfg *SafeOutputsConfig) map[string]any { @@ -756,6 +791,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddTemplatableInt("max", c.Max). AddIfNotEmpty("github-token", c.GitHubToken). AddIfNotEmpty("project", c.Project). + AddIfTrue("staged", c.Staged). Build() }, "set_issue_type": func(cfg *SafeOutputsConfig) map[string]any { @@ -770,6 +806,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). Build() // If config is empty, it means set_issue_type was explicitly configured with no options // (null config), which means "allow any type". Return non-nil empty map to diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index 7177035273c..ed638ccfae3 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -1721,6 +1721,113 @@ func TestHandlerConfigStagedMode(t *testing.T) { }, handlerKey: "close_pull_request", }, + { + name: "create_issue staged", + safeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "create_issue", + }, + { + name: "add_comment staged", + safeOutputs: &SafeOutputsConfig{ + AddComments: &AddCommentsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "add_comment", + }, + { + name: "create_pull_request staged", + safeOutputs: &SafeOutputsConfig{ + CreatePullRequests: &CreatePullRequestsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "create_pull_request", + }, + { + name: "update_issue staged", + safeOutputs: &SafeOutputsConfig{ + UpdateIssues: &UpdateIssuesConfig{ + UpdateEntityConfig: UpdateEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + }, + handlerKey: "update_issue", + }, + { + name: "update_pull_request staged", + safeOutputs: &SafeOutputsConfig{ + UpdatePullRequests: &UpdatePullRequestsConfig{ + UpdateEntityConfig: UpdateEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + }, + handlerKey: "update_pull_request", + }, + { + name: "update_discussion staged", + safeOutputs: &SafeOutputsConfig{ + UpdateDiscussions: &UpdateDiscussionsConfig{ + UpdateEntityConfig: UpdateEntityConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + }, + handlerKey: "update_discussion", + }, + { + name: "add_labels staged", + safeOutputs: &SafeOutputsConfig{ + AddLabels: &AddLabelsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + }, + }, + handlerKey: "add_labels", + }, + { + name: "dispatch_workflow staged", + safeOutputs: &SafeOutputsConfig{ + DispatchWorkflow: &DispatchWorkflowConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + Workflows: []string{"my-workflow"}, + }, + }, + handlerKey: "dispatch_workflow", + }, + { + name: "call_workflow staged", + safeOutputs: &SafeOutputsConfig{ + CallWorkflow: &CallWorkflowConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Staged: true, + }, + Workflows: []string{"my-workflow"}, + }, + }, + handlerKey: "call_workflow", + }, } for _, tt := range tests {