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
11 changes: 7 additions & 4 deletions .github/workflows/constraint-solving-potd.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 3 additions & 12 deletions actions/setup-cli/install.sh

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 33 additions & 26 deletions actions/setup/js/handle_create_pr_error.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/// <reference types="@actions/github-script" />

const { sanitizeContent } = require("./sanitize_content.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
* Handle create_pull_request permission errors
Expand Down Expand Up @@ -56,34 +57,40 @@ async function main() {

// Search for existing issue with the same title
const searchQuery = "repo:" + owner + "/" + repo + ' is:issue is:open in:title "' + issueTitle + '"';
const searchResult = await github.rest.search.issuesAndPullRequests({
q: searchQuery,
per_page: 1,
});

if (searchResult.data.total_count > 0) {
const existingIssue = searchResult.data.items[0];
core.info("Issue already exists: #" + existingIssue.number);

// Add a comment with run details
const commentBody = sanitizeContent("This error occurred again in workflow run: " + runUrl);
await github.rest.issues.createComment({
owner,
repo,
issue_number: existingIssue.number,
body: commentBody,
});
core.info("Added comment to existing issue #" + existingIssue.number);
} else {
// Create new issue
const { data: issue } = await github.rest.issues.create({
owner,
repo,
title: issueTitle,
body: sanitizeContent(issueBody),
labels: ["agentic-workflows", "configuration"],
try {
const searchResult = await github.rest.search.issuesAndPullRequests({
q: searchQuery,
per_page: 1,
});
core.info("Created issue #" + issue.number + ": " + issue.html_url);

if (searchResult.data.total_count > 0) {
const existingIssue = searchResult.data.items[0];
core.info("Issue already exists: #" + existingIssue.number);

// Add a comment with run details
const commentBody = sanitizeContent("This error occurred again in workflow run: " + runUrl);
await github.rest.issues.createComment({
owner,
repo,
issue_number: existingIssue.number,
body: commentBody,
});
core.info("Added comment to existing issue #" + existingIssue.number);
} else {
// Create new issue
const { data: issue } = await github.rest.issues.create({
owner,
repo,
title: issueTitle,
body: sanitizeContent(issueBody),
labels: ["agentic-workflows", "configuration"],
});
core.info("Created issue #" + issue.number + ": " + issue.html_url);
}
} catch (error) {
core.warning("Failed to create or update permission error issue: " + getErrorMessage(error));
// Don't fail the conclusion job if we can't report the PR creation error
}
}

Expand Down
134 changes: 134 additions & 0 deletions actions/setup/js/handle_create_pr_error.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import path from "path";
const mockCore = {
debug: vi.fn(),
info: vi.fn(),
notice: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
},
mockGithub = {
rest: {
search: {
issuesAndPullRequests: vi.fn(),
},
issues: {
createComment: vi.fn(),
create: vi.fn(),
},
},
},
mockContext = { repo: { owner: "testowner", repo: "testrepo" } };
((global.core = mockCore),
(global.github = mockGithub),
(global.context = mockContext),
describe("handle_create_pr_error.cjs", () => {
let scriptContent, originalEnv;
(beforeEach(() => {
(vi.clearAllMocks(),
(originalEnv = {
CREATE_PR_ERROR_MESSAGE: process.env.CREATE_PR_ERROR_MESSAGE,
GH_AW_WORKFLOW_NAME: process.env.GH_AW_WORKFLOW_NAME,
GH_AW_RUN_URL: process.env.GH_AW_RUN_URL,
GH_AW_WORKFLOW_SOURCE: process.env.GH_AW_WORKFLOW_SOURCE,
GH_AW_WORKFLOW_SOURCE_URL: process.env.GH_AW_WORKFLOW_SOURCE_URL,
}));
const scriptPath = path.join(process.cwd(), "handle_create_pr_error.cjs");
scriptContent = fs.readFileSync(scriptPath, "utf8");
}),
afterEach(() => {
Object.keys(originalEnv).forEach(key => {
void 0 !== originalEnv[key] ? (process.env[key] = originalEnv[key]) : delete process.env[key];
});
}),
describe("when no error message is set", () => {
it("should skip and not call any API", async () => {
(delete process.env.CREATE_PR_ERROR_MESSAGE,
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockCore.info).toHaveBeenCalledWith("No create_pull_request error message - skipping"),
Comment on lines +39 to +51
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test reads the script source and executes it via eval. That approach is unusually fragile in this repo (no other Vitest tests use eval), and can break on CommonJS-only constructs in the evaluated file (e.g., module.exports, require resolution) or when process.cwd() differs. Prefer importing the module like other tests here (e.g., const { main } = await import('./handle_create_pr_error.cjs?t=' + Date.now())) and invoking main() directly, which also avoids the process.cwd()-based path.

Copilot uses AI. Check for mistakes.
expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled());
});
}),
describe("when error is not a permission error", () => {
it("should skip and not call any API", async () => {
((process.env.CREATE_PR_ERROR_MESSAGE = "Some unrelated error"),
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockCore.info).toHaveBeenCalledWith("Not a permission error - skipping"),
expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled());
});
}),
describe("when it is the permission error", () => {
beforeEach(() => {
((process.env.CREATE_PR_ERROR_MESSAGE = "GitHub Actions is not permitted to create or approve pull requests"),
(process.env.GH_AW_WORKFLOW_NAME = "test-workflow"),
(process.env.GH_AW_RUN_URL = "https://github.com/owner/repo/actions/runs/123"));
});

it("should create a new issue when none exists", async () => {
(mockGithub.rest.search.issuesAndPullRequests.mockResolvedValueOnce({ data: { total_count: 0, items: [] } }),
mockGithub.rest.issues.create.mockResolvedValueOnce({ data: { number: 42, html_url: "https://github.com/owner/repo/issues/42" } }),
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockGithub.rest.issues.create).toHaveBeenCalledWith(
expect.objectContaining({
owner: "testowner",
repo: "testrepo",
title: "[aw] GitHub Actions needs permission to create pull requests",
labels: ["agentic-workflows", "configuration"],
})
),
expect(mockCore.info).toHaveBeenCalledWith("Created issue #42: https://github.com/owner/repo/issues/42"),
expect(mockCore.setFailed).not.toHaveBeenCalled());
});

it("should add a comment to an existing issue", async () => {
(mockGithub.rest.search.issuesAndPullRequests.mockResolvedValueOnce({
data: { total_count: 1, items: [{ number: 10, html_url: "https://github.com/owner/repo/issues/10" }] },
}),
mockGithub.rest.issues.createComment.mockResolvedValueOnce({ data: {} }),
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith(
expect.objectContaining({
owner: "testowner",
repo: "testrepo",
issue_number: 10,
body: expect.stringContaining("https://github.com/owner/repo/actions/runs/123"),
})
),
expect(mockCore.info).toHaveBeenCalledWith("Added comment to existing issue #10"),
expect(mockCore.setFailed).not.toHaveBeenCalled());
});

describe("error handling", () => {
it("should warn but not fail when search API throws", async () => {
(mockGithub.rest.search.issuesAndPullRequests.mockRejectedValueOnce(new Error("Rate limit exceeded")),
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to create or update permission error issue")),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Rate limit exceeded")),
expect(mockCore.setFailed).not.toHaveBeenCalled());
});

it("should warn but not fail when issue creation throws", async () => {
(mockGithub.rest.search.issuesAndPullRequests.mockResolvedValueOnce({ data: { total_count: 0, items: [] } }),
mockGithub.rest.issues.create.mockRejectedValueOnce(new Error("Forbidden")),
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to create or update permission error issue")),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Forbidden")),
expect(mockCore.setFailed).not.toHaveBeenCalled());
});

it("should warn but not fail when createComment throws", async () => {
(mockGithub.rest.search.issuesAndPullRequests.mockResolvedValueOnce({
data: { total_count: 1, items: [{ number: 10 }] },
}),
mockGithub.rest.issues.createComment.mockRejectedValueOnce(new Error("Network error")),
await eval(`(async () => { ${scriptContent}; await main(); })()`),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to create or update permission error issue")),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Network error")),
expect(mockCore.setFailed).not.toHaveBeenCalled());
});
});
}));
}));
Loading