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
71 changes: 39 additions & 32 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1370,10 +1370,10 @@ async function main(config = {}) {
}

// Push the commits from the bundle to the remote branch
if (manifestProtectionFallback) {
core.info("Skipping branch push because protected-files fallback-to-issue was triggered");
manifestProtectionPushFailedError = new Error("Push skipped because protected-files fallback-to-issue was triggered");
} else {
// Note: when manifestProtectionFallback is set we still push the branch so the
// fallback issue can include a compare URL. Genuine push failures are handled in
// the catch block below.
{
try {
branchName = await handleRemoteBranchCollision(branchName, preserveBranchName, { recreateRef, githubClient, owner: repoParts.owner, repo: repoParts.repo });

Expand Down Expand Up @@ -1438,20 +1438,26 @@ async function main(config = {}) {
if (!pushRecovered) {
core.error(`Git push failed: ${pushError instanceof Error ? pushError.message : String(pushError)}`);

if (!fallbackAsIssue) {
if (manifestProtectionFallback) {
// Push failed specifically for a protected-file modification. Don't create
// a generic push-failed issue — fall through to the manifestProtectionFallback
// block below, which will create the proper protected-file review issue with
// patch artifact download instructions (since the branch was not pushed).
core.warning("Git push failed for protected-file modification - deferring to protected-file review issue");
manifestProtectionPushFailedError = pushError;
} else if (!fallbackAsIssue) {
const error = `Failed to push changes: ${pushError instanceof Error ? pushError.message : String(pushError)}`;
return { success: false, error, error_type: "push_failed" };
}

core.warning("Git push operation failed - creating fallback issue instead of pull request");
} else {
core.warning("Git push operation failed - creating fallback issue instead of pull request");

const runUrl = buildWorkflowRunUrl(context, context.repo);
const runId = context.runId;
const runUrl = buildWorkflowRunUrl(context, context.repo);
const runId = context.runId;

const artifactFileName = bundleFilePath ? bundleFilePath.replace("/tmp/gh-aw/", "") : "aw-unknown.bundle";
const fallbackBundleSourceRef = `refs/heads/${originalAgentBranch || branchName}`;
const fallbackBundleTempRef = createBundleTempRef(branchName);
const fallbackBody = `${issueSafeBody}
const artifactFileName = bundleFilePath ? bundleFilePath.replace("/tmp/gh-aw/", "") : "aw-unknown.bundle";
const fallbackBundleSourceRef = `refs/heads/${originalAgentBranch || branchName}`;
const fallbackBundleTempRef = createBundleTempRef(branchName);
const fallbackBody = `${issueSafeBody}

---

Expand Down Expand Up @@ -1484,22 +1490,23 @@ git push origin ${branchName}
gh pr create --title '${title}' --base ${baseBranch} --head ${branchName} --repo ${repoParts.owner}/${repoParts.repo}
\`\`\``;

try {
const { data: issue } = await createFallbackIssue(githubClient, repoParts, title, fallbackBody, mergeFallbackIssueLabels(effectiveFallbackLabels), configAssignees);
try {
const { data: issue } = await createFallbackIssue(githubClient, repoParts, title, fallbackBody, mergeFallbackIssueLabels(effectiveFallbackLabels), configAssignees);

core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`);
await assignCopilotToFallbackIssueIfEnabled(repoParts.owner, repoParts.repo, issue.number);
await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue");
core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`);
await assignCopilotToFallbackIssueIfEnabled(repoParts.owner, repoParts.repo, issue.number);
await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue");

return {
success: true,
fallback_used: true,
issue_number: issue.number,
issue_url: issue.html_url,
};
} catch (issueError) {
const error = `Failed to push changes and failed to create fallback issue. Push error: ${pushError instanceof Error ? pushError.message : String(pushError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}`;
return { success: false, error };
return {
success: true,
fallback_used: true,
issue_number: issue.number,
issue_url: issue.html_url,
};
} catch (issueError) {
const error = `Failed to push changes and failed to create fallback issue. Push error: ${pushError instanceof Error ? pushError.message : String(pushError)}. Issue error: ${issueError instanceof Error ? issueError.message : String(issueError)}`;
return { success: false, error };
}
}
}
}
Expand Down Expand Up @@ -1681,10 +1688,10 @@ gh pr create --title '${title}' --base ${baseBranch} --head ${branchName} --repo
}

// Push the applied commits to the branch (with fallback to issue creation on failure)
if (manifestProtectionFallback) {
core.info("Skipping branch push because protected-files fallback-to-issue was triggered");
manifestProtectionPushFailedError = new Error("Push skipped because protected-files fallback-to-issue was triggered");
} else {
// Note: when manifestProtectionFallback is set we still push the branch so the
// fallback issue can include a compare URL. Genuine push failures are handled in
// the catch block below.
{
try {
branchName = await handleRemoteBranchCollision(branchName, preserveBranchName, { recreateRef, githubClient, owner: repoParts.owner, repo: repoParts.repo });

Expand Down
30 changes: 18 additions & 12 deletions actions/setup/js/create_pull_request.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ ${diffs}
expect(global.github.rest.pulls.createReview.mock.calls[1][0].event).toBe("COMMENT");
});

it("should use patch-artifact fallback instructions when protected-files fallback skips push", async () => {
it("should push branch with compare URL for protected-files fallback (patch transport)", async () => {
const patchPath = writePatch(createPatchWithFiles(".github/aw/instructions.md"));
const promptsDir = path.join(tempDir, "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
Expand All @@ -1567,17 +1567,20 @@ ${diffs}
expect(result.success).toBe(true);
expect(result.fallback_used).toBe(true);
expect(result.issue_number).toBe(77);
expect(pushSignedSpy).not.toHaveBeenCalled();
// Branch must be pushed so the fallback issue can include a compare URL
expect(pushSignedSpy).toHaveBeenCalled();
expect(global.github.rest.issues.create).toHaveBeenCalledTimes(1);
expect(global.github.rest.issues.update).not.toHaveBeenCalled();
// Issue body is updated after creation to add the close-keyword link
expect(global.github.rest.issues.update).toHaveBeenCalled();

const createCall = global.github.rest.issues.create.mock.calls[0][0];
expect(createCall.body).toContain("gh run download");
expect(createCall.body).toContain("git am --3way");
expect(createCall.body).not.toContain("/compare/main...");
// Should use the create-PR fallback template (compare URL), not the push-failed template
expect(createCall.body).toContain("/compare/main...");
expect(createCall.body).not.toContain("gh run download");
expect(createCall.body).not.toContain("git am --3way");
});

it("should use patch-artifact fallback instructions for protected-files fallback in bundle transport", async () => {
it("should push branch with compare URL for protected-files fallback (bundle transport)", async () => {
const patchPath = writePatch(createPatchWithFiles(".github/aw/instructions.md"));
const bundlePath = path.join(tempDir, "aw-protected.bundle");
fs.writeFileSync(bundlePath, "bundle content");
Expand All @@ -1604,13 +1607,16 @@ ${diffs}
expect(result.success).toBe(true);
expect(result.fallback_used).toBe(true);
expect(result.issue_number).toBe(77);
expect(pushSignedSpy).not.toHaveBeenCalled();
expect(global.github.rest.issues.update).not.toHaveBeenCalled();
// Branch must be pushed so the fallback issue can include a compare URL
expect(pushSignedSpy).toHaveBeenCalled();
// Issue body is updated after creation to add the close-keyword link
expect(global.github.rest.issues.update).toHaveBeenCalled();

const createCall = global.github.rest.issues.create.mock.calls[0][0];
expect(createCall.body).toContain("gh run download");
expect(createCall.body).toContain("git am --3way");
expect(createCall.body).not.toContain("/compare/main...");
// Should use the create-PR fallback template (compare URL), not the push-failed template
expect(createCall.body).toContain("/compare/main...");
expect(createCall.body).not.toContain("gh run download");
expect(createCall.body).not.toContain("git am --3way");
});
});

Expand Down
Loading