diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 16dae4a926f..45d0912261d 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -1611,6 +1611,8 @@ jobs: with: script: | async function main() { + // Check if we're in staged mode + const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; // Read the validated output content from environment variable const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT; if (!outputContent) { @@ -1646,6 +1648,28 @@ jobs: return; } console.log(`Found ${createIssueItems.length} create-issue item(s)`); + // If in staged mode, emit step summary instead of creating issues + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Issues Preview\n\n"; + summaryContent += + "The following issues would be created if staged mode was disabled:\n\n"; + for (let i = 0; i < createIssueItems.length; i++) { + const item = createIssueItems[i]; + summaryContent += `### Issue ${i + 1}\n`; + summaryContent += `**Title:** ${item.title || "No title provided"}\n\n`; + if (item.body) { + summaryContent += `**Body:**\n${item.body}\n\n`; + } + if (item.labels && item.labels.length > 0) { + summaryContent += `**Labels:** ${item.labels.join(", ")}\n\n`; + } + summaryContent += "---\n\n"; + } + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Issue creation preview written to step summary"); + return; + } // Check if we're in an issue context (triggered by an issue event) const parentIssueNumber = context.payload?.issue?.number; // Parse labels from environment variable (comma-separated string) @@ -1788,6 +1812,8 @@ jobs: with: script: | async function main() { + // Check if we're in staged mode + const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; // Read the validated output content from environment variable const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT; if (!outputContent) { @@ -1823,6 +1849,27 @@ jobs: return; } console.log(`Found ${commentItems.length} add-issue-comment item(s)`); + // If in staged mode, emit step summary instead of creating comments + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; + summaryContent += + "The following comments would be added if staged mode was disabled:\n\n"; + for (let i = 0; i < commentItems.length; i++) { + const item = commentItems[i]; + summaryContent += `### Comment ${i + 1}\n`; + if (item.issue_number) { + summaryContent += `**Target Issue:** #${item.issue_number}\n\n`; + } else { + summaryContent += `**Target:** Current issue/PR\n\n`; + } + summaryContent += `**Body:**\n${item.body || "No content provided"}\n\n`; + summaryContent += "---\n\n"; + } + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Comment creation preview written to step summary"); + return; + } // Get the target configuration from environment variable const commentTarget = process.env.GITHUB_AW_COMMENT_TARGET || "triggering"; console.log(`Comment target configuration: ${commentTarget}`); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 2c0c11d5cb2..2f0c6c8f28c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1500,6 +1500,10 @@ "description": "Enable missing tool reporting with default configuration" } ] + }, + "staged": { + "type": "boolean", + "description": "If true, emit step summary messages instead of making GitHub API calls (preview mode)" } }, "additionalProperties": false diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index 5d98d62fcc6..67997d09e64 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -88,6 +88,16 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str claudeEnv := "" if hasOutput { claudeEnv += " GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }}" + + // Add staged flag if specified + if workflowData.SafeOutputs.Staged != nil { + if *workflowData.SafeOutputs.Staged { + if claudeEnv != "" { + claudeEnv += "\n" + } + claudeEnv += " GITHUB_AW_SAFE_OUTPUTS_STAGED: \"true\"" + } + } } // Add custom environment variables from engine config diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 7409d5523d1..cc3d1ed69c9 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -116,6 +116,11 @@ codex exec \ hasOutput := workflowData.SafeOutputs != nil if hasOutput { env["GITHUB_AW_SAFE_OUTPUTS"] = "${{ env.GITHUB_AW_SAFE_OUTPUTS }}" + + // Add staged flag if specified + if workflowData.SafeOutputs.Staged != nil && *workflowData.SafeOutputs.Staged { + env["GITHUB_AW_SAFE_OUTPUTS_STAGED"] = "true" + } } // Add custom environment variables from engine config diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index d96b281fbca..f2fcaffa3df 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -164,6 +164,7 @@ type SafeOutputsConfig struct { PushToPullRequestBranch *PushToPullRequestBranchConfig `yaml:"push-to-pr-branch,omitempty"` MissingTool *MissingToolConfig `yaml:"missing-tool,omitempty"` // Optional for reporting missing functionality AllowedDomains []string `yaml:"allowed-domains,omitempty"` + Staged *bool `yaml:"staged,omitempty"` // If true, emit step summary messages instead of making GitHub API calls } // CreateIssuesConfig holds configuration for creating GitHub issues from agent output @@ -3503,6 +3504,13 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut if missingToolConfig != nil { config.MissingTool = missingToolConfig } + + // Handle staged flag + if staged, exists := outputMap["staged"]; exists { + if stagedBool, ok := staged.(bool); ok { + config.Staged = &stagedBool + } + } } } diff --git a/pkg/workflow/custom_engine.go b/pkg/workflow/custom_engine.go index 2b8a9579adc..2c14812958b 100644 --- a/pkg/workflow/custom_engine.go +++ b/pkg/workflow/custom_engine.go @@ -52,6 +52,11 @@ func (e *CustomEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str // Add GITHUB_AW_SAFE_OUTPUTS if safe-outputs feature is used if workflowData.SafeOutputs != nil { envVars["GITHUB_AW_SAFE_OUTPUTS"] = "${{ env.GITHUB_AW_SAFE_OUTPUTS }}" + + // Add staged flag if specified + if workflowData.SafeOutputs.Staged != nil && *workflowData.SafeOutputs.Staged { + envVars["GITHUB_AW_SAFE_OUTPUTS_STAGED"] = "true" + } } // Add GITHUB_AW_MAX_TURNS if max-turns is configured diff --git a/pkg/workflow/js/add_labels.cjs b/pkg/workflow/js/add_labels.cjs index 0959482bd2e..6868949ca5b 100644 --- a/pkg/workflow/js/add_labels.cjs +++ b/pkg/workflow/js/add_labels.cjs @@ -43,6 +43,28 @@ async function main() { labelsCount: labelsItem.labels.length, }); + // If in staged mode, emit step summary instead of adding labels + if (process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true") { + let summaryContent = "## 🎭 Staged Mode: Add Labels Preview\n\n"; + summaryContent += + "The following labels would be added if staged mode was disabled:\n\n"; + + if (labelsItem.issue_number) { + summaryContent += `**Target Issue:** #${labelsItem.issue_number}\n\n`; + } else { + summaryContent += `**Target:** Current issue/PR\n\n`; + } + + if (labelsItem.labels && labelsItem.labels.length > 0) { + summaryContent += `**Labels to add:** ${labelsItem.labels.join(", ")}\n\n`; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Label addition preview written to step summary"); + return; + } + // Read the allowed labels from environment variable (optional) const allowedLabelsEnv = process.env.GITHUB_AW_LABELS_ALLOWED; let allowedLabels = null; diff --git a/pkg/workflow/js/create_code_scanning_alert.cjs b/pkg/workflow/js/create_code_scanning_alert.cjs index ec7b9a61658..248f6f877c0 100644 --- a/pkg/workflow/js/create_code_scanning_alert.cjs +++ b/pkg/workflow/js/create_code_scanning_alert.cjs @@ -32,13 +32,10 @@ async function main() { // Find all create-code-scanning-alert items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => - item.type === "create-code-scanning-alert" + /** @param {any} item */ item => item.type === "create-code-scanning-alert" ); if (securityItems.length === 0) { - console.log( - "No create-code-scanning-alert items found in agent output" - ); + console.log("No create-code-scanning-alert items found in agent output"); return; } @@ -46,6 +43,31 @@ async function main() { `Found ${securityItems.length} create-code-scanning-alert item(s)` ); + // If in staged mode, emit step summary instead of creating code scanning alerts + if (process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true") { + let summaryContent = + "## 🎭 Staged Mode: Create Code Scanning Alerts Preview\n\n"; + summaryContent += + "The following code scanning alerts would be created if staged mode was disabled:\n\n"; + + for (let i = 0; i < securityItems.length; i++) { + const item = securityItems[i]; + summaryContent += `### Security Finding ${i + 1}\n`; + summaryContent += `**File:** ${item.file || "No file provided"}\n\n`; + summaryContent += `**Line:** ${item.line || "No line provided"}\n\n`; + summaryContent += `**Severity:** ${item.severity || "No severity provided"}\n\n`; + summaryContent += `**Message:**\n${item.message || "No message provided"}\n\n`; + summaryContent += "---\n\n"; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log( + "📝 Code scanning alert creation preview written to step summary" + ); + return; + } + // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -85,9 +107,7 @@ async function main() { // Validate required fields if (!securityItem.file) { - console.log( - 'Missing required field "file" in code scanning alert item' - ); + console.log('Missing required field "file" in code scanning alert item'); continue; } diff --git a/pkg/workflow/js/create_code_scanning_alert.test.cjs b/pkg/workflow/js/create_code_scanning_alert.test.cjs index cc27d51c55c..45cbc542cbf 100644 --- a/pkg/workflow/js/create_code_scanning_alert.test.cjs +++ b/pkg/workflow/js/create_code_scanning_alert.test.cjs @@ -54,10 +54,7 @@ describe("create_code_scanning_alert.cjs", () => { afterEach(() => { // Clean up any created files try { - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); if (fs.existsSync(sarifFile)) { fs.unlinkSync(sarifFile); } @@ -160,10 +157,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Check that SARIF file was created - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); expect(fs.existsSync(sarifFile)).toBe(true); // Check SARIF content @@ -235,10 +229,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Check that SARIF file was created with only 1 finding - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); expect(fs.existsSync(sarifFile)).toBe(true); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); @@ -300,10 +291,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Check that SARIF file was created with only the 1 valid finding - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); expect(fs.existsSync(sarifFile)).toBe(true); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); @@ -339,10 +327,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check driver name @@ -376,10 +361,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check default driver name @@ -422,10 +404,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check first result has custom column @@ -487,10 +466,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Only the first valid finding should be processed - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); expect(sarifContent.runs[0].results).toHaveLength(1); expect(sarifContent.runs[0].results[0].message.text).toBe( @@ -541,10 +517,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check first result has custom rule ID @@ -617,10 +590,7 @@ describe("create_code_scanning_alert.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Only the first valid finding should be processed - const sarifFile = path.join( - process.cwd(), - "code-scanning-alert.sarif" - ); + const sarifFile = path.join(process.cwd(), "code-scanning-alert.sarif"); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); expect(sarifContent.runs[0].results).toHaveLength(1); expect(sarifContent.runs[0].results[0].message.text).toBe( diff --git a/pkg/workflow/js/create_comment.cjs b/pkg/workflow/js/create_comment.cjs index 080f64da4f7..7ed0fe27663 100644 --- a/pkg/workflow/js/create_comment.cjs +++ b/pkg/workflow/js/create_comment.cjs @@ -1,4 +1,7 @@ async function main() { + // Check if we're in staged mode + const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; + // Read the validated output content from environment variable const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT; if (!outputContent) { @@ -41,6 +44,30 @@ async function main() { console.log(`Found ${commentItems.length} add-issue-comment item(s)`); + // If in staged mode, emit step summary instead of creating comments + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; + summaryContent += + "The following comments would be added if staged mode was disabled:\n\n"; + + for (let i = 0; i < commentItems.length; i++) { + const item = commentItems[i]; + summaryContent += `### Comment ${i + 1}\n`; + if (item.issue_number) { + summaryContent += `**Target Issue:** #${item.issue_number}\n\n`; + } else { + summaryContent += `**Target:** Current issue/PR\n\n`; + } + summaryContent += `**Body:**\n${item.body || "No content provided"}\n\n`; + summaryContent += "---\n\n"; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Comment creation preview written to step summary"); + return; + } + // Get the target configuration from environment variable const commentTarget = process.env.GITHUB_AW_COMMENT_TARGET || "triggering"; console.log(`Comment target configuration: ${commentTarget}`); diff --git a/pkg/workflow/js/create_discussion.cjs b/pkg/workflow/js/create_discussion.cjs index 85f34fb6443..673561a2940 100644 --- a/pkg/workflow/js/create_discussion.cjs +++ b/pkg/workflow/js/create_discussion.cjs @@ -42,6 +42,31 @@ async function main() { `Found ${createDiscussionItems.length} create-discussion item(s)` ); + // If in staged mode, emit step summary instead of creating discussions + if (process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true") { + let summaryContent = "## 🎭 Staged Mode: Create Discussions Preview\n\n"; + summaryContent += + "The following discussions would be created if staged mode was disabled:\n\n"; + + for (let i = 0; i < createDiscussionItems.length; i++) { + const item = createDiscussionItems[i]; + summaryContent += `### Discussion ${i + 1}\n`; + summaryContent += `**Title:** ${item.title || "No title provided"}\n\n`; + if (item.body) { + summaryContent += `**Body:**\n${item.body}\n\n`; + } + if (item.category_id) { + summaryContent += `**Category ID:** ${item.category_id}\n\n`; + } + summaryContent += "---\n\n"; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Discussion creation preview written to step summary"); + return; + } + // Get repository ID and discussion categories using GraphQL API let discussionCategories = []; let repositoryId = null; diff --git a/pkg/workflow/js/create_issue.cjs b/pkg/workflow/js/create_issue.cjs index f0f0811cd97..fa4deef9ec5 100644 --- a/pkg/workflow/js/create_issue.cjs +++ b/pkg/workflow/js/create_issue.cjs @@ -1,4 +1,7 @@ async function main() { + // Check if we're in staged mode + const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; + // Read the validated output content from environment variable const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT; if (!outputContent) { @@ -40,6 +43,31 @@ async function main() { console.log(`Found ${createIssueItems.length} create-issue item(s)`); + // If in staged mode, emit step summary instead of creating issues + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Issues Preview\n\n"; + summaryContent += + "The following issues would be created if staged mode was disabled:\n\n"; + + for (let i = 0; i < createIssueItems.length; i++) { + const item = createIssueItems[i]; + summaryContent += `### Issue ${i + 1}\n`; + summaryContent += `**Title:** ${item.title || "No title provided"}\n\n`; + if (item.body) { + summaryContent += `**Body:**\n${item.body}\n\n`; + } + if (item.labels && item.labels.length > 0) { + summaryContent += `**Labels:** ${item.labels.join(", ")}\n\n`; + } + summaryContent += "---\n\n"; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Issue creation preview written to step summary"); + return; + } + // Check if we're in an issue context (triggered by an issue event) const parentIssueNumber = context.payload?.issue?.number; diff --git a/pkg/workflow/js/create_pr_review_comment.cjs b/pkg/workflow/js/create_pr_review_comment.cjs index 732b1517de7..727a6a87561 100644 --- a/pkg/workflow/js/create_pr_review_comment.cjs +++ b/pkg/workflow/js/create_pr_review_comment.cjs @@ -46,6 +46,34 @@ async function main() { `Found ${reviewCommentItems.length} create-pull-request-review-comment item(s)` ); + // If in staged mode, emit step summary instead of creating review comments + if (process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true") { + let summaryContent = + "## 🎭 Staged Mode: Create PR Review Comments Preview\n\n"; + summaryContent += + "The following review comments would be created if staged mode was disabled:\n\n"; + + for (let i = 0; i < reviewCommentItems.length; i++) { + const item = reviewCommentItems[i]; + summaryContent += `### Review Comment ${i + 1}\n`; + summaryContent += `**File:** ${item.path || "No path provided"}\n\n`; + summaryContent += `**Line:** ${item.line || "No line provided"}\n\n`; + if (item.start_line) { + summaryContent += `**Start Line:** ${item.start_line}\n\n`; + } + summaryContent += `**Side:** ${item.side || "RIGHT"}\n\n`; + summaryContent += `**Body:**\n${item.body || "No content provided"}\n\n`; + summaryContent += "---\n\n"; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log( + "📝 PR review comment creation preview written to step summary" + ); + return; + } + // Get the side configuration from environment variable const defaultSide = process.env.GITHUB_AW_PR_REVIEW_COMMENT_SIDE || "RIGHT"; console.log(`Default comment side configuration: ${defaultSide}`); diff --git a/pkg/workflow/js/create_pull_request.cjs b/pkg/workflow/js/create_pull_request.cjs index d1fc84df678..1ce0664c201 100644 --- a/pkg/workflow/js/create_pull_request.cjs +++ b/pkg/workflow/js/create_pull_request.cjs @@ -5,6 +5,9 @@ const crypto = require("crypto"); const { execSync } = require("child_process"); async function main() { + // Check if we're in staged mode + const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; + // Environment validation - fail early if required variables are missing const workflowId = process.env.GITHUB_AW_WORKFLOW_ID; if (!workflowId) { @@ -120,6 +123,36 @@ async function main() { bodyLength: pullRequestItem.body.length, }); + // If in staged mode, emit step summary instead of creating PR + 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 += `**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/aw.patch")) { + const patchStats = fs.readFileSync("/tmp/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`; + } + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Pull request creation preview written to step summary"); + return; + } + // Extract title, body, and branch from the JSON item let title = pullRequestItem.title.trim(); let bodyLines = pullRequestItem.body.split("\n"); diff --git a/pkg/workflow/js/push_to_pr_branch.cjs b/pkg/workflow/js/push_to_pr_branch.cjs index 12c533904e4..7dbc722f17f 100644 --- a/pkg/workflow/js/push_to_pr_branch.cjs +++ b/pkg/workflow/js/push_to_pr_branch.cjs @@ -108,6 +108,34 @@ async function main() { console.log("Found push-to-pr-branch item"); + // If in staged mode, emit step summary instead of pushing changes + if (process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true") { + let summaryContent = "## 🎭 Staged Mode: Push to PR Branch Preview\n\n"; + summaryContent += + "The following changes would be pushed if staged mode was disabled:\n\n"; + + summaryContent += `**Target:** ${target}\n\n`; + + if (pushItem.commit_message) { + summaryContent += `**Commit Message:** ${pushItem.commit_message}\n\n`; + } + + if (fs.existsSync("/tmp/aw.patch")) { + const patchStats = fs.readFileSync("/tmp/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`; + } + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Push to PR branch preview written to step summary"); + return; + } + // Validate target configuration for pull request context if (target !== "*" && target !== "triggering") { // If target is a specific number, validate it's a valid pull request number diff --git a/pkg/workflow/js/update_issue.cjs b/pkg/workflow/js/update_issue.cjs index 0a001224ef8..3ba6e75142d 100644 --- a/pkg/workflow/js/update_issue.cjs +++ b/pkg/workflow/js/update_issue.cjs @@ -1,4 +1,7 @@ async function main() { + // Check if we're in staged mode + const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true"; + // Read the validated output content from environment variable const outputContent = process.env.GITHUB_AW_AGENT_OUTPUT; if (!outputContent) { @@ -41,6 +44,39 @@ async function main() { console.log(`Found ${updateItems.length} update-issue item(s)`); + // If in staged mode, emit step summary instead of updating issues + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Update Issues Preview\n\n"; + summaryContent += + "The following issue updates would be applied if staged mode was disabled:\n\n"; + + for (let i = 0; i < updateItems.length; i++) { + const item = updateItems[i]; + summaryContent += `### Issue Update ${i + 1}\n`; + if (item.issue_number) { + summaryContent += `**Target Issue:** #${item.issue_number}\n\n`; + } else { + summaryContent += `**Target:** Current issue\n\n`; + } + + if (item.title !== undefined) { + summaryContent += `**New Title:** ${item.title}\n\n`; + } + if (item.body !== undefined) { + summaryContent += `**New Body:**\n${item.body}\n\n`; + } + if (item.status !== undefined) { + summaryContent += `**New Status:** ${item.status}\n\n`; + } + summaryContent += "---\n\n"; + } + + // Write to step summary + await core.summary.addRaw(summaryContent).write(); + console.log("📝 Issue update preview written to step summary"); + return; + } + // Get the configuration from environment variables const updateTarget = process.env.GITHUB_AW_UPDATE_TARGET || "triggering"; const canUpdateStatus = process.env.GITHUB_AW_UPDATE_STATUS === "true"; diff --git a/pkg/workflow/staged_test.go b/pkg/workflow/staged_test.go new file mode 100644 index 00000000000..d67c8f77461 --- /dev/null +++ b/pkg/workflow/staged_test.go @@ -0,0 +1,164 @@ +package workflow + +import ( + "strings" + "testing" +) + +func TestStagedFlag(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test frontmatter with staged: true + frontmatter := map[string]any{ + "safe-outputs": map[string]any{ + "create-issue": nil, + "staged": true, + }, + } + + // Extract the safe outputs config + config := c.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be parsed") + } + + // Verify staged flag is correctly parsed + if config.Staged == nil { + t.Fatal("Expected staged flag to be parsed") + } + + if !*config.Staged { + t.Fatal("Expected staged flag to be true") + } + + // Test that CreateIssues config is also present + if config.CreateIssues == nil { + t.Fatal("Expected CreateIssues config to be present") + } +} + +func TestStagedFlagDefault(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test frontmatter without staged flag + frontmatter := map[string]any{ + "safe-outputs": map[string]any{ + "create-issue": nil, + }, + } + + // Extract the safe outputs config + config := c.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be parsed") + } + + // Verify staged flag is nil (not specified) + if config.Staged != nil { + t.Fatal("Expected staged flag to be nil when not specified") + } +} + +func TestStagedFlagFalse(t *testing.T) { + // Create a compiler instance + c := NewCompiler(false, "", "test") + + // Test frontmatter with staged: false + frontmatter := map[string]any{ + "safe-outputs": map[string]any{ + "create-issue": nil, + "staged": false, + }, + } + + // Extract the safe outputs config + config := c.extractSafeOutputsConfig(frontmatter) + if config == nil { + t.Fatal("Expected config to be parsed") + } + + // Verify staged flag is correctly parsed as false + if config.Staged == nil { + t.Fatal("Expected staged flag to be parsed") + } + + if *config.Staged { + t.Fatal("Expected staged flag to be false") + } +} + +func TestClaudeEngineWithStagedFlag(t *testing.T) { + engine := NewClaudeEngine() + + // Test with staged flag true + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{Max: 1}, + Staged: &[]bool{true}[0], // pointer to true + }, + } + + steps := engine.GetExecutionSteps(workflowData, "test-log") + if len(steps) == 0 { + t.Fatalf("Expected at least one step, got none") + } + + // Convert first step to YAML string for testing + stepContent := strings.Join([]string(steps[0]), "\n") + + // Check that GITHUB_AW_SAFE_OUTPUTS_STAGED is included + if !strings.Contains(stepContent, "GITHUB_AW_SAFE_OUTPUTS_STAGED: \"true\"") { + t.Error("Expected GITHUB_AW_SAFE_OUTPUTS_STAGED environment variable to be set to true") + } + + // Test with staged flag false + workflowData.SafeOutputs.Staged = &[]bool{false}[0] // pointer to false + + steps = engine.GetExecutionSteps(workflowData, "test-log") + stepContent = strings.Join([]string(steps[0]), "\n") + + // Check that GITHUB_AW_SAFE_OUTPUTS_STAGED is not included when false + if strings.Contains(stepContent, "GITHUB_AW_SAFE_OUTPUTS_STAGED") { + t.Error("Expected GITHUB_AW_SAFE_OUTPUTS_STAGED environment variable not to be set when staged is false") + } + + // Test with staged flag nil (not specified) + workflowData.SafeOutputs.Staged = nil + + steps = engine.GetExecutionSteps(workflowData, "test-log") + stepContent = strings.Join([]string(steps[0]), "\n") + + // Check that GITHUB_AW_SAFE_OUTPUTS_STAGED is not included when nil + if strings.Contains(stepContent, "GITHUB_AW_SAFE_OUTPUTS_STAGED") { + t.Error("Expected GITHUB_AW_SAFE_OUTPUTS_STAGED environment variable not to be set when staged is nil") + } +} + +func TestCodexEngineWithStagedFlag(t *testing.T) { + engine := NewCodexEngine() + + // Test with staged flag true + workflowData := &WorkflowData{ + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{Max: 1}, + Staged: &[]bool{true}[0], // pointer to true + }, + } + + steps := engine.GetExecutionSteps(workflowData, "test-log") + if len(steps) == 0 { + t.Fatalf("Expected at least one step, got none") + } + + // Convert first step to YAML string for testing + stepContent := strings.Join([]string(steps[0]), "\n") + + // Check that GITHUB_AW_SAFE_OUTPUTS_STAGED is included in the env section + if !strings.Contains(stepContent, "GITHUB_AW_SAFE_OUTPUTS_STAGED: true") { + t.Error("Expected GITHUB_AW_SAFE_OUTPUTS_STAGED environment variable to be set to true in Codex engine") + } +}