diff --git a/.github/workflows/constraint-solving-potd.lock.yml b/.github/workflows/constraint-solving-potd.lock.yml index b125c171b69..d3098c919f0 100644 --- a/.github/workflows/constraint-solving-potd.lock.yml +++ b/.github/workflows/constraint-solving-potd.lock.yml @@ -66,7 +66,7 @@ jobs: GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} GH_AW_INFO_VERSION: "" - GH_AW_INFO_AGENT_VERSION: "0.0.420" + GH_AW_INFO_AGENT_VERSION: "0.0.421" GH_AW_INFO_WORKFLOW_NAME: "Constraint Solving — Problem of the Day" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" @@ -301,7 +301,7 @@ jobs: git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.420 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.421 - name: Install awf binary run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 - name: Determine automatic lockdown mode for GitHub MCP Server @@ -315,7 +315,7 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.7 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.8 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs @@ -604,7 +604,7 @@ jobs: export DEBUG="*" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.7' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.8' mkdir -p /home/runner/.copilot cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh @@ -946,6 +946,9 @@ jobs: contents: read discussions: write issues: write + concurrency: + group: "gh-aw-conclusion-constraint-solving-potd" + cancel-in-progress: false outputs: noop_message: ${{ steps.noop.outputs.noop_message }} tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index 58d73c6805d..e1ac3479a12 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -239,17 +239,8 @@ fi if [ "$TRY_GH_INSTALL" = true ] && command -v gh &> /dev/null; then print_info "Attempting to install gh-aw using 'gh extension install'..." - # Call gh extension install directly to avoid command injection - install_result=0 - if [ -n "$VERSION" ] && [ "$VERSION" != "latest" ]; then - gh extension install "$REPO" --force --pin "$VERSION" 2>&1 | tee /tmp/gh-install.log - install_result=${PIPESTATUS[0]} - else - gh extension install "$REPO" --force 2>&1 | tee /tmp/gh-install.log - install_result=${PIPESTATUS[0]} - fi - - if [ $install_result -eq 0 ]; then + # Try to install using gh + if gh extension install "$REPO" --force 2>&1 | tee /tmp/gh-install.log; then # Verify the installation succeeded if gh aw version &> /dev/null; then INSTALLED_VERSION=$(gh aw version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1) @@ -258,7 +249,7 @@ if [ "$TRY_GH_INSTALL" = true ] && command -v gh &> /dev/null; then # Set output for GitHub Actions if [ -n "${GITHUB_OUTPUT}" ]; then - echo "installed_version=${INSTALLED_VERSION}" >> "${GITHUB_OUTPUT}" + echo "installed_version=${VERSION}" >> "${GITHUB_OUTPUT}" fi exit 0 diff --git a/actions/setup/js/handle_create_pr_error.cjs b/actions/setup/js/handle_create_pr_error.cjs index 20b5099d926..1d070731f83 100644 --- a/actions/setup/js/handle_create_pr_error.cjs +++ b/actions/setup/js/handle_create_pr_error.cjs @@ -2,6 +2,7 @@ /// const { sanitizeContent } = require("./sanitize_content.cjs"); +const { getErrorMessage } = require("./error_helpers.cjs"); /** * Handle create_pull_request permission errors @@ -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 } } diff --git a/actions/setup/js/handle_create_pr_error.test.cjs b/actions/setup/js/handle_create_pr_error.test.cjs new file mode 100644 index 00000000000..9dcd8279fc2 --- /dev/null +++ b/actions/setup/js/handle_create_pr_error.test.cjs @@ -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"), + 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()); + }); + }); + })); + }));