Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -311,16 +311,18 @@ function buildForkContextHint() {
* section with clearer remediation instructions.
* @param {string} codePushFailureErrors - Newline-separated list of "type:error" entries
* @param {{number: number, html_url: string, head_sha?: string, mergeable?: boolean | null, mergeable_state?: string, updated_at?: string} | null} pullRequest - PR info if available
* @param {string} [runUrl] - URL of the current workflow run, used to provide patch download instructions
* @returns {string} Formatted context string, or empty string if no failures
*/
function buildCodePushFailureContext(codePushFailureErrors, pullRequest = null) {
function buildCodePushFailureContext(codePushFailureErrors, pullRequest = null, runUrl = "") {
if (!codePushFailureErrors) {
return "";
}

// Split errors into protected-file protection refusals, patch size errors, and other push failures
// Split errors into protected-file protection refusals, patch size errors, patch apply failures, and other push failures
const manifestErrors = [];
const patchSizeErrors = [];
const patchApplyErrors = [];
const otherErrors = [];
const errorLines = codePushFailureErrors.split("\n").filter(line => line.trim());
for (const errorLine of errorLines) {
Expand All @@ -332,6 +334,8 @@ function buildCodePushFailureContext(codePushFailureErrors, pullRequest = null)
manifestErrors.push({ type, error });
} else if (error.includes("Patch size") && error.includes("exceeds")) {
patchSizeErrors.push({ type, error });
} else if (error.includes("Failed to apply patch")) {
patchApplyErrors.push({ type, error });
} else {
otherErrors.push({ type, error });
}
Expand Down Expand Up @@ -394,6 +398,56 @@ function buildCodePushFailureContext(codePushFailureErrors, pullRequest = null)
context += yamlSnippet;
}

// Patch apply failure section β€” shown when the patch could not be applied (e.g. merge conflict)
if (patchApplyErrors.length > 0) {
context += "\n**πŸ”€ Patch Apply Failed**: The patch could not be applied to the current state of the repository. " + "This is typically caused by a merge conflict between the agent's changes and recent commits on the target branch.\n";
if (pullRequest) {
context += `\n**Target Pull Request:** [#${pullRequest.number}](${pullRequest.html_url})\n`;
}
context += "\n**Failed Operations:**\n";
for (const { type, error } of patchApplyErrors) {
context += `- \`${type}\`: ${error}\n`;
}

// Extract run ID from runUrl for use in the download command
let runId = "";
if (runUrl) {
const runIdMatch = runUrl.match(/\/actions\/runs\/(\d+)/);
if (runIdMatch) {
runId = runIdMatch[1];
}
}

context += "\nTo manually apply the patch:\n\n";
if (runId) {
context += `\`\`\`sh
# Download the patch artifact from the workflow run
gh run download ${runId} -n agent-artifacts -D /tmp/agent-artifacts-${runId}

# List available patches
ls /tmp/agent-artifacts-${runId}/*.patch

# Create a new branch (adjust as needed)
git checkout -b aw/manual-apply

# Apply the patch (--3way handles cross-repo patches)
git am --3way /tmp/agent-artifacts-${runId}/YOUR_PATCH_FILE.patch

# If there are conflicts, resolve them and continue (or abort):
# git am --continue
# git am --abort

# Push and create a pull request
git push origin aw/manual-apply
gh pr create --head aw/manual-apply
\`\`\`
${runUrl ? `\nThe patch artifact is available at: [View run and download artifacts](${runUrl})\n` : ""}`;
} else {
context += "Download the patch artifact from the workflow run, then apply it with `git am --3way <patch-file>`.\n";
}
context += "\n";
}

// Generic code-push failure section
if (otherErrors.length > 0) {
context += "\n**⚠️ Code Push Failed**: A code push safe output failed, and subsequent safe outputs were cancelled.";
Expand Down Expand Up @@ -443,8 +497,8 @@ function buildCodePushFailureContext(codePushFailureErrors, pullRequest = null)
context += `- \`${type}\`: ${error}\n`;
}
context += "\n";
} else if (manifestErrors.length > 0 || patchSizeErrors.length > 0) {
// Only manifest or patch size errors β€” ensure trailing newline
} else if (manifestErrors.length > 0 || patchSizeErrors.length > 0 || patchApplyErrors.length > 0) {
// Only manifest, patch size, or patch apply errors β€” ensure trailing newline
context += "\n";
}

Expand Down Expand Up @@ -785,7 +839,7 @@ async function main() {
const createDiscussionErrorsContext = hasCreateDiscussionErrors ? buildCreateDiscussionErrorsContext(createDiscussionErrors) : "";

// Build code-push failure context
const codePushFailureContext = hasCodePushFailures ? buildCodePushFailureContext(codePushFailureErrors, pullRequest) : "";
const codePushFailureContext = hasCodePushFailures ? buildCodePushFailureContext(codePushFailureErrors, pullRequest, runUrl) : "";

// Build repo-memory validation errors context
let repoMemoryValidationContext = "";
Expand Down Expand Up @@ -909,7 +963,7 @@ async function main() {
const createDiscussionErrorsContext = hasCreateDiscussionErrors ? buildCreateDiscussionErrorsContext(createDiscussionErrors) : "";

// Build code-push failure context
const codePushFailureContext = hasCodePushFailures ? buildCodePushFailureContext(codePushFailureErrors, pullRequest) : "";
const codePushFailureContext = hasCodePushFailures ? buildCodePushFailureContext(codePushFailureErrors, pullRequest, runUrl) : "";

// Build repo-memory validation errors context
let repoMemoryValidationContext = "";
Expand Down
66 changes: 66 additions & 0 deletions actions/setup/js/handle_agent_failure.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,72 @@ describe("handle_agent_failure", () => {
expect(result).toContain("Code Push Failed");
expect(result).toContain("Branch not found");
});

// ──────────────────────────────────────────────────────
// Patch Apply Failed (merge conflict)
// ──────────────────────────────────────────────────────

it("shows patch apply failed section for create_pull_request patch apply error", () => {
const errors = "create_pull_request:Failed to apply patch";
const result = buildCodePushFailureContext(errors);
expect(result).toContain("πŸ”€ Patch Apply Failed");
expect(result).toContain("merge conflict");
expect(result).toContain("`create_pull_request`");
expect(result).toContain("Failed to apply patch");
// Should NOT show generic "Code Push Failed" for pure patch apply errors
expect(result).not.toContain("Code Push Failed");
});

it("shows patch apply failed section for push_to_pull_request_branch patch apply error", () => {
const errors = "push_to_pull_request_branch:Failed to apply patch";
const result = buildCodePushFailureContext(errors);
expect(result).toContain("πŸ”€ Patch Apply Failed");
expect(result).toContain("`push_to_pull_request_branch`");
expect(result).not.toContain("Code Push Failed");
});

it("includes PR link in patch apply failed section when PR is provided", () => {
const errors = "create_pull_request:Failed to apply patch";
const pullRequest = { number: 77, html_url: "https://github.com/owner/repo/pull/77" };
const result = buildCodePushFailureContext(errors, pullRequest);
expect(result).toContain("πŸ”€ Patch Apply Failed");
expect(result).toContain("#77");
expect(result).toContain("https://github.com/owner/repo/pull/77");
});

it("includes patch download instructions with run ID when runUrl is provided", () => {
const errors = "create_pull_request:Failed to apply patch";
const runUrl = "https://github.com/owner/repo/actions/runs/12345678";
const result = buildCodePushFailureContext(errors, null, runUrl);
expect(result).toContain("πŸ”€ Patch Apply Failed");
expect(result).toContain("gh run download 12345678");
expect(result).toContain("agent-artifacts");
expect(result).toContain("git am --3way");
expect(result).toContain(runUrl);
});

it("shows generic download instructions when runUrl is not provided", () => {
const errors = "create_pull_request:Failed to apply patch";
const result = buildCodePushFailureContext(errors);
expect(result).toContain("πŸ”€ Patch Apply Failed");
expect(result).toContain("git am --3way");
// No specific run ID in instructions
expect(result).not.toContain("gh run download");
});

it("shows both patch apply failed and generic sections when mixed", () => {
const errors = ["create_pull_request:Failed to apply patch", "push_to_pull_request_branch:Branch not found"].join("\n");
const result = buildCodePushFailureContext(errors);
expect(result).toContain("πŸ”€ Patch Apply Failed");
expect(result).toContain("Code Push Failed");
expect(result).toContain("Branch not found");
});

it("does not show patch apply section for generic errors", () => {
const errors = "push_to_pull_request_branch:Branch not found";
const result = buildCodePushFailureContext(errors);
expect(result).not.toContain("πŸ”€ Patch Apply Failed");
});
});

// ──────────────────────────────────────────────────────
Expand Down
Loading