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")
+ }
+}