diff --git a/.changeset/patch-standardize-safe-output-error-codes.md b/.changeset/patch-standardize-safe-output-error-codes.md new file mode 100644 index 0000000000..56ea344ca8 --- /dev/null +++ b/.changeset/patch-standardize-safe-output-error-codes.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Added the new safe-output error code registry and updated every handler/test to use the standardized codes so messages are machine-readable and consistent. diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index 713898c85e..5b4b3d4eaf 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -17,6 +17,7 @@ const { getMessages } = require("./messages_core.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { MAX_COMMENT_LENGTH, MAX_MENTIONS, MAX_LINKS, enforceCommentLimits } = require("./comment_limit_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); /** @type {string} Safe output type handled by this module */ const HANDLER_TYPE = "add_comment"; @@ -229,7 +230,7 @@ async function commentOnDiscussion(github, owner, repo, discussionNumber, messag ); if (!repository || !repository.discussion) { - throw new Error(`Discussion #${discussionNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${discussionNumber} not found in ${owner}/${repo}`); } const discussionId = repository.discussion.id; @@ -522,7 +523,7 @@ async function main(config = {}) { const discussionId = queryResult?.repository?.discussion?.id; if (!discussionId) { - throw new Error(`Discussion #${itemNumber} not found in ${itemRepo}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${itemNumber} not found in ${itemRepo}`); } comment = await commentOnDiscussion(github, repoParts.owner, repoParts.repo, itemNumber, processedBody, null); @@ -592,7 +593,7 @@ async function main(config = {}) { const discussionId = queryResult?.repository?.discussion?.id; if (!discussionId) { - throw new Error(`Discussion #${itemNumber} not found in ${itemRepo}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${itemNumber} not found in ${itemRepo}`); } core.info(`Found discussion #${itemNumber}, adding comment...`); diff --git a/actions/setup/js/add_copilot_reviewer.cjs b/actions/setup/js/add_copilot_reviewer.cjs index 6636214454..0bbf4e3767 100644 --- a/actions/setup/js/add_copilot_reviewer.cjs +++ b/actions/setup/js/add_copilot_reviewer.cjs @@ -2,6 +2,7 @@ /// const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_CONFIG, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Add Copilot as a reviewer to a pull request. @@ -22,13 +23,13 @@ async function main() { const prNumberStr = process.env.PR_NUMBER?.trim(); if (!prNumberStr) { - core.setFailed("PR_NUMBER environment variable is required but not set"); + core.setFailed(`${ERR_CONFIG}: PR_NUMBER environment variable is required but not set`); return; } const prNumber = parseInt(prNumberStr, 10); if (isNaN(prNumber) || prNumber <= 0) { - core.setFailed(`Invalid PR_NUMBER: ${prNumberStr}. Must be a positive integer.`); + core.setFailed(`${ERR_VALIDATION}: Invalid PR_NUMBER: ${prNumberStr}. Must be a positive integer.`); return; } @@ -55,7 +56,7 @@ Successfully added Copilot as a reviewer to PR #${prNumber}.` } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to add Copilot as reviewer: ${errorMessage}`); - core.setFailed(`Failed to add Copilot as reviewer to PR #${prNumber}: ${errorMessage}`); + core.setFailed(`${ERR_NOT_FOUND}: Failed to add Copilot as reviewer to PR #${prNumber}: ${errorMessage}`); } } diff --git a/actions/setup/js/add_copilot_reviewer.test.cjs b/actions/setup/js/add_copilot_reviewer.test.cjs index 03ad132413..9e61365fd2 100644 --- a/actions/setup/js/add_copilot_reviewer.test.cjs +++ b/actions/setup/js/add_copilot_reviewer.test.cjs @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; +const { ERR_CONFIG } = require("./error_codes.cjs"); // Mock the global objects that GitHub Actions provides const mockCore = { @@ -76,7 +77,7 @@ describe("add_copilot_reviewer", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("PR_NUMBER environment variable is required but not set"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: PR_NUMBER environment variable is required but not set`); expect(mockGithub.rest.pulls.requestReviewers).not.toHaveBeenCalled(); }); @@ -85,7 +86,7 @@ describe("add_copilot_reviewer", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("PR_NUMBER environment variable is required but not set"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: PR_NUMBER environment variable is required but not set`); expect(mockGithub.rest.pulls.requestReviewers).not.toHaveBeenCalled(); }); diff --git a/actions/setup/js/add_reaction.cjs b/actions/setup/js/add_reaction.cjs index f4de27252a..62e1ba7653 100644 --- a/actions/setup/js/add_reaction.cjs +++ b/actions/setup/js/add_reaction.cjs @@ -2,6 +2,7 @@ /// const { getErrorMessage, isLockedError } = require("./error_helpers.cjs"); +const { ERR_API, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Add a reaction to the triggering item (issue, PR, comment, or discussion). @@ -18,7 +19,7 @@ async function main() { // Validate reaction type const validReactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]; if (!validReactions.includes(reaction)) { - core.setFailed(`Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`); + core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`); return; } @@ -33,7 +34,7 @@ async function main() { case "issues": const issueNumber = context.payload?.issue?.number; if (!issueNumber) { - core.setFailed("Issue number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); return; } reactionEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/reactions`; @@ -42,7 +43,7 @@ async function main() { case "issue_comment": const commentId = context.payload?.comment?.id; if (!commentId) { - core.setFailed("Comment ID not found in event payload"); + core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`); return; } reactionEndpoint = `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`; @@ -51,7 +52,7 @@ async function main() { case "pull_request": const prNumber = context.payload?.pull_request?.number; if (!prNumber) { - core.setFailed("Pull request number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); return; } // PRs are "issues" for the reactions endpoint @@ -61,7 +62,7 @@ async function main() { case "pull_request_review_comment": const reviewCommentId = context.payload?.comment?.id; if (!reviewCommentId) { - core.setFailed("Review comment ID not found in event payload"); + core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`); return; } reactionEndpoint = `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`; @@ -70,7 +71,7 @@ async function main() { case "discussion": const discussionNumber = context.payload?.discussion?.number; if (!discussionNumber) { - core.setFailed("Discussion number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); return; } // Discussions use GraphQL API - get the node ID @@ -81,14 +82,14 @@ async function main() { case "discussion_comment": const commentNodeId = context.payload?.comment?.node_id; if (!commentNodeId) { - core.setFailed("Discussion comment node ID not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`); return; } await addDiscussionReaction(commentNodeId, reaction); return; // Early return for discussion comment events default: - core.setFailed(`Unsupported event type: ${eventName}`); + core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`); return; } @@ -107,7 +108,7 @@ async function main() { // For other errors, fail as before core.error(`Failed to add reaction: ${errorMessage}`); - core.setFailed(`Failed to add reaction: ${errorMessage}`); + core.setFailed(`${ERR_API}: Failed to add reaction: ${errorMessage}`); } } @@ -154,7 +155,7 @@ async function addDiscussionReaction(subjectId, reaction) { const reactionContent = reactionMap[reaction]; if (!reactionContent) { - throw new Error(`Invalid reaction type for GraphQL: ${reaction}`); + throw new Error(`${ERR_VALIDATION}: Invalid reaction type for GraphQL: ${reaction}`); } const result = await github.graphql( @@ -197,7 +198,7 @@ async function getDiscussionId(owner, repo, discussionNumber) { ); if (!repository || !repository.discussion) { - throw new Error(`Discussion #${discussionNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${discussionNumber} not found in ${owner}/${repo}`); } return { diff --git a/actions/setup/js/add_reaction.test.cjs b/actions/setup/js/add_reaction.test.cjs index e5ff4ff9dc..6bc310bc76 100644 --- a/actions/setup/js/add_reaction.test.cjs +++ b/actions/setup/js/add_reaction.test.cjs @@ -1,5 +1,6 @@ // @ts-check import { describe, it, expect, beforeEach, vi } from "vitest"; +const { ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); // Mock the global objects that GitHub Actions provides const mockCore = { @@ -139,7 +140,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Issue number not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Issue number not found in event payload`); expect(mockGithub.request).not.toHaveBeenCalled(); }); }); @@ -166,7 +167,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Comment ID not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Comment ID not found in event payload`); }); }); @@ -192,7 +193,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Pull request number not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); }); }); @@ -218,7 +219,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Review comment ID not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Review comment ID not found in event payload`); }); }); @@ -268,7 +269,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion number not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); }); it("should handle discussion not found error", async () => { @@ -317,7 +318,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion comment node ID not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`); }); }); @@ -360,7 +361,7 @@ describe("add_reaction", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Unsupported event type: push"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Unsupported event type: push`); expect(mockGithub.request).not.toHaveBeenCalled(); }); }); diff --git a/actions/setup/js/add_reaction_and_edit_comment.cjs b/actions/setup/js/add_reaction_and_edit_comment.cjs index 5dc2abde72..2f9810ab6b 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.cjs @@ -5,6 +5,7 @@ const { getRunStartedMessage } = require("./messages_run_status.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); +const { ERR_API, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); async function main() { // Read inputs from environment variables @@ -22,7 +23,7 @@ async function main() { // Validate reaction type const validReactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]; if (!validReactions.includes(reaction)) { - core.setFailed(`Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`); + core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${validReactions.join(", ")}`); return; } @@ -39,7 +40,7 @@ async function main() { case "issues": const issueNumber = context.payload?.issue?.number; if (!issueNumber) { - core.setFailed("Issue number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); return; } reactionEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/reactions`; @@ -52,11 +53,11 @@ async function main() { const commentId = context.payload?.comment?.id; const issueNumberForComment = context.payload?.issue?.number; if (!commentId) { - core.setFailed("Comment ID not found in event payload"); + core.setFailed(`${ERR_VALIDATION}: Comment ID not found in event payload`); return; } if (!issueNumberForComment) { - core.setFailed("Issue number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); return; } reactionEndpoint = `/repos/${owner}/${repo}/issues/comments/${commentId}/reactions`; @@ -69,7 +70,7 @@ async function main() { case "pull_request": const prNumber = context.payload?.pull_request?.number; if (!prNumber) { - core.setFailed("Pull request number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); return; } // PRs are "issues" for the reactions endpoint @@ -83,11 +84,11 @@ async function main() { const reviewCommentId = context.payload?.comment?.id; const prNumberForReviewComment = context.payload?.pull_request?.number; if (!reviewCommentId) { - core.setFailed("Review comment ID not found in event payload"); + core.setFailed(`${ERR_VALIDATION}: Review comment ID not found in event payload`); return; } if (!prNumberForReviewComment) { - core.setFailed("Pull request number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); return; } reactionEndpoint = `/repos/${owner}/${repo}/pulls/comments/${reviewCommentId}/reactions`; @@ -100,7 +101,7 @@ async function main() { case "discussion": const discussionNumber = context.payload?.discussion?.number; if (!discussionNumber) { - core.setFailed("Discussion number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); return; } // Discussions use GraphQL API - get the node ID @@ -115,13 +116,13 @@ async function main() { const discussionCommentNumber = context.payload?.discussion?.number; const discussionCommentId = context.payload?.comment?.id; if (!discussionCommentNumber || !discussionCommentId) { - core.setFailed("Discussion or comment information not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`); return; } // Get the comment node ID from the payload const commentNodeId = context.payload?.comment?.node_id; if (!commentNodeId) { - core.setFailed("Discussion comment node ID not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`); return; } reactionEndpoint = commentNodeId; // Store node ID for GraphQL @@ -131,7 +132,7 @@ async function main() { break; default: - core.setFailed(`Unsupported event type: ${eventName}`); + core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`); return; } @@ -170,7 +171,7 @@ async function main() { // For other errors, fail as before core.error(`Failed to process reaction and comment creation: ${errorMessage}`); - core.setFailed(`Failed to process reaction and comment creation: ${errorMessage}`); + core.setFailed(`${ERR_API}: Failed to process reaction and comment creation: ${errorMessage}`); } } @@ -217,7 +218,7 @@ async function addDiscussionReaction(subjectId, reaction) { const reactionContent = reactionMap[reaction]; if (!reactionContent) { - throw new Error(`Invalid reaction type for GraphQL: ${reaction}`); + throw new Error(`${ERR_VALIDATION}: Invalid reaction type for GraphQL: ${reaction}`); } const result = await github.graphql( @@ -260,7 +261,7 @@ async function getDiscussionId(owner, repo, discussionNumber) { ); if (!repository || !repository.discussion) { - throw new Error(`Discussion #${discussionNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${discussionNumber} not found in ${owner}/${repo}`); } return { @@ -280,7 +281,7 @@ async function getDiscussionId(owner, repo, discussionNumber) { async function getDiscussionCommentId(owner, repo, discussionNumber, commentId) { // First, get the discussion ID const discussion = await getDiscussionId(owner, repo, discussionNumber); - if (!discussion) throw new Error(`Discussion #${discussionNumber} not found in ${owner}/${repo}`); + if (!discussion) throw new Error(`${ERR_NOT_FOUND}: Discussion #${discussionNumber} not found in ${owner}/${repo}`); // Then fetch the comment by traversing discussion comments // Note: GitHub's GraphQL API doesn't provide a direct way to query comment by database ID @@ -297,7 +298,7 @@ async function getDiscussionCommentId(owner, repo, discussionNumber, commentId) }; } - throw new Error(`Discussion comment node ID not found in event payload for comment ${commentId}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload for comment ${commentId}`); } /** diff --git a/actions/setup/js/add_reaction_and_edit_comment.test.cjs b/actions/setup/js/add_reaction_and_edit_comment.test.cjs index e204f2cb00..dc5c1a6c3b 100644 --- a/actions/setup/js/add_reaction_and_edit_comment.test.cjs +++ b/actions/setup/js/add_reaction_and_edit_comment.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), info: vi.fn(), @@ -134,7 +135,7 @@ const mockCore = { (global.context.eventName = "discussion_comment"), (global.context.payload = { discussion: { number: 10 }, comment: { id: 123 }, repository: { html_url: "https://github.com/testowner/testrepo" } }), await eval(`(async () => { ${reactionScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion comment node ID not found in event payload"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion comment node ID not found in event payload`), expect(mockGithub.graphql).not.toHaveBeenCalled()); })); }), @@ -220,21 +221,21 @@ const mockCore = { (global.context.eventName = "discussion"), (global.context.payload = { repository: { html_url: "https://github.com/testowner/testrepo" } }), await eval(`(async () => { ${reactionScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion number not found in event payload")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion number not found in event payload`)); }), it("should handle missing discussion or comment info for discussion_comment", async () => { ((process.env.GH_AW_REACTION = "eyes"), (global.context.eventName = "discussion_comment"), (global.context.payload = { discussion: { number: 10 }, repository: { html_url: "https://github.com/testowner/testrepo" } }), await eval(`(async () => { ${reactionScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion or comment information not found in event payload")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`)); }), it("should handle unsupported event types", async () => { ((process.env.GH_AW_REACTION = "eyes"), (global.context.eventName = "push"), (global.context.payload = { repository: { html_url: "https://github.com/testowner/testrepo" } }), await eval(`(async () => { ${reactionScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("Unsupported event type: push")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Unsupported event type: push`)); }), it("should silently ignore locked issue errors (status 403)", async () => { const lockedError = new Error("Issue is locked"); diff --git a/actions/setup/js/add_workflow_run_comment.cjs b/actions/setup/js/add_workflow_run_comment.cjs index 98b22196d7..8cc5508ab2 100644 --- a/actions/setup/js/add_workflow_run_comment.cjs +++ b/actions/setup/js/add_workflow_run_comment.cjs @@ -5,6 +5,7 @@ const { getRunStartedMessage } = require("./messages_run_status.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { generateWorkflowIdMarker } = require("./generate_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); +const { ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Event type descriptions for comment messages @@ -77,7 +78,7 @@ async function main() { case "issues": { const issueNumber = context.payload?.issue?.number; if (!issueNumber) { - core.setFailed("Issue number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); return; } commentEndpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/comments`; @@ -87,7 +88,7 @@ async function main() { case "issue_comment": { const issueNumberForComment = context.payload?.issue?.number; if (!issueNumberForComment) { - core.setFailed("Issue number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in event payload`); return; } // Create new comment on the issue itself, not on the comment @@ -98,7 +99,7 @@ async function main() { case "pull_request": { const prNumber = context.payload?.pull_request?.number; if (!prNumber) { - core.setFailed("Pull request number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); return; } commentEndpoint = `/repos/${owner}/${repo}/issues/${prNumber}/comments`; @@ -108,7 +109,7 @@ async function main() { case "pull_request_review_comment": { const prNumberForReviewComment = context.payload?.pull_request?.number; if (!prNumberForReviewComment) { - core.setFailed("Pull request number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); return; } // Create new comment on the PR itself (using issues endpoint since PRs are issues) @@ -119,7 +120,7 @@ async function main() { case "discussion": { const discussionNumber = context.payload?.discussion?.number; if (!discussionNumber) { - core.setFailed("Discussion number not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); return; } commentEndpoint = `discussion:${discussionNumber}`; // Special format to indicate discussion @@ -130,7 +131,7 @@ async function main() { const discussionCommentNumber = context.payload?.discussion?.number; const discussionCommentId = context.payload?.comment?.id; if (!discussionCommentNumber || !discussionCommentId) { - core.setFailed("Discussion or comment information not found in event payload"); + core.setFailed(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`); return; } commentEndpoint = `discussion_comment:${discussionCommentNumber}:${discussionCommentId}`; // Special format @@ -138,7 +139,7 @@ async function main() { } default: - core.setFailed(`Unsupported event type: ${eventName}`); + core.setFailed(`${ERR_VALIDATION}: Unsupported event type: ${eventName}`); return; } diff --git a/actions/setup/js/add_workflow_run_comment.test.cjs b/actions/setup/js/add_workflow_run_comment.test.cjs index 7df5a52883..02d265347e 100644 --- a/actions/setup/js/add_workflow_run_comment.test.cjs +++ b/actions/setup/js/add_workflow_run_comment.test.cjs @@ -1,5 +1,6 @@ // @ts-check import { describe, it, expect, beforeEach, vi } from "vitest"; +const { ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); // Mock the global objects that GitHub Actions provides const mockCore = { @@ -139,7 +140,7 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Issue number not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Issue number not found in event payload`); expect(mockGithub.request).not.toHaveBeenCalled(); }); }); @@ -201,7 +202,7 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Pull request number not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Pull request number not found in event payload`); expect(mockGithub.request).not.toHaveBeenCalled(); }); }); @@ -260,7 +261,7 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion number not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion number not found in event payload`); }); }); @@ -303,7 +304,7 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Discussion or comment information not found in event payload"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Discussion or comment information not found in event payload`); }); }); @@ -318,7 +319,7 @@ describe("add_workflow_run_comment", () => { await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Unsupported event type: unsupported_event"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Unsupported event type: unsupported_event`); expect(mockGithub.request).not.toHaveBeenCalled(); }); }); diff --git a/actions/setup/js/assign_copilot_to_created_issues.cjs b/actions/setup/js/assign_copilot_to_created_issues.cjs index f52c38a29b..05a61c639b 100644 --- a/actions/setup/js/assign_copilot_to_created_issues.cjs +++ b/actions/setup/js/assign_copilot_to_created_issues.cjs @@ -4,6 +4,7 @@ const { AGENT_LOGIN_NAMES, findAgent, getIssueDetails, assignAgentToIssue, generatePermissionErrorSummary } = require("./assign_agent_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { sleep } = require("./error_recovery.cjs"); +const { ERR_API, ERR_PERMISSION } = require("./error_codes.cjs"); /** * Assign copilot to issues created by create_issue job. @@ -74,7 +75,7 @@ async function main() { core.info(`Looking for ${agentName} coding agent...`); agentId = await findAgent(owner, repo, agentName); if (!agentId) { - throw new Error(`${agentName} coding agent is not available for this repository`); + throw new Error(`${ERR_PERMISSION}: ${agentName} coding agent is not available for this repository`); } core.info(`Found ${agentName} coding agent (ID: ${agentId})`); } @@ -83,7 +84,7 @@ async function main() { core.info(`Getting details for issue #${issueNumber} in ${repoSlug}...`); const issueDetails = await getIssueDetails(owner, repo, issueNumber); if (!issueDetails) { - throw new Error("Failed to get issue details"); + throw new Error(`${ERR_API}: Failed to get issue details`); } core.info(`Issue ID: ${issueDetails.issueId}`); @@ -105,7 +106,7 @@ async function main() { const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null); if (!success) { - throw new Error(`Failed to assign ${agentName} via GraphQL`); + throw new Error(`${ERR_API}: Failed to assign ${agentName} via GraphQL`); } core.info(`Successfully assigned ${agentName} coding agent to issue #${issueNumber}`); @@ -164,7 +165,7 @@ async function main() { // Fail if any assignments failed if (failureCount > 0) { - core.setFailed(`Failed to assign copilot to ${failureCount} issue(s)`); + core.setFailed(`${ERR_API}: Failed to assign copilot to ${failureCount} issue(s)`); } } diff --git a/actions/setup/js/assign_issue.cjs b/actions/setup/js/assign_issue.cjs index 412ab586f6..5bbf202b1f 100644 --- a/actions/setup/js/assign_issue.cjs +++ b/actions/setup/js/assign_issue.cjs @@ -3,6 +3,7 @@ const { getAgentName, getIssueDetails, findAgent, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PERMISSION } = require("./error_codes.cjs"); /** * Assign an issue to a user or bot (including copilot) @@ -18,19 +19,19 @@ async function main() { // Check if GH_TOKEN is present if (!ghToken?.trim()) { const docsUrl = "https://github.github.com/gh-aw/reference/safe-outputs/#assigning-issues-to-copilot"; - core.setFailed(`GH_TOKEN environment variable is required but not set. This token is needed to assign issues. For more information on configuring Copilot tokens, see: ${docsUrl}`); + core.setFailed(`${ERR_CONFIG}: GH_TOKEN environment variable is required but not set. This token is needed to assign issues. For more information on configuring Copilot tokens, see: ${docsUrl}`); return; } // Validate assignee if (!assignee?.trim()) { - core.setFailed("ASSIGNEE environment variable is required but not set"); + core.setFailed(`${ERR_CONFIG}: ASSIGNEE environment variable is required but not set`); return; } // Validate issue number if (!issueNumber?.trim()) { - core.setFailed("ISSUE_NUMBER environment variable is required but not set"); + core.setFailed(`${ERR_CONFIG}: ISSUE_NUMBER environment variable is required but not set`); return; } @@ -54,14 +55,14 @@ async function main() { // Find the agent in the repository const agentId = await findAgent(owner, repo, agentName); if (!agentId) { - throw new Error(`${agentName} coding agent is not available for this repository`); + throw new Error(`${ERR_PERMISSION}: ${agentName} coding agent is not available for this repository`); } core.info(`Found ${agentName} coding agent (ID: ${agentId})`); // Get issue details const issueDetails = await getIssueDetails(owner, repo, issueNum); if (!issueDetails) { - throw new Error("Failed to get issue details"); + throw new Error(`${ERR_API}: Failed to get issue details`); } // Check if agent is already assigned @@ -72,7 +73,7 @@ async function main() { const success = await assignAgentToIssue(issueDetails.issueId, agentId, issueDetails.currentAssignees, agentName, null); if (!success) { - throw new Error(`Failed to assign ${agentName} via GraphQL`); + throw new Error(`${ERR_API}: Failed to assign ${agentName} via GraphQL`); } } } else { @@ -89,7 +90,7 @@ async function main() { } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to assign issue: ${errorMessage}`); - core.setFailed(`Failed to assign issue #${issueNum} to ${trimmedAssignee}: ${errorMessage}`); + core.setFailed(`${ERR_NOT_FOUND}: Failed to assign issue #${issueNum} to ${trimmedAssignee}: ${errorMessage}`); } } diff --git a/actions/setup/js/assign_issue.test.cjs b/actions/setup/js/assign_issue.test.cjs index 159816cd53..44fd507e43 100644 --- a/actions/setup/js/assign_issue.test.cjs +++ b/actions/setup/js/assign_issue.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_CONFIG, ERR_NOT_FOUND } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), info: vi.fn(), @@ -65,7 +66,7 @@ const mockCore = { (process.env.ISSUE_NUMBER = "123"), delete process.env.ASSIGNEE, await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("ASSIGNEE environment variable is required but not set"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ASSIGNEE environment variable is required but not set`), expect(mockExec.exec).not.toHaveBeenCalled()); }), it("should fail when ASSIGNEE is empty string", async () => { @@ -73,7 +74,7 @@ const mockCore = { (process.env.ASSIGNEE = " "), (process.env.ISSUE_NUMBER = "123"), await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("ASSIGNEE environment variable is required but not set"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ASSIGNEE environment variable is required but not set`), expect(mockExec.exec).not.toHaveBeenCalled()); }), it("should fail when ISSUE_NUMBER is not set", async () => { @@ -81,7 +82,7 @@ const mockCore = { (process.env.ASSIGNEE = "test-user"), delete process.env.ISSUE_NUMBER, await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("ISSUE_NUMBER environment variable is required but not set"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ISSUE_NUMBER environment variable is required but not set`), expect(mockExec.exec).not.toHaveBeenCalled()); }), it("should fail when ISSUE_NUMBER is empty string", async () => { @@ -89,7 +90,7 @@ const mockCore = { (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = " "), await eval(`(async () => { ${assignIssueScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("ISSUE_NUMBER environment variable is required but not set"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: ISSUE_NUMBER environment variable is required but not set`), expect(mockExec.exec).not.toHaveBeenCalled()); })); }), @@ -135,7 +136,7 @@ const mockCore = { (mockExec.exec.mockRejectedValue(testError), await eval(`(async () => { ${assignIssueScript}; await main(); })()`), expect(mockCore.error).toHaveBeenCalledWith("Failed to assign issue: User not found"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to assign issue #999 to test-user: User not found")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to assign issue #999 to test-user: User not found`)); }), it("should handle non-Error objects in catch block", async () => { ((process.env.GH_TOKEN = "ghp_test123"), (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = "999")); @@ -143,7 +144,7 @@ const mockCore = { (mockExec.exec.mockRejectedValue(stringError), await eval(`(async () => { ${assignIssueScript}; await main(); })()`), expect(mockCore.error).toHaveBeenCalledWith("Failed to assign issue: Command failed"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to assign issue #999 to test-user: Command failed")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to assign issue #999 to test-user: Command failed`)); }), it("should handle top-level errors with catch handler", async () => { ((process.env.GH_TOKEN = "ghp_test123"), (process.env.ASSIGNEE = "test-user"), (process.env.ISSUE_NUMBER = "123")); diff --git a/actions/setup/js/check_command_position.cjs b/actions/setup/js/check_command_position.cjs index 90787871aa..9feabed4db 100644 --- a/actions/setup/js/check_command_position.cjs +++ b/actions/setup/js/check_command_position.cjs @@ -1,6 +1,8 @@ // @ts-check /// +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); + /** * Check if command is the first word in the triggering text * This prevents accidental command triggers from words appearing later in content @@ -12,7 +14,7 @@ async function main() { const { getErrorMessage } = require("./error_helpers.cjs"); if (!commandsJSON) { - core.setFailed("Configuration error: GH_AW_COMMANDS not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_COMMANDS not specified.`); return; } @@ -21,16 +23,16 @@ async function main() { try { commands = JSON.parse(commandsJSON); if (!Array.isArray(commands)) { - core.setFailed("Configuration error: GH_AW_COMMANDS must be an array."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_COMMANDS must be an array.`); return; } } catch (error) { - core.setFailed(`Configuration error: Failed to parse GH_AW_COMMANDS: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_CONFIG}: Configuration error: Failed to parse GH_AW_COMMANDS: ${getErrorMessage(error)}`); return; } if (commands.length === 0) { - core.setFailed("Configuration error: No commands specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: No commands specified.`); return; } @@ -88,7 +90,7 @@ async function main() { core.setOutput("matched_command", ""); } } catch (error) { - core.setFailed(getErrorMessage(error)); + core.setFailed(`${ERR_API}: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/check_command_position.test.cjs b/actions/setup/js/check_command_position.test.cjs index 5c3d10fdce..636c1c12da 100644 --- a/actions/setup/js/check_command_position.test.cjs +++ b/actions/setup/js/check_command_position.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_CONFIG } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), info: vi.fn(), @@ -42,7 +43,9 @@ const mockCore = { void 0 !== originalEnv.GH_AW_COMMANDS ? (process.env.GH_AW_COMMANDS = originalEnv.GH_AW_COMMANDS) : delete process.env.GH_AW_COMMANDS; }), it("should fail when GH_AW_COMMANDS is not set", async () => { - (delete process.env.GH_AW_COMMANDS, await eval(`(async () => { ${checkCommandPositionScript}; await main(); })()`), expect(mockCore.setFailed).toHaveBeenCalledWith("Configuration error: GH_AW_COMMANDS not specified.")); + (delete process.env.GH_AW_COMMANDS, + await eval(`(async () => { ${checkCommandPositionScript}; await main(); })()`), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Configuration error: GH_AW_COMMANDS not specified.`)); }), it("should pass when command is the first word in issue body", async () => { ((process.env.GH_AW_COMMANDS = JSON.stringify(["test-bot"])), diff --git a/actions/setup/js/check_permissions.cjs b/actions/setup/js/check_permissions.cjs index 5ba831db3f..da433fae8c 100644 --- a/actions/setup/js/check_permissions.cjs +++ b/actions/setup/js/check_permissions.cjs @@ -2,6 +2,7 @@ /// const { parseRequiredPermissions, checkRepositoryPermission } = require("./check_permissions_utils.cjs"); +const { ERR_API, ERR_CONFIG, ERR_PERMISSION } = require("./error_codes.cjs"); async function main() { const { eventName, actor, repo } = context; @@ -26,7 +27,7 @@ async function main() { if (!requiredPermissions || requiredPermissions.length === 0) { core.error("❌ Configuration error: Required permissions not specified. Contact repository administrator."); - core.setFailed("Configuration error: Required permissions not specified"); + core.setFailed(`${ERR_CONFIG}: Configuration error: Required permissions not specified`); return; } @@ -34,14 +35,14 @@ async function main() { const result = await checkRepositoryPermission(actor, owner, repoName, requiredPermissions); if (result.error) { - core.setFailed(`Repository permission check failed: ${result.error}`); + core.setFailed(`${ERR_API}: Repository permission check failed: ${result.error}`); return; } if (!result.authorized) { // Fail the workflow when permission check fails (cancellation handled by activation job's if condition) core.warning(`Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}`); - core.setFailed(`Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}`); + core.setFailed(`${ERR_PERMISSION}: Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}`); } } diff --git a/actions/setup/js/check_skip_if_match.cjs b/actions/setup/js/check_skip_if_match.cjs index 95093621a6..d646d03c93 100644 --- a/actions/setup/js/check_skip_if_match.cjs +++ b/actions/setup/js/check_skip_if_match.cjs @@ -2,6 +2,7 @@ /// const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); async function main() { const skipQuery = process.env.GH_AW_SKIP_QUERY; @@ -9,18 +10,18 @@ async function main() { const maxMatchesStr = process.env.GH_AW_SKIP_MAX_MATCHES ?? "1"; if (!skipQuery) { - core.setFailed("Configuration error: GH_AW_SKIP_QUERY not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_QUERY not specified.`); return; } if (!workflowName) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_NAME not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_WORKFLOW_NAME not specified.`); return; } const maxMatches = parseInt(maxMatchesStr, 10); if (isNaN(maxMatches) || maxMatches < 1) { - core.setFailed(`Configuration error: GH_AW_SKIP_MAX_MATCHES must be a positive integer, got "${maxMatchesStr}".`); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_MAX_MATCHES must be a positive integer, got "${maxMatchesStr}".`); return; } @@ -50,7 +51,7 @@ async function main() { core.info(`✓ Found ${totalCount} matches (below threshold of ${maxMatches}), workflow can proceed`); core.setOutput("skip_check_ok", "true"); } catch (error) { - core.setFailed(`Failed to execute search query: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_API}: Failed to execute search query: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/check_skip_if_no_match.cjs b/actions/setup/js/check_skip_if_no_match.cjs index 8198affe66..083ea68eb1 100644 --- a/actions/setup/js/check_skip_if_no_match.cjs +++ b/actions/setup/js/check_skip_if_no_match.cjs @@ -2,23 +2,24 @@ /// const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); async function main() { const { GH_AW_SKIP_QUERY: skipQuery, GH_AW_WORKFLOW_NAME: workflowName, GH_AW_SKIP_MIN_MATCHES: minMatchesStr = "1" } = process.env; if (!skipQuery) { - core.setFailed("Configuration error: GH_AW_SKIP_QUERY not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_QUERY not specified.`); return; } if (!workflowName) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_NAME not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_WORKFLOW_NAME not specified.`); return; } const minMatches = parseInt(minMatchesStr, 10); if (isNaN(minMatches) || minMatches < 1) { - core.setFailed(`Configuration error: GH_AW_SKIP_MIN_MATCHES must be a positive integer, got "${minMatchesStr}".`); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_MIN_MATCHES must be a positive integer, got "${minMatchesStr}".`); return; } @@ -49,7 +50,7 @@ async function main() { core.info(`✓ Found ${totalCount} matches (meets or exceeds minimum of ${minMatches}), workflow can proceed`); core.setOutput("skip_no_match_check_ok", "true"); } catch (error) { - core.setFailed(`Failed to execute search query: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_API}: Failed to execute search query: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/check_skip_if_no_match.test.cjs b/actions/setup/js/check_skip_if_no_match.test.cjs index 3959ecd618..449010078a 100644 --- a/actions/setup/js/check_skip_if_no_match.test.cjs +++ b/actions/setup/js/check_skip_if_no_match.test.cjs @@ -1,6 +1,7 @@ // @ts-check import { describe, it, expect, beforeEach } from "vitest"; const { main } = require("./check_skip_if_no_match.cjs"); +const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); describe("check_skip_if_no_match", () => { let mockCore; @@ -62,7 +63,7 @@ describe("check_skip_if_no_match", () => { await main(); - expect(mockCore.errors).toContain("Configuration error: GH_AW_SKIP_QUERY not specified."); + expect(mockCore.errors).toContain(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_QUERY not specified.`); }); it("should fail when GH_AW_WORKFLOW_NAME is not specified", async () => { @@ -71,7 +72,7 @@ describe("check_skip_if_no_match", () => { await main(); - expect(mockCore.errors).toContain("Configuration error: GH_AW_WORKFLOW_NAME not specified."); + expect(mockCore.errors).toContain(`${ERR_CONFIG}: Configuration error: GH_AW_WORKFLOW_NAME not specified.`); }); it("should fail when GH_AW_SKIP_MIN_MATCHES is not a valid positive integer", async () => { @@ -81,7 +82,7 @@ describe("check_skip_if_no_match", () => { await main(); - expect(mockCore.errors).toContain('Configuration error: GH_AW_SKIP_MIN_MATCHES must be a positive integer, got "invalid".'); + expect(mockCore.errors).toContain(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_MIN_MATCHES must be a positive integer, got "invalid".`); }); it("should fail when GH_AW_SKIP_MIN_MATCHES is zero", async () => { @@ -91,7 +92,7 @@ describe("check_skip_if_no_match", () => { await main(); - expect(mockCore.errors).toContain('Configuration error: GH_AW_SKIP_MIN_MATCHES must be a positive integer, got "0".'); + expect(mockCore.errors).toContain(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_MIN_MATCHES must be a positive integer, got "0".`); }); it("should use default min matches of 1 when not specified", async () => { @@ -195,7 +196,7 @@ describe("check_skip_if_no_match", () => { await main(); - expect(mockCore.errors).toContain("Failed to execute search query: API rate limit exceeded"); + expect(mockCore.errors).toContain(`${ERR_API}: Failed to execute search query: API rate limit exceeded`); }); it("should log info messages during execution", async () => { diff --git a/actions/setup/js/check_stop_time.cjs b/actions/setup/js/check_stop_time.cjs index 4e86da0f8a..6addb4c216 100644 --- a/actions/setup/js/check_stop_time.cjs +++ b/actions/setup/js/check_stop_time.cjs @@ -1,17 +1,18 @@ // @ts-check /// +const { ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); async function main() { const stopTime = process.env.GH_AW_STOP_TIME; const workflowName = process.env.GH_AW_WORKFLOW_NAME; if (!stopTime) { - core.setFailed("Configuration error: GH_AW_STOP_TIME not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_STOP_TIME not specified.`); return; } if (!workflowName) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_NAME not specified."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_WORKFLOW_NAME not specified.`); return; } @@ -21,7 +22,7 @@ async function main() { const stopTimeDate = new Date(stopTime); if (isNaN(stopTimeDate.getTime())) { - core.setFailed(`Invalid stop-time format: ${stopTime}. Expected format: YYYY-MM-DD HH:MM:SS`); + core.setFailed(`${ERR_VALIDATION}: Invalid stop-time format: ${stopTime}. Expected format: YYYY-MM-DD HH:MM:SS`); return; } diff --git a/actions/setup/js/check_team_member.cjs b/actions/setup/js/check_team_member.cjs index e9fe8d138d..5d24eeb4fc 100644 --- a/actions/setup/js/check_team_member.cjs +++ b/actions/setup/js/check_team_member.cjs @@ -5,6 +5,7 @@ * Check if user has admin or maintainer permissions * @returns {Promise} */ +const { ERR_PERMISSION } = require("./error_codes.cjs"); async function main() { const actor = context.actor; const { owner, repo } = context.repo; @@ -34,7 +35,7 @@ async function main() { // Fail the workflow when team membership check fails (cancellation handled by activation job's if condition) core.warning(`Access denied: Only authorized team members can trigger this workflow. User '${actor}' is not authorized.`); - core.setFailed(`Access denied: User '${actor}' is not authorized for this workflow`); + core.setFailed(`${ERR_PERMISSION}: Access denied: User '${actor}' is not authorized for this workflow`); core.setOutput("is_team_member", "false"); } diff --git a/actions/setup/js/check_workflow_timestamp.cjs b/actions/setup/js/check_workflow_timestamp.cjs index 5fd61d1587..faab4288b0 100644 --- a/actions/setup/js/check_workflow_timestamp.cjs +++ b/actions/setup/js/check_workflow_timestamp.cjs @@ -9,18 +9,19 @@ const fs = require("fs"); const path = require("path"); +const { ERR_CONFIG } = require("./error_codes.cjs"); async function main() { const workspace = process.env.GITHUB_WORKSPACE; const workflowFile = process.env.GH_AW_WORKFLOW_FILE; if (!workspace) { - core.setFailed("Configuration error: GITHUB_WORKSPACE not available."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GITHUB_WORKSPACE not available.`); return; } if (!workflowFile) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_WORKFLOW_FILE not available.`); return; } diff --git a/actions/setup/js/check_workflow_timestamp_api.cjs b/actions/setup/js/check_workflow_timestamp_api.cjs index a3cd2d3f27..f77d5cd46f 100644 --- a/actions/setup/js/check_workflow_timestamp_api.cjs +++ b/actions/setup/js/check_workflow_timestamp_api.cjs @@ -10,12 +10,13 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { extractHashFromLockFile, computeFrontmatterHash, createGitHubFileReader } = require("./frontmatter_hash_pure.cjs"); const { getFileContent } = require("./github_api_helpers.cjs"); +const { ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); async function main() { const workflowFile = process.env.GH_AW_WORKFLOW_FILE; if (!workflowFile) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available."); + core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_WORKFLOW_FILE not available.`); return; } @@ -159,7 +160,7 @@ async function main() { await summary.write(); // Fail the step to prevent workflow from running with outdated configuration - core.setFailed(warningMessage); + core.setFailed(`${ERR_CONFIG}: ${warningMessage}`); } else if (hashComparison.match) { // Hashes match - lock file is up to date despite timestamp difference core.info("✅ Lock file is up to date (frontmatter hashes match despite timestamp difference)"); @@ -189,7 +190,7 @@ async function main() { await summary.write(); // Fail the step to prevent workflow from running with outdated configuration - core.setFailed(warningMessage); + core.setFailed(`${ERR_CONFIG}: ${warningMessage}`); } } else if (workflowCommit.sha === lockCommit.sha) { // Same commit - definitely up to date @@ -233,7 +234,7 @@ async function main() { await summary.write(); // Fail the step to prevent workflow from running with outdated configuration - core.setFailed(warningMessage); + core.setFailed(`${ERR_CONFIG}: ${warningMessage}`); } } else { // Lock file is newer than workflow file diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs index 4fad71ceac..e48e687906 100644 --- a/actions/setup/js/checkout_pr_branch.cjs +++ b/actions/setup/js/checkout_pr_branch.cjs @@ -29,6 +29,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { renderTemplate } = require("./messages_core.cjs"); const { detectForkPR } = require("./pr_helpers.cjs"); const fs = require("fs"); +const { ERR_API } = require("./error_codes.cjs"); /** * Log detailed PR context information for debugging @@ -231,7 +232,7 @@ Pull request #${pullRequest.number} is closed. The checkout failed because the b }); await core.summary.addRaw(summaryContent).write(); - core.setFailed(`Failed to checkout PR branch: ${errorMsg}`); + core.setFailed(`${ERR_API}: Failed to checkout PR branch: ${errorMsg}`); } } diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs index 6466264573..bdaa893645 100644 --- a/actions/setup/js/checkout_pr_branch.test.cjs +++ b/actions/setup/js/checkout_pr_branch.test.cjs @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; - +const { ERR_API } = require("./error_codes.cjs"); describe("checkout_pr_branch.cjs", () => { let mockCore; let mockExec; @@ -153,6 +153,9 @@ If the pull request is still open, verify that: }, }; } + if (module === "./error_codes.cjs") { + return require("./error_codes.cjs"); + } throw new Error(`Module ${module} not mocked in test`); }; @@ -206,7 +209,7 @@ If the pull request is still open, verify that: expect(summaryCall).toContain("git fetch failed"); expect(summaryCall).toContain("pull request has been closed"); - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: git fetch failed"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: git fetch failed`); }); it("should handle git checkout errors", async () => { @@ -222,7 +225,7 @@ If the pull request is still open, verify that: expect(summaryCall).toContain("Failed to Checkout PR Branch"); expect(summaryCall).toContain("git checkout failed"); - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: git checkout failed"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: git checkout failed`); }); }); @@ -266,7 +269,7 @@ If the pull request is still open, verify that: expect(summaryCall).toContain("gh pr checkout failed"); expect(summaryCall).toContain("pull request has been closed"); - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: gh pr checkout failed"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: gh pr checkout failed`); }); it("should pass environment variables to gh command", async () => { @@ -355,7 +358,7 @@ If the pull request is still open, verify that: await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: string error"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: string error`); }); it("should handle errors with custom messages", async () => { @@ -364,7 +367,7 @@ If the pull request is still open, verify that: await runScript(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: Permission denied: unable to access repository"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: Permission denied: unable to access repository`); }); }); @@ -420,7 +423,7 @@ If the pull request is still open, verify that: await runScript(); expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "false"); - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: checkout failed"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: checkout failed`); }); it("should set output to true when no PR context", async () => { @@ -630,7 +633,7 @@ If the pull request is still open, verify that: expect(mockCore.error).toHaveBeenCalledWith("Event type: pull_request"); // Should fail the step - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to checkout PR branch: network error"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Failed to checkout PR branch: network error`); expect(mockCore.setOutput).toHaveBeenCalledWith("checkout_pr_success", "false"); }); diff --git a/actions/setup/js/close_discussion.cjs b/actions/setup/js/close_discussion.cjs index 10d580947e..4e929453ea 100644 --- a/actions/setup/js/close_discussion.cjs +++ b/actions/setup/js/close_discussion.cjs @@ -8,6 +8,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); /** * Get discussion details using GraphQL with pagination for labels @@ -53,7 +54,7 @@ async function getDiscussionDetails(github, owner, repo, discussionNumber) { ); if (!query?.repository?.discussion) { - throw new Error(`Discussion #${discussionNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${discussionNumber} not found in ${owner}/${repo}`); } // Store the discussion metadata from the first query @@ -75,7 +76,7 @@ async function getDiscussionDetails(github, owner, repo, discussionNumber) { } if (!discussion) { - throw new Error(`Failed to fetch discussion #${discussionNumber}`); + throw new Error(`${ERR_NOT_FOUND}: Failed to fetch discussion #${discussionNumber}`); } return { diff --git a/actions/setup/js/close_issue.cjs b/actions/setup/js/close_issue.cjs index 02d5deff95..47ca716ab2 100644 --- a/actions/setup/js/close_issue.cjs +++ b/actions/setup/js/close_issue.cjs @@ -9,6 +9,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); /** * Get issue details using REST API @@ -26,7 +27,7 @@ async function getIssueDetails(github, owner, repo, issueNumber) { }); if (!issue) { - throw new Error(`Issue #${issueNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Issue #${issueNumber} not found in ${owner}/${repo}`); } return issue; diff --git a/actions/setup/js/close_pull_request.cjs b/actions/setup/js/close_pull_request.cjs index 5712039bd2..4a64ba512c 100644 --- a/actions/setup/js/close_pull_request.cjs +++ b/actions/setup/js/close_pull_request.cjs @@ -6,6 +6,7 @@ const { getTrackerID } = require("./get_tracker_id.cjs"); const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -29,7 +30,7 @@ async function getPullRequestDetails(github, owner, repo, prNumber) { }); if (!pr) { - throw new Error(`Pull request #${prNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Pull request #${prNumber} not found in ${owner}/${repo}`); } return pr; diff --git a/actions/setup/js/collect_ndjson_output.cjs b/actions/setup/js/collect_ndjson_output.cjs index c7410d351b..2e84d5aa00 100644 --- a/actions/setup/js/collect_ndjson_output.cjs +++ b/actions/setup/js/collect_ndjson_output.cjs @@ -4,6 +4,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { repairJson, sanitizePrototypePollution } = require("./json_repair_helpers.cjs"); const { AGENT_OUTPUT_FILENAME, TMP_GH_AW_PATH } = require("./constants.cjs"); +const { ERR_API, ERR_PARSE } = require("./error_codes.cjs"); async function main() { try { @@ -141,7 +142,7 @@ async function main() { core.info(`invalid input json: ${jsonStr}`); const originalMsg = originalError instanceof Error ? originalError.message : String(originalError); const repairMsg = repairError instanceof Error ? repairError.message : String(repairError); - throw new Error(`JSON parsing failed. Original: ${originalMsg}. After attempted repair: ${repairMsg}`); + throw new Error(`${ERR_PARSE}: JSON parsing failed. Original: ${originalMsg}. After attempted repair: ${repairMsg}`); } } } @@ -361,7 +362,7 @@ async function main() { core.setOutput("output", ""); core.setOutput("output_types", ""); core.setOutput("has_patch", "false"); - core.setFailed(`Agent output ingestion failed: ${errorMsg}`); + core.setFailed(`${ERR_API}: Agent output ingestion failed: ${errorMsg}`); throw error; } } diff --git a/actions/setup/js/create_project.cjs b/actions/setup/js/create_project.cjs index 59d88ccf22..e5dceffca4 100644 --- a/actions/setup/js/create_project.cjs +++ b/actions/setup/js/create_project.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { normalizeTemporaryId, isTemporaryId, generateTemporaryId, getOrGenerateTemporaryId } = require("./temporary_id.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_CONFIG, ERR_NOT_FOUND, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Log detailed GraphQL error information @@ -161,12 +162,12 @@ async function getIssueNodeId(owner, repo, issueNumber) { */ function parseProjectUrl(projectUrl) { if (!projectUrl || typeof projectUrl !== "string") { - throw new Error(`Invalid project URL: expected string, got ${typeof projectUrl}`); + throw new Error(`${ERR_VALIDATION}: Invalid project URL: expected string, got ${typeof projectUrl}`); } const match = projectUrl.match(/github\.com\/(users|orgs)\/([^/]+)\/projects\/(\d+)/); if (!match) { - throw new Error(`Invalid project URL: "${projectUrl}". Expected format: https://github.com/orgs/myorg/projects/123`); + throw new Error(`${ERR_VALIDATION}: Invalid project URL: "${projectUrl}". Expected format: https://github.com/orgs/myorg/projects/123`); } return { @@ -224,12 +225,12 @@ async function createProjectView(projectUrl, viewConfig) { const name = typeof viewConfig.name === "string" ? viewConfig.name.trim() : ""; if (!name) { - throw new Error("View name is required and must be a non-empty string"); + throw new Error(`${ERR_VALIDATION}: View name is required and must be a non-empty string`); } const layout = typeof viewConfig.layout === "string" ? viewConfig.layout.trim() : ""; if (!layout || !["table", "board", "roadmap"].includes(layout)) { - throw new Error(`Invalid view layout "${layout}". Must be one of: table, board, roadmap`); + throw new Error(`${ERR_VALIDATION}: Invalid view layout "${layout}". Must be one of: table, board, roadmap`); } const filter = typeof viewConfig.filter === "string" ? viewConfig.filter : undefined; @@ -238,7 +239,7 @@ async function createProjectView(projectUrl, viewConfig) { if (visibleFields) { const invalid = visibleFields.filter(v => typeof v !== "number" || !Number.isFinite(v)); if (invalid.length > 0) { - throw new Error(`Invalid visible_fields. Must be an array of numbers (field IDs). Invalid values: ${invalid.map(v => JSON.stringify(v)).join(", ")}`); + throw new Error(`${ERR_VALIDATION}: Invalid visible_fields. Must be an array of numbers (field IDs). Invalid values: ${invalid.map(v => JSON.stringify(v)).join(", ")}`); } } @@ -305,7 +306,7 @@ async function main(config = {}, githubClient = null) { const github = githubClient || global.github; if (!github) { - throw new Error("GitHub client is required but not provided. Either pass a github client to main() or ensure global.github is set by github-script action."); + throw new Error(`${ERR_CONFIG}: GitHub client is required but not provided. Either pass a github client to main() or ensure global.github is set by github-script action.`); } if (defaultTargetOwner) { @@ -378,7 +379,7 @@ async function main(config = {}, githubClient = null) { core.info(`Resolved temporary ID ${tempIdStr} in item_url to ${resolvedUrl}`); item_url = resolvedUrl; } else { - throw new Error(`Temporary ID '${tempIdStr}' in item_url not found. Ensure create_issue was called before create_project.`); + throw new Error(`${ERR_NOT_FOUND}: Temporary ID '${tempIdStr}' in item_url not found. Ensure create_issue was called before create_project.`); } } } @@ -399,14 +400,14 @@ async function main(config = {}, githubClient = null) { title = `${titlePrefix} #${issueNumber}`; core.info(`Generated title from issue number: "${title}"`); } else { - throw new Error("Missing required field 'title' in create_project call and unable to generate from context"); + throw new Error(`${ERR_VALIDATION}: Missing required field 'title' in create_project call and unable to generate from context`); } } // Determine owner - use explicit owner, default, or error const targetOwner = owner || defaultTargetOwner; if (!targetOwner) { - throw new Error("No owner specified and no default target-owner configured. Either provide 'owner' field or configure 'target-owner' in safe-outputs.create-project"); + throw new Error(`${ERR_VALIDATION}: No owner specified and no default target-owner configured. Either provide 'owner' field or configure 'target-owner' in safe-outputs.create-project`); } // Determine owner type (org or user) diff --git a/actions/setup/js/create_project_status_update.cjs b/actions/setup/js/create_project_status_update.cjs index 9100c79c09..65e6d70949 100644 --- a/actions/setup/js/create_project_status_update.cjs +++ b/actions/setup/js/create_project_status_update.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_CONFIG, ERR_NOT_FOUND, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -57,12 +58,12 @@ function logGraphQLError(error, operation) { */ function parseProjectUrl(projectUrl) { if (!projectUrl || typeof projectUrl !== "string") { - throw new Error(`Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`); + throw new Error(`${ERR_VALIDATION}: Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`); } const match = projectUrl.match(/^https:\/\/[^/]+\/(users|orgs)\/([^/]+)\/projects\/(\d+)/); if (!match) { - throw new Error(`Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`); + throw new Error(`${ERR_VALIDATION}: Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`); } return { @@ -209,7 +210,9 @@ async function resolveProjectV2(projectInfo, projectNumberInt) { } catch (fallbackError) { // Both direct query and fallback list query failed - this could be a transient API error const who = projectInfo.scope === "orgs" ? `org ${projectInfo.ownerLogin}` : `user ${projectInfo.ownerLogin}`; - throw new Error(`Unable to resolve project #${projectNumberInt} for ${who}. Both direct projectV2 query and fallback projectsV2 list query failed. This may be a transient GitHub API error. Error: ${getErrorMessage(fallbackError)}`); + throw new Error( + `${ERR_NOT_FOUND}: Unable to resolve project #${projectNumberInt} for ${who}. Both direct projectV2 query and fallback projectsV2 list query failed. This may be a transient GitHub API error. Error: ${getErrorMessage(fallbackError)}` + ); } const nodes = Array.isArray(list.nodes) ? list.nodes : []; @@ -221,7 +224,7 @@ async function resolveProjectV2(projectInfo, projectNumberInt) { const total = typeof list.totalCount === "number" ? ` (totalCount=${list.totalCount})` : ""; const who = projectInfo.scope === "orgs" ? `org ${projectInfo.ownerLogin}` : `user ${projectInfo.ownerLogin}`; - throw new Error(`Project #${projectNumberInt} not found or not accessible for ${who}.${total} Accessible Projects v2: ${summary}`); + throw new Error(`${ERR_NOT_FOUND}: Project #${projectNumberInt} not found or not accessible for ${who}.${total} Accessible Projects v2: ${summary}`); } /** @@ -291,7 +294,7 @@ async function main(config = {}, githubClient = null) { const github = githubClient || global.github; if (!github) { - throw new Error("GitHub client is required but not provided. Either pass a github client to main() or ensure global.github is set by github-script action."); + throw new Error(`${ERR_CONFIG}: GitHub client is required but not provided. Either pass a github client to main() or ensure global.github is set by github-script action.`); } core.info(`Max count: ${maxCount}`); @@ -351,7 +354,7 @@ async function main(config = {}, githubClient = null) { const projectNumberInt = parseInt(projectInfo.projectNumber, 10); if (!Number.isFinite(projectNumberInt)) { - throw new Error(`Invalid project number parsed from URL: ${projectInfo.projectNumber}`); + throw new Error(`${ERR_PARSE}: Invalid project number parsed from URL: ${projectInfo.projectNumber}`); } const project = await resolveProjectV2(projectInfo, projectNumberInt); diff --git a/actions/setup/js/error_codes.cjs b/actions/setup/js/error_codes.cjs new file mode 100644 index 0000000000..07ac2798e9 --- /dev/null +++ b/actions/setup/js/error_codes.cjs @@ -0,0 +1,53 @@ +// @ts-check + +/** + * Standardized error codes for safe-outputs handlers. + * + * These codes provide machine-readable prefixes for error messages, + * enabling structured logging, monitoring dashboards, and alerting rules. + * + * Usage: + * const { ERR_VALIDATION } = require("./error_codes.cjs"); + * throw new Error(`${ERR_VALIDATION}: Missing required field: title`); + * core.setFailed(`${ERR_CONFIG}: GH_AW_PROMPT environment variable is not set`); + * + * Error code categories: + * ERR_VALIDATION - Input validation failures (missing fields, invalid format, limits exceeded) + * ERR_PERMISSION - Authorization and permission check failures + * ERR_API - GitHub API call failures + * ERR_CONFIG - Configuration errors (missing env vars, bad setup) + * ERR_NOT_FOUND - Resource not found (issues, discussions, PRs) + * ERR_PARSE - Parsing failures (JSON, NDJSON, log formats) + * ERR_SYSTEM - System and I/O errors (file access, git operations) + */ + +/** @type {string} Input validation failures */ +const ERR_VALIDATION = "ERR_VALIDATION"; + +/** @type {string} Authorization and permission check failures */ +const ERR_PERMISSION = "ERR_PERMISSION"; + +/** @type {string} GitHub API call failures */ +const ERR_API = "ERR_API"; + +/** @type {string} Configuration errors (missing env vars, bad setup) */ +const ERR_CONFIG = "ERR_CONFIG"; + +/** @type {string} Resource not found */ +const ERR_NOT_FOUND = "ERR_NOT_FOUND"; + +/** @type {string} Parsing failures (JSON, NDJSON, log formats) */ +const ERR_PARSE = "ERR_PARSE"; + +/** @type {string} System and I/O errors */ +const ERR_SYSTEM = "ERR_SYSTEM"; + +module.exports = { + ERR_VALIDATION, + ERR_PERMISSION, + ERR_API, + ERR_CONFIG, + ERR_NOT_FOUND, + ERR_PARSE, + ERR_SYSTEM, +}; diff --git a/actions/setup/js/error_recovery.cjs b/actions/setup/js/error_recovery.cjs index 14da05572e..b3a5fffded 100644 --- a/actions/setup/js/error_recovery.cjs +++ b/actions/setup/js/error_recovery.cjs @@ -7,6 +7,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API } = require("./error_codes.cjs"); /** * Configuration for retry behavior @@ -145,13 +146,14 @@ async function withRetry(operation, config = {}, operationName = "operation") { * @param {number} [context.maxRetries] - Maximum retry attempts * @param {boolean} context.retryable - Whether the error is retryable * @param {string} context.suggestion - Suggestion for resolving the error + * @param {string} [context.code] - Optional standardized error code (e.g., ERR_API) * @returns {Error} Enhanced error with context */ function enhanceError(error, context) { const originalMessage = getErrorMessage(error); const timestamp = new Date().toISOString(); - let enhancedMessage = `[${timestamp}] ${context.operation} failed`; + let enhancedMessage = `${context.code || ERR_API}: [${timestamp}] ${context.operation} failed`; if (context.maxRetries !== undefined) { enhancedMessage += ` after ${context.maxRetries} retry attempts`; diff --git a/actions/setup/js/file_helpers.cjs b/actions/setup/js/file_helpers.cjs index 189a710e33..b797995002 100644 --- a/actions/setup/js/file_helpers.cjs +++ b/actions/setup/js/file_helpers.cjs @@ -12,6 +12,7 @@ const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_SYSTEM } = require("./error_codes.cjs"); /** * List all files recursively in a directory @@ -72,7 +73,7 @@ function checkFileExists(filePath, artifactDir, fileDescription, required) { core.info(" Found " + files.length + " file(s):"); files.forEach(file => core.info(" - " + file)); } - core.setFailed("❌ " + fileDescription + " not found at: " + filePath); + core.setFailed(`${ERR_SYSTEM}: ❌ ${fileDescription} not found at: ${filePath}`); return false; } else { core.info("No " + fileDescription.toLowerCase() + " found at: " + filePath); diff --git a/actions/setup/js/frontmatter_hash_pure.cjs b/actions/setup/js/frontmatter_hash_pure.cjs index 003afd5cca..bfeae067cd 100644 --- a/actions/setup/js/frontmatter_hash_pure.cjs +++ b/actions/setup/js/frontmatter_hash_pure.cjs @@ -3,6 +3,7 @@ const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); +const { ERR_PARSE, ERR_SYSTEM } = require("./error_codes.cjs"); /** * Default file reader using Node.js fs module @@ -96,7 +97,7 @@ function extractFrontmatterAndBody(content) { } if (endIndex === -1) { - throw new Error("Frontmatter not properly closed"); + throw new Error(`${ERR_PARSE}: Frontmatter not properly closed`); } const frontmatterText = lines.slice(1, endIndex).join("\n"); @@ -348,7 +349,7 @@ function createGitHubFileReader(github, owner, repo, ref) { return response.data.content; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to read file ${filePath} from GitHub: ${errorMessage}`); + throw new Error(`${ERR_SYSTEM}: Failed to read file ${filePath} from GitHub: ${errorMessage}`); } }; } diff --git a/actions/setup/js/get_current_branch.cjs b/actions/setup/js/get_current_branch.cjs index b0a5e0f2a2..353ce866a7 100644 --- a/actions/setup/js/get_current_branch.cjs +++ b/actions/setup/js/get_current_branch.cjs @@ -2,6 +2,7 @@ /// const { execSync } = require("child_process"); +const { ERR_CONFIG } = require("./error_codes.cjs"); /** * Get the current git branch name @@ -36,7 +37,7 @@ function getCurrentBranch() { return ghRefName; } - throw new Error("Failed to determine current branch: git command failed and no GitHub environment variables available"); + throw new Error(`${ERR_CONFIG}: Failed to determine current branch: git command failed and no GitHub environment variables available`); } module.exports = { diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index cff8eea239..f8c94c48d3 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -2,6 +2,7 @@ /// const { spawnSync } = require("child_process"); +const { ERR_SYSTEM } = require("./error_codes.cjs"); /** * Safely execute git command using spawnSync with args array to prevent shell injection @@ -39,7 +40,7 @@ function execGitSync(args, options = {}) { } if (result.status !== 0) { - const errorMsg = result.stderr || `Git command failed with status ${result.status}`; + const errorMsg = `${ERR_SYSTEM}: ${result.stderr || `Git command failed with status ${result.status}`}`; if (typeof core !== "undefined" && core.error) { core.error(`Git command failed: ${gitCommand}`); core.error(`Exit status: ${result.status}`); diff --git a/actions/setup/js/interpolate_prompt.cjs b/actions/setup/js/interpolate_prompt.cjs index 1484c8beef..0002a80587 100644 --- a/actions/setup/js/interpolate_prompt.cjs +++ b/actions/setup/js/interpolate_prompt.cjs @@ -9,6 +9,7 @@ const fs = require("fs"); const { isTruthy } = require("./is_truthy.cjs"); const { processRuntimeImports } = require("./runtime_import.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Interpolates variables in the prompt content @@ -141,7 +142,7 @@ async function main() { const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) { - core.setFailed("GH_AW_PROMPT environment variable is not set"); + core.setFailed(`${ERR_CONFIG}: GH_AW_PROMPT environment variable is not set`); return; } core.info(`[main] Prompt path: ${promptPath}`); @@ -149,7 +150,7 @@ async function main() { // Get the workspace directory for runtime imports const workspaceDir = process.env.GITHUB_WORKSPACE; if (!workspaceDir) { - core.setFailed("GITHUB_WORKSPACE environment variable is not set"); + core.setFailed(`${ERR_CONFIG}: GITHUB_WORKSPACE environment variable is not set`); return; } core.info(`[main] Workspace directory: ${workspaceDir}`); @@ -256,7 +257,7 @@ async function main() { if (err.stack) { core.info(`[main] Stack trace:\n${err.stack}`); } - core.setFailed(getErrorMessage(error)); + core.setFailed(`${ERR_API}: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/interpolate_prompt.test.cjs b/actions/setup/js/interpolate_prompt.test.cjs index f3096787f3..031e6a8e93 100644 --- a/actions/setup/js/interpolate_prompt.test.cjs +++ b/actions/setup/js/interpolate_prompt.test.cjs @@ -3,6 +3,7 @@ import fs from "fs"; import path from "path"; import os from "os"; import { fileURLToPath } from "url"; +const { ERR_CONFIG } = require("./error_codes.cjs"); const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename), core = { info: vi.fn(), setFailed: vi.fn() }; @@ -118,7 +119,7 @@ describe("interpolate_prompt", () => { const mainMatch = interpolatePromptScript.match(/async function main\(\)\s*{[\s\S]*?^}/m); if (!mainMatch) throw new Error("Could not extract main function"); const main = eval(`(${mainMatch[0]})`); - (main(), expect(core.setFailed).toHaveBeenCalledWith("GH_AW_PROMPT environment variable is not set")); + (main(), expect(core.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: GH_AW_PROMPT environment variable is not set`)); })); })); }); diff --git a/actions/setup/js/lock-issue.cjs b/actions/setup/js/lock-issue.cjs index dae96e3395..996789903f 100644 --- a/actions/setup/js/lock-issue.cjs +++ b/actions/setup/js/lock-issue.cjs @@ -8,6 +8,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); async function main() { // Log actor and event information for debugging @@ -17,7 +18,7 @@ async function main() { const issueNumber = context.issue.number; if (!issueNumber) { - core.setFailed("Issue number not found in context"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in context`); return; } @@ -63,7 +64,7 @@ async function main() { } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to lock issue: ${errorMessage}`); - core.setFailed(`Failed to lock issue #${issueNumber}: ${errorMessage}`); + core.setFailed(`${ERR_NOT_FOUND}: Failed to lock issue #${issueNumber}: ${errorMessage}`); core.setOutput("locked", "false"); } } diff --git a/actions/setup/js/lock-issue.test.cjs b/actions/setup/js/lock-issue.test.cjs index 39bfa7c74e..0b58fe9150 100644 --- a/actions/setup/js/lock-issue.test.cjs +++ b/actions/setup/js/lock-issue.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn(), setFailed: vi.fn(), setOutput: vi.fn() }, mockGithub = { rest: { issues: { get: vi.fn(), lock: vi.fn() } } }, mockContext = { eventName: "issues", runId: 12345, repo: { owner: "testowner", repo: "testrepo" }, issue: { number: 42 }, payload: { issue: { number: 42 }, repository: { html_url: "https://github.com/testowner/testrepo" } } }; @@ -51,7 +52,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn delete global.context.payload.issue, await eval(`(async () => { ${lockIssueScript}; await main(); })()`), expect(mockGithub.rest.issues.lock).not.toHaveBeenCalled(), - expect(mockCore.setFailed).toHaveBeenCalledWith("Issue number not found in context")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Issue number not found in context`)); }), it("should handle API errors gracefully", async () => { mockGithub.rest.issues.get.mockResolvedValue({ data: { number: 42, locked: !1 } }); @@ -60,7 +61,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn await eval(`(async () => { ${lockIssueScript}; await main(); })()`), expect(mockGithub.rest.issues.lock).toHaveBeenCalled(), expect(mockCore.error).toHaveBeenCalledWith("Failed to lock issue: API rate limit exceeded"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to lock issue #42: API rate limit exceeded"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to lock issue #42: API rate limit exceeded`), expect(mockCore.setOutput).toHaveBeenCalledWith("locked", "false")); }), it("should handle non-Error exceptions", async () => { @@ -68,7 +69,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn mockGithub.rest.issues.lock.mockRejectedValue("String error"), await eval(`(async () => { ${lockIssueScript}; await main(); })()`), expect(mockCore.error).toHaveBeenCalledWith("Failed to lock issue: String error"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to lock issue #42: String error"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to lock issue #42: String error`), expect(mockCore.setOutput).toHaveBeenCalledWith("locked", "false")); }), it("should work with different issue numbers", async () => { diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index f6d4e38616..3168b349fe 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -3,6 +3,7 @@ const { generatePlainTextSummary, generateCopilotCliStyleSummary, wrapAgentLogInSection, formatSafeOutputsPreview } = require("./log_parser_shared.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Bootstrap helper for log parser entry points. @@ -177,21 +178,21 @@ async function runLogParser(options) { // Claude-specific guardrail: if no structured log entries were parsed, treat as execution failure. // This catches silent startup failures where Claude exits before producing JSON tool activity. if (parserName === "Claude" && (!logEntries || logEntries.length === 0)) { - core.setFailed("Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution."); + core.setFailed(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); } // Handle MCP server failures if present if (mcpFailures && mcpFailures.length > 0) { const failedServers = mcpFailures.join(", "); - core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + core.setFailed(`${ERR_API}: MCP server(s) failed to launch: ${failedServers}`); } // Handle max-turns limit if hit if (maxTurnsHit) { - core.setFailed(`Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); + core.setFailed(`${ERR_VALIDATION}: Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); } } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); + core.setFailed(`${ERR_API}: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index 9ce177df88..3e9cd8c9ff 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename); describe("log_parser_bootstrap.cjs", () => { @@ -70,7 +71,7 @@ describe("log_parser_bootstrap.cjs", () => { process.env.GH_AW_AGENT_OUTPUT = logFile; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); - expect(mockCore.setFailed).toHaveBeenCalledWith("Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution."); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); } finally { fs.unlinkSync(logFile); fs.rmdirSync(tmpDir); @@ -109,7 +110,7 @@ describe("log_parser_bootstrap.cjs", () => { logFile = path.join(tmpDir, "test.log"); (fs.writeFileSync(logFile, "content"), (process.env.GH_AW_AGENT_OUTPUT = logFile)); const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: ["server1", "server2"], maxTurnsHit: !1 }); - (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), expect(mockCore.setFailed).toHaveBeenCalledWith("MCP server(s) failed to launch: server1, server2"), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); + (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: MCP server(s) failed to launch: server1, server2`), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); }), it("should handle max-turns limit reached", () => { const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")), @@ -117,7 +118,7 @@ describe("log_parser_bootstrap.cjs", () => { (fs.writeFileSync(logFile, "content"), (process.env.GH_AW_AGENT_OUTPUT = logFile)); const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: !0 }); (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), - expect(mockCore.setFailed).toHaveBeenCalledWith("Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully."), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); }), @@ -159,7 +160,7 @@ describe("log_parser_bootstrap.cjs", () => { const mockParseLog = vi.fn().mockImplementation(() => { throw new Error("Parser error"); }); - (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), expect(mockCore.setFailed).toHaveBeenCalledWith(expect.any(Error)), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); + (runLogParser({ parseLog: mockParseLog, parserName: "TestParser" }), expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Parser error`), fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); }), it("should handle failed parse (empty result)", () => { const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")), diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs index e94300d7d6..74b7a467f3 100644 --- a/actions/setup/js/log_parser_shared.cjs +++ b/actions/setup/js/log_parser_shared.cjs @@ -3,6 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { unfenceMarkdown } = require("./markdown_unfencing.cjs"); +const { ERR_PARSE } = require("./error_codes.cjs"); /** * Shared utility functions for log parsers @@ -837,7 +838,7 @@ function parseLogEntries(logContent) { try { logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries) || logEntries.length === 0) { - throw new Error("Not a JSON array or empty array"); + throw new Error(`${ERR_PARSE}: Not a JSON array or empty array`); } return logEntries; } catch (jsonArrayError) { diff --git a/actions/setup/js/mark_pull_request_as_ready_for_review.cjs b/actions/setup/js/mark_pull_request_as_ready_for_review.cjs index e722454429..4b2fdfda40 100644 --- a/actions/setup/js/mark_pull_request_as_ready_for_review.cjs +++ b/actions/setup/js/mark_pull_request_as_ready_for_review.cjs @@ -9,6 +9,7 @@ const { generateFooterWithMessages } = require("./messages_footer.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); /** @type {string} Safe output type handled by this module */ const HANDLER_TYPE = "mark_pull_request_as_ready_for_review"; @@ -29,7 +30,7 @@ async function getPullRequestDetails(github, owner, repo, prNumber) { }); if (!pr) { - throw new Error(`Pull request #${prNumber} not found in ${owner}/${repo}`); + throw new Error(`${ERR_NOT_FOUND}: Pull request #${prNumber} not found in ${owner}/${repo}`); } return pr; diff --git a/actions/setup/js/mcp_http_transport.cjs b/actions/setup/js/mcp_http_transport.cjs index 044a756b49..4d4baa41ba 100644 --- a/actions/setup/js/mcp_http_transport.cjs +++ b/actions/setup/js/mcp_http_transport.cjs @@ -27,6 +27,7 @@ moduleLogger.debug("Module is being loaded"); const http = require("http"); const { randomUUID } = require("crypto"); const { createServer, registerTool, handleRequest } = require("./mcp_server_core.cjs"); +const { ERR_SYSTEM } = require("./error_codes.cjs"); /** * Simple MCP Server wrapper that provides a class-like interface @@ -140,7 +141,7 @@ class MCPHTTPTransport { const logger = createLogger("MCPHTTPTransport"); logger.debug(`Called, started=${this.started}`); if (this.started) { - throw new Error("Transport already started"); + throw new Error(`${ERR_SYSTEM}: Transport already started`); } this.started = true; logger.debug("Set started=true"); diff --git a/actions/setup/js/mcp_server_core.cjs b/actions/setup/js/mcp_server_core.cjs index dd8f6dd9b0..2ee94cd1d5 100644 --- a/actions/setup/js/mcp_server_core.cjs +++ b/actions/setup/js/mcp_server_core.cjs @@ -3,6 +3,7 @@ const { createLogger } = require("./mcp_logger.cjs"); const moduleLogger = createLogger("mcp_server_core"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); // Log immediately at module load time moduleLogger.debug("Module is being loaded"); @@ -802,7 +803,7 @@ function start(server, options = {}) { server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); if (!Object.keys(server.tools).length) { - throw new Error("No tools registered"); + throw new Error(`${ERR_VALIDATION}: No tools registered`); } const onData = async chunk => { diff --git a/actions/setup/js/merge_remote_agent_github_folder.cjs b/actions/setup/js/merge_remote_agent_github_folder.cjs index 77a0a58356..2e026ff61f 100644 --- a/actions/setup/js/merge_remote_agent_github_folder.cjs +++ b/actions/setup/js/merge_remote_agent_github_folder.cjs @@ -23,6 +23,7 @@ const path = require("path"); const { execFileSync } = require("child_process"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_CONFIG, ERR_PARSE, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); // Get the core object - in github-script context it's global, for testing we create a minimal version const coreObj = @@ -133,7 +134,7 @@ function validateGitParameter(value, name) { // This is safe for git owner/repo/ref names const safePattern = /^[a-zA-Z0-9._/-]+$/; if (!safePattern.test(value)) { - throw new Error(`Invalid ${name}: contains unsafe characters. Only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed.`); + throw new Error(`${ERR_VALIDATION}: Invalid ${name}: contains unsafe characters. Only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed.`); } } @@ -147,12 +148,12 @@ function validateGitParameter(value, name) { function validateSafePath(userPath, basePath, name) { // Reject paths with null bytes if (userPath.includes("\0")) { - throw new Error(`Invalid ${name}: contains null bytes`); + throw new Error(`${ERR_VALIDATION}: Invalid ${name}: contains null bytes`); } // Reject paths that attempt to traverse up (..) if (userPath.includes("..")) { - throw new Error(`Invalid ${name}: path traversal detected`); + throw new Error(`${ERR_VALIDATION}: Invalid ${name}: path traversal detected`); } // Resolve the full path and ensure it's within the base path @@ -160,7 +161,7 @@ function validateSafePath(userPath, basePath, name) { const resolvedBase = path.resolve(basePath); if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) { - throw new Error(`Invalid ${name}: path escapes base directory`); + throw new Error(`${ERR_VALIDATION}: Invalid ${name}: path escapes base directory`); } return resolvedPath; @@ -212,7 +213,7 @@ function sparseCheckoutGithubFolder(owner, repo, ref, tempDir) { coreObj.info("Sparse checkout completed successfully"); } catch (error) { - throw new Error(`Sparse checkout failed: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Sparse checkout failed: ${getErrorMessage(error)}`); } } @@ -302,7 +303,7 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { // Check if the pre-checked-out folder exists if (!pathExists(checkoutPath)) { - throw new Error(`Pre-checked-out repository not found at ${checkoutPath}. The actions/checkout step may have failed.`); + throw new Error(`${ERR_SYSTEM}: Pre-checked-out repository not found at ${checkoutPath}. The actions/checkout step may have failed.`); } // Check if .github folder exists in the checked-out repository @@ -329,7 +330,7 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { for (const conflict of conflicts) { coreObj.error(` - ${conflict}`); } - throw new Error(`Cannot merge .github folder from ${owner}/${repo}@${ref}: ${conflicts.length} file(s) conflict with existing files`); + throw new Error(`${ERR_VALIDATION}: Cannot merge .github folder from ${owner}/${repo}@${ref}: ${conflicts.length} file(s) conflict with existing files`); } if (merged > 0) { @@ -356,17 +357,17 @@ async function main() { try { repositoryImports = JSON.parse(repositoryImportsEnv); } catch (error) { - throw new Error(`Failed to parse GH_AW_REPOSITORY_IMPORTS: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Failed to parse GH_AW_REPOSITORY_IMPORTS: ${getErrorMessage(error)}`); } if (!Array.isArray(repositoryImports)) { - throw new Error("GH_AW_REPOSITORY_IMPORTS must be a JSON array"); + throw new Error(`${ERR_PARSE}: GH_AW_REPOSITORY_IMPORTS must be a JSON array`); } // Get workspace path const workspace = process.env.GITHUB_WORKSPACE; if (!workspace) { - throw new Error("GITHUB_WORKSPACE environment variable not set"); + throw new Error(`${ERR_CONFIG}: GITHUB_WORKSPACE environment variable not set`); } // Process each repository import @@ -419,7 +420,7 @@ async function main() { // Get workspace path const workspace = process.env.GITHUB_WORKSPACE; if (!workspace) { - throw new Error("GITHUB_WORKSPACE environment variable not set"); + throw new Error(`${ERR_CONFIG}: GITHUB_WORKSPACE environment variable not set`); } await mergeRepositoryGithubFolder(owner, repo, ref, workspace); diff --git a/actions/setup/js/notify_comment_error.cjs b/actions/setup/js/notify_comment_error.cjs index 9622f1c9e7..8a416200cb 100644 --- a/actions/setup/js/notify_comment_error.cjs +++ b/actions/setup/js/notify_comment_error.cjs @@ -10,6 +10,7 @@ const { getRunSuccessMessage, getRunFailureMessage, getDetectionFailureMessage } const { getMessages } = require("./messages_core.cjs"); const { getErrorMessage, isLockedError } = require("./error_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Collect generated asset URLs from safe output jobs @@ -105,7 +106,7 @@ async function main() { // At this point, we have a comment to update if (!runUrl) { - core.setFailed("Run URL is required"); + core.setFailed(`${ERR_VALIDATION}: Run URL is required`); return; } @@ -261,7 +262,7 @@ async function main() { // At this point, we must have a comment ID (verified by earlier checks) if (!commentId) { - core.setFailed("Comment ID is required for updating existing comment"); + core.setFailed(`${ERR_VALIDATION}: Comment ID is required for updating existing comment`); return; } diff --git a/actions/setup/js/notify_comment_error.test.cjs b/actions/setup/js/notify_comment_error.test.cjs index 327e7aafee..58a87a3083 100644 --- a/actions/setup/js/notify_comment_error.test.cjs +++ b/actions/setup/js/notify_comment_error.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_VALIDATION } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), info: vi.fn(), @@ -113,7 +114,7 @@ const mockCore = { (process.env.GH_AW_WORKFLOW_NAME = "test-workflow"), (process.env.GH_AW_AGENT_CONCLUSION = "failure"), await eval(`(async () => { ${notifyCommentScript}; await main(); })()`), - expect(mockCore.setFailed).toHaveBeenCalledWith("Run URL is required"), + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Run URL is required`), expect(mockGithub.request).not.toHaveBeenCalled(), expect(mockGithub.graphql).not.toHaveBeenCalled()); }); diff --git a/actions/setup/js/parse_claude_log.test.cjs b/actions/setup/js/parse_claude_log.test.cjs index 3c6a77c817..8d69c30d1c 100644 --- a/actions/setup/js/parse_claude_log.test.cjs +++ b/actions/setup/js/parse_claude_log.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); describe("parse_claude_log.cjs", () => { let mockCore, originalConsole, originalProcess; @@ -407,7 +408,7 @@ describe("parse_claude_log.cjs", () => { expect(mockCore.summary.addRaw).toHaveBeenCalled(); expect(mockCore.summary.write).toHaveBeenCalled(); - expect(mockCore.setFailed).toHaveBeenCalledWith("MCP server(s) failed to launch: broken_server"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: MCP server(s) failed to launch: broken_server`); }); it("should call setFailed when max-turns limit is hit", async () => { @@ -421,7 +422,7 @@ describe("parse_claude_log.cjs", () => { expect(mockCore.summary.addRaw).toHaveBeenCalled(); expect(mockCore.summary.write).toHaveBeenCalled(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully."); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_VALIDATION}: Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); }); it("should handle missing log file", async () => { @@ -440,7 +441,7 @@ describe("parse_claude_log.cjs", () => { it("should fail when Claude log has no structured entries", async () => { await runScript("this is not structured Claude JSON output"); - expect(mockCore.setFailed).toHaveBeenCalledWith("Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution."); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); }); }); diff --git a/actions/setup/js/parse_copilot_log.cjs b/actions/setup/js/parse_copilot_log.cjs index 15dbb76d7c..48223d8a00 100644 --- a/actions/setup/js/parse_copilot_log.cjs +++ b/actions/setup/js/parse_copilot_log.cjs @@ -2,6 +2,7 @@ /// const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries } = require("./log_parser_shared.cjs"); +const { ERR_PARSE } = require("./error_codes.cjs"); const main = createEngineLogParser({ parserName: "Copilot", @@ -45,7 +46,7 @@ function parseCopilotLog(logContent) { try { logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - throw new Error("Not a JSON array"); + throw new Error(`${ERR_PARSE}: Not a JSON array`); } } catch (jsonArrayError) { // If that fails, try to parse as debug logs format diff --git a/actions/setup/js/parse_firewall_logs.cjs b/actions/setup/js/parse_firewall_logs.cjs index 32dce7dfab..fc69fdadce 100644 --- a/actions/setup/js/parse_firewall_logs.cjs +++ b/actions/setup/js/parse_firewall_logs.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const { sanitizeWorkflowName } = require("./sanitize_workflow_name.cjs"); +const { ERR_PARSE } = require("./error_codes.cjs"); /** * Parses firewall logs and creates a step summary @@ -90,7 +91,7 @@ async function main() { core.summary.addRaw(summary).write(); core.info("Firewall log summary generated successfully"); } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); + core.setFailed(`${ERR_PARSE}: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs index 7247c41465..3d7f36f4ba 100644 --- a/actions/setup/js/parse_mcp_gateway_log.cjs +++ b/actions/setup/js/parse_mcp_gateway_log.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { displayDirectories } = require("./display_file_helpers.cjs"); +const { ERR_PARSE } = require("./error_codes.cjs"); /** * Parses MCP gateway logs and creates a step summary @@ -81,7 +82,7 @@ async function main() { const summary = generateGatewayLogSummary(gatewayLogContent, stderrLogContent); core.summary.addRaw(summary).write(); } catch (error) { - core.setFailed(getErrorMessage(error)); + core.setFailed(`${ERR_PARSE}: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/parse_safe_inputs_logs.cjs b/actions/setup/js/parse_safe_inputs_logs.cjs index ab2b85cbbc..21387f80da 100644 --- a/actions/setup/js/parse_safe_inputs_logs.cjs +++ b/actions/setup/js/parse_safe_inputs_logs.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_PARSE } = require("./error_codes.cjs"); /** * Parses safe-inputs MCP server logs and creates a step summary @@ -64,7 +65,7 @@ async function main() { const summary = generateSafeInputsSummary(allLogEntries); core.summary.addRaw(summary).write(); } catch (error) { - core.setFailed(getErrorMessage(error)); + core.setFailed(`${ERR_PARSE}: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/parse_safe_inputs_logs.test.cjs b/actions/setup/js/parse_safe_inputs_logs.test.cjs index 8dd30f3890..8f1cc4c33e 100644 --- a/actions/setup/js/parse_safe_inputs_logs.test.cjs +++ b/actions/setup/js/parse_safe_inputs_logs.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_PARSE } = require("./error_codes.cjs"); describe("parse_safe_inputs_logs.cjs", () => { let mockCore, originalConsole; @@ -415,7 +416,7 @@ describe("parse_safe_inputs_logs.cjs", () => { await main(); - expect(mockCore.setFailed).toHaveBeenCalledWith("Test error"); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_PARSE}: Test error`); }); it("should process multiple log files", async () => { diff --git a/actions/setup/js/parse_threat_detection_results.cjs b/actions/setup/js/parse_threat_detection_results.cjs index 102592e8ec..e064c35e4b 100644 --- a/actions/setup/js/parse_threat_detection_results.cjs +++ b/actions/setup/js/parse_threat_detection_results.cjs @@ -15,6 +15,7 @@ const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); const { listFilesRecursively } = require("./file_helpers.cjs"); const { AGENT_OUTPUT_FILENAME } = require("./constants.cjs"); +const { ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Main entry point for parsing threat detection results @@ -40,7 +41,7 @@ async function main() { core.info(" Found " + files.length + " file(s):"); files.forEach(file => core.info(" - " + file)); } - core.setFailed("❌ Agent output file not found at: " + outputPath); + core.setFailed(`${ERR_SYSTEM}: ❌ Agent output file not found at: ${outputPath}`); return; } const outputContent = fs.readFileSync(outputPath, "utf8"); @@ -71,7 +72,7 @@ async function main() { // Set success output to false before failing core.setOutput("success", "false"); - core.setFailed("❌ Security threats detected: " + threats.join(", ") + reasonsText); + core.setFailed(`${ERR_VALIDATION}: ❌ Security threats detected: ${threats.join(", ")}${reasonsText}`); } else { core.info("✅ No security threats detected. Safe outputs may proceed."); // Set success output to true when no threats detected diff --git a/actions/setup/js/read_buffer.cjs b/actions/setup/js/read_buffer.cjs index 1c70a28582..57dcbc6f98 100644 --- a/actions/setup/js/read_buffer.cjs +++ b/actions/setup/js/read_buffer.cjs @@ -2,6 +2,7 @@ /// const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_PARSE } = require("./error_codes.cjs"); /** * ReadBuffer Module @@ -59,7 +60,7 @@ class ReadBuffer { try { return JSON.parse(line); } catch (error) { - throw new Error(`Parse error: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Parse error: ${getErrorMessage(error)}`); } } } diff --git a/actions/setup/js/redact_secrets.cjs b/actions/setup/js/redact_secrets.cjs index 738cc745a9..2340269dcb 100644 --- a/actions/setup/js/redact_secrets.cjs +++ b/actions/setup/js/redact_secrets.cjs @@ -8,6 +8,8 @@ */ const fs = require("fs"); const path = require("path"); +const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Recursively finds all files matching the specified extensions * @param {string} dir - Directory to search @@ -216,10 +218,8 @@ async function main() { core.info("Secret redaction complete: no secrets found"); } } catch (error) { - core.setFailed(`Secret redaction failed: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_VALIDATION}: Secret redaction failed: ${getErrorMessage(error)}`); } } -const { getErrorMessage } = require("./error_helpers.cjs"); - module.exports = { main, redactSecrets, redactBuiltInPatterns, BUILT_IN_PATTERNS }; diff --git a/actions/setup/js/render_template.cjs b/actions/setup/js/render_template.cjs index f309beab9d..ae3630e9cc 100644 --- a/actions/setup/js/render_template.cjs +++ b/actions/setup/js/render_template.cjs @@ -8,6 +8,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const fs = require("fs"); +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Determines if a value is truthy according to template logic @@ -144,7 +145,7 @@ function main() { const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) { if (typeof core !== "undefined") { - core.setFailed("GH_AW_PROMPT environment variable is not set"); + core.setFailed(`${ERR_CONFIG}: GH_AW_PROMPT environment variable is not set`); } process.exit(1); } @@ -216,7 +217,7 @@ function main() { if (err.stack) { core.info(`[main] Stack trace:\n${err.stack}`); } - core.setFailed(getErrorMessage(error)); + core.setFailed(`${ERR_API}: ${getErrorMessage(error)}`); } else { throw error; } diff --git a/actions/setup/js/repo_helpers.cjs b/actions/setup/js/repo_helpers.cjs index 5e373e4a25..a156255539 100644 --- a/actions/setup/js/repo_helpers.cjs +++ b/actions/setup/js/repo_helpers.cjs @@ -7,6 +7,7 @@ */ const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Parse the allowed repos from config value (array or comma-separated string) @@ -159,7 +160,7 @@ function resolveAndValidateRepo(item, defaultTargetRepo, allowedRepos, operation // When valid is false, error is guaranteed to be non-null const errorMessage = repoValidation.error; if (!errorMessage) { - throw new Error("Internal error: repoValidation.error should not be null when valid is false"); + throw new Error(`${ERR_VALIDATION}: Internal error: repoValidation.error should not be null when valid is false`); } return { success: false, diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index 79d9e08cca..194400202d 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -7,6 +7,7 @@ // Also processes inline @path and @url references. const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG, ERR_PARSE, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); const fs = require("fs"); const path = require("path"); @@ -412,7 +413,7 @@ function processExpressions(content, source) { // If any unsafe expressions found, throw error if (unsafeExpressions.length > 0) { const errorMsg = - `${source} contains unauthorized GitHub Actions expressions:\n` + + `${ERR_VALIDATION}: ${source} contains unauthorized GitHub Actions expressions:\n` + unsafeExpressions.map(e => ` - ${e}`).join("\n") + "\n\n" + "Only expressions from the safe list can be used in runtime imports.\n" + @@ -541,13 +542,13 @@ async function processUrlImport(url, optional, startLine, endLine) { const end = endLine !== undefined ? endLine : totalLines; if (start < 1 || start > totalLines) { - throw new Error(`Invalid start line ${start} for URL ${url} (total lines: ${totalLines})`); + throw new Error(`${ERR_VALIDATION}: Invalid start line ${start} for URL ${url} (total lines: ${totalLines})`); } if (end < 1 || end > totalLines) { - throw new Error(`Invalid end line ${end} for URL ${url} (total lines: ${totalLines})`); + throw new Error(`${ERR_VALIDATION}: Invalid end line ${end} for URL ${url} (total lines: ${totalLines})`); } if (start > end) { - throw new Error(`Start line ${start} cannot be greater than end line ${end} for URL ${url}`); + throw new Error(`${ERR_VALIDATION}: Start line ${start} cannot be greater than end line ${end} for URL ${url}`); } // Extract lines (convert to 0-indexed) @@ -759,11 +760,11 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start // Security check: ensure the resolved path is within the workspace const relativePath = path.relative(normalizedBaseFolder, normalizedPath); if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { - throw new Error(`Security: Path ${filepathOrUrl} must be within workspace (resolves to: ${relativePath})`); + throw new Error(`${ERR_CONFIG}: Security: Path ${filepathOrUrl} must be within workspace (resolves to: ${relativePath})`); } // Additional check: ensure path stays within .agents folder if (!relativePath.startsWith(".agents" + path.sep) && relativePath !== ".agents") { - throw new Error(`Security: Path ${filepathOrUrl} must be within .agents folder`); + throw new Error(`${ERR_VALIDATION}: Security: Path ${filepathOrUrl} must be within .agents folder`); } } else { // Regular paths resolve within .github folder @@ -776,7 +777,7 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start // Security check: ensure the resolved path is within the .github folder const relativePath = path.relative(normalizedBaseFolder, normalizedPath); if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { - throw new Error(`Security: Path ${filepathOrUrl} must be within .github folder (resolves to: ${relativePath})`); + throw new Error(`${ERR_VALIDATION}: Security: Path ${filepathOrUrl} must be within .github folder (resolves to: ${relativePath})`); } } @@ -786,7 +787,7 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start core.warning(`Optional runtime import file not found: ${filepath}`); return ""; } - throw new Error(`Runtime import file not found: ${filepath}`); + throw new Error(`${ERR_SYSTEM}: Runtime import file not found: ${filepath}`); } // Read the file @@ -802,13 +803,13 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start const end = endLine !== undefined ? endLine : totalLines; if (start < 1 || start > totalLines) { - throw new Error(`Invalid start line ${start} for file ${filepath} (total lines: ${totalLines})`); + throw new Error(`${ERR_VALIDATION}: Invalid start line ${start} for file ${filepath} (total lines: ${totalLines})`); } if (end < 1 || end > totalLines) { - throw new Error(`Invalid end line ${end} for file ${filepath} (total lines: ${totalLines})`); + throw new Error(`${ERR_VALIDATION}: Invalid end line ${end} for file ${filepath} (total lines: ${totalLines})`); } if (start > end) { - throw new Error(`Start line ${start} cannot be greater than end line ${end} for file ${filepath}`); + throw new Error(`${ERR_VALIDATION}: Start line ${start} cannot be greater than end line ${end} for file ${filepath}`); } // Extract lines (convert to 0-indexed) @@ -929,7 +930,7 @@ async function processRuntimeImports(content, workspaceDir, importedFiles = new // Check for circular dependencies if (importStack.includes(filepathWithRange)) { const cycle = [...importStack, filepathWithRange].join(" -> "); - throw new Error(`Circular dependency detected: ${cycle}`); + throw new Error(`${ERR_PARSE}: Circular dependency detected: ${cycle}`); } // Add to import stack for circular dependency detection @@ -953,7 +954,7 @@ async function processRuntimeImports(content, workspaceDir, importedFiles = new processedContent = processedContent.replace(fullMatch, importedContent); } catch (error) { const errorMessage = getErrorMessage(error); - throw new Error(`Failed to process runtime import for ${filepathWithRange}: ${errorMessage}`); + throw new Error(`${ERR_API}: Failed to process runtime import for ${filepathWithRange}: ${errorMessage}`); } finally { // Remove from import stack importStack.pop(); diff --git a/actions/setup/js/safe_inputs_config_loader.cjs b/actions/setup/js/safe_inputs_config_loader.cjs index 33836c2e0e..da494c49ac 100644 --- a/actions/setup/js/safe_inputs_config_loader.cjs +++ b/actions/setup/js/safe_inputs_config_loader.cjs @@ -8,6 +8,7 @@ */ const fs = require("fs"); +const { ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); /** * @typedef {Object} SafeInputsToolConfig @@ -34,7 +35,7 @@ const fs = require("fs"); */ function loadConfig(configPath) { if (!fs.existsSync(configPath)) { - throw new Error(`Configuration file not found: ${configPath}`); + throw new Error(`${ERR_SYSTEM}: Configuration file not found: ${configPath}`); } const configContent = fs.readFileSync(configPath, "utf-8"); @@ -42,7 +43,7 @@ function loadConfig(configPath) { // Validate required fields if (!config.tools || !Array.isArray(config.tools)) { - throw new Error("Configuration must contain a 'tools' array"); + throw new Error(`${ERR_VALIDATION}: Configuration must contain a 'tools' array`); } return config; diff --git a/actions/setup/js/safe_inputs_mcp_server_http.cjs b/actions/setup/js/safe_inputs_mcp_server_http.cjs index 85029ee4e8..b703bdcd0d 100644 --- a/actions/setup/js/safe_inputs_mcp_server_http.cjs +++ b/actions/setup/js/safe_inputs_mcp_server_http.cjs @@ -30,6 +30,7 @@ const { generateEnhancedErrorMessage } = require("./mcp_enhanced_errors.cjs"); const { createLogger } = require("./mcp_logger.cjs"); const { bootstrapSafeInputsServer, cleanupConfigFile } = require("./safe_inputs_bootstrap.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Create and configure the MCP server with tools diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index fb29aaa534..5f7a2845f7 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -11,6 +11,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_CONFIG, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); const { hasUnresolvedTemporaryIds, replaceTemporaryIdReferences, normalizeTemporaryId } = require("./temporary_id.cjs"); const { generateMissingInfoSections } = require("./missing_info_formatter.cjs"); const { setCollectedMissings } = require("./missing_messages_helper.cjs"); @@ -85,7 +86,7 @@ const CODE_PUSH_TYPES = new Set(["push_to_pull_request_branch", "create_pull_req */ function loadConfig() { if (!process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG) { - throw new Error("GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG environment variable is required but not set"); + throw new Error(`${ERR_CONFIG}: GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG environment variable is required but not set`); } try { @@ -94,7 +95,7 @@ function loadConfig() { // Normalize config keys: convert hyphens to underscores return Object.fromEntries(Object.entries(config).map(([k, v]) => [k.replace(/-/g, "_"), v])); } catch (error) { - throw new Error(`Failed to parse GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Failed to parse GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${getErrorMessage(error)}`); } } @@ -990,7 +991,7 @@ async function main() { core.info("Safe Output Handler Manager completed"); } catch (error) { - core.setFailed(`Handler manager failed: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_VALIDATION}: Handler manager failed: ${getErrorMessage(error)}`); } finally { // Guarantee the manifest file exists for artifact upload even when the handler fails. // This is a no-op if the file was already created by createManifestLogger(). diff --git a/actions/setup/js/safe_output_manifest.cjs b/actions/setup/js/safe_output_manifest.cjs index adc2ecda61..8cb1b316ea 100644 --- a/actions/setup/js/safe_output_manifest.cjs +++ b/actions/setup/js/safe_output_manifest.cjs @@ -2,6 +2,7 @@ const fs = require("fs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_SYSTEM } = require("./error_codes.cjs"); /** * Default path for the safe output items manifest file. @@ -75,7 +76,7 @@ function createManifestLogger(manifestFile = MANIFEST_FILE_PATH) { try { fs.appendFileSync(manifestFile, jsonLine); } catch (error) { - throw new Error(`Failed to write to manifest file: ${getErrorMessage(error)}`); + throw new Error(`${ERR_SYSTEM}: Failed to write to manifest file: ${getErrorMessage(error)}`); } }; } @@ -92,7 +93,7 @@ function ensureManifestExists(manifestFile = MANIFEST_FILE_PATH) { try { fs.writeFileSync(manifestFile, ""); } catch (error) { - throw new Error(`Failed to create manifest file: ${getErrorMessage(error)}`); + throw new Error(`${ERR_SYSTEM}: Failed to create manifest file: ${getErrorMessage(error)}`); } } } diff --git a/actions/setup/js/safe_output_processor.cjs b/actions/setup/js/safe_output_processor.cjs index e0aa6a7129..328bee0449 100644 --- a/actions/setup/js/safe_output_processor.cjs +++ b/actions/setup/js/safe_output_processor.cjs @@ -10,6 +10,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); const { parseAllowedItems, resolveTarget } = require("./safe_output_helpers.cjs"); const { getSafeOutputConfig, validateMaxCount } = require("./safe_output_validator.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * @typedef {Object} ProcessorConfig diff --git a/actions/setup/js/safe_output_unified_handler_manager.cjs b/actions/setup/js/safe_output_unified_handler_manager.cjs index 79dd6a65ea..3283317272 100644 --- a/actions/setup/js/safe_output_unified_handler_manager.cjs +++ b/actions/setup/js/safe_output_unified_handler_manager.cjs @@ -16,6 +16,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_CONFIG, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); const { hasUnresolvedTemporaryIds, replaceTemporaryIdReferences, normalizeTemporaryId, loadTemporaryIdMap, isTemporaryId } = require("./temporary_id.cjs"); const { generateMissingInfoSections } = require("./missing_info_formatter.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); @@ -127,7 +128,7 @@ function loadConfig() { } } } catch (error) { - throw new Error(`Failed to parse GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Failed to parse GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${getErrorMessage(error)}`); } } @@ -140,13 +141,13 @@ function loadConfig() { // Explicitly provided project config takes precedence over auto-split config Object.assign(project, Object.fromEntries(Object.entries(config).map(([k, v]) => [k.replace(/-/g, "_"), v]))); } catch (error) { - throw new Error(`Failed to parse GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: ${getErrorMessage(error)}`); + throw new Error(`${ERR_PARSE}: Failed to parse GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: ${getErrorMessage(error)}`); } } // At least one config must be present if (Object.keys(regular).length === 0 && Object.keys(project).length === 0) { - throw new Error("At least one of GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG or GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG environment variables is required"); + throw new Error(`${ERR_CONFIG}: At least one of GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG or GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG environment variables is required`); } const regularCount = Object.keys(regular).length; @@ -168,7 +169,7 @@ function loadConfig() { async function setupProjectGitHubClient() { const projectToken = process.env.GH_AW_PROJECT_GITHUB_TOKEN; if (!projectToken) { - throw new Error("GH_AW_PROJECT_GITHUB_TOKEN environment variable is required for project-related safe outputs. " + "Configure a GitHub token with Projects permissions in your workflow secrets."); + throw new Error(`${ERR_CONFIG}: GH_AW_PROJECT_GITHUB_TOKEN environment variable is required for project-related safe outputs. Configure a GitHub token with Projects permissions in your workflow secrets.`); } core.info("Setting up separate Octokit client for project handlers with GH_AW_PROJECT_GITHUB_TOKEN"); @@ -242,7 +243,7 @@ async function loadHandlers(configs, projectOctokit = null, prReviewBuffer = nul try { // Ensure we have an Octokit instance for project handlers if (!projectOctokit) { - throw new Error(`Octokit instance is required for project handler ${type}. This is a configuration error - projectOctokit should be provided when project handlers are configured.`); + throw new Error(`${ERR_CONFIG}: Octokit instance is required for project handler ${type}. This is a configuration error - projectOctokit should be provided when project handlers are configured.`); } const handlerModule = require(handlerPath); @@ -749,7 +750,7 @@ function normalizeAndValidateTemporaryId(message, messageType, messageIndex) { } if (typeof message.temporary_id !== "string") { - throw new Error(`Message ${messageIndex + 1} (${messageType}): temporary_id must be a string (got ${typeof message.temporary_id})`); + throw new Error(`${ERR_VALIDATION}: Message ${messageIndex + 1} (${messageType}): temporary_id must be a string (got ${typeof message.temporary_id})`); } const raw = message.temporary_id; @@ -757,7 +758,7 @@ function normalizeAndValidateTemporaryId(message, messageType, messageIndex) { const withoutHash = trimmed.startsWith("#") ? trimmed.substring(1).trim() : trimmed; if (!isTemporaryId(withoutHash)) { - throw new Error(`Message ${messageIndex + 1} (${messageType}): invalid temporary_id '${raw}'. Temporary IDs must be 'aw_' followed by 3 to 8 alphanumeric characters (A-Za-z0-9), e.g. 'aw_abc' or 'aw_Test123'`); + throw new Error(`${ERR_VALIDATION}: Message ${messageIndex + 1} (${messageType}): invalid temporary_id '${raw}'. Temporary IDs must be 'aw_' followed by 3 to 8 alphanumeric characters (A-Za-z0-9), e.g. 'aw_abc' or 'aw_Test123'`); } // Normalize to the strict bare ID to keep lookups consistent. @@ -1157,7 +1158,7 @@ async function main() { core.info("=== Unified Safe Output Handler Manager Completed ==="); } catch (error) { - core.setFailed(`Handler manager failed: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_VALIDATION}: Handler manager failed: ${getErrorMessage(error)}`); } finally { // Guarantee the manifest file exists for artifact upload even when the handler fails. // This is a no-op if the file was already created by createManifestLogger(). diff --git a/actions/setup/js/safe_outputs_append.cjs b/actions/setup/js/safe_outputs_append.cjs index 88bb82dcd9..1bb25db55f 100644 --- a/actions/setup/js/safe_outputs_append.cjs +++ b/actions/setup/js/safe_outputs_append.cjs @@ -3,6 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const fs = require("fs"); +const { ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Create an append function for the safe outputs file @@ -20,7 +21,7 @@ function createAppendFunction(outputFile) { * @param {Object} entry - The entry to append */ return function appendSafeOutput(entry) { - if (!outputFile) throw new Error("No output file configured"); + if (!outputFile) throw new Error(`${ERR_VALIDATION}: No output file configured`); // Normalize type to use underscores (convert any dashes to underscores) entry.type = entry.type.replace(/-/g, "_"); // CRITICAL: Use JSON.stringify WITHOUT formatting parameters for JSONL format @@ -29,7 +30,7 @@ function createAppendFunction(outputFile) { try { fs.appendFileSync(outputFile, jsonLine); } catch (error) { - throw new Error(`Failed to write to output file: ${getErrorMessage(error)}`); + throw new Error(`${ERR_SYSTEM}: Failed to write to output file: ${getErrorMessage(error)}`); } }; } diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 4e41efdbaa..5880d09c14 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -12,6 +12,7 @@ const { getBaseBranch } = require("./get_base_branch.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); const { enforceCommentLimits } = require("./comment_limit_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Create handlers for safe output tools @@ -84,7 +85,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { */ const uploadAssetHandler = args => { const branchName = process.env.GH_AW_ASSETS_BRANCH; - if (!branchName) throw new Error("GH_AW_ASSETS_BRANCH not set"); + if (!branchName) throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_BRANCH not set`); // Normalize the branch name to ensure it's a valid git branch name const normalizedBranchName = normalizeBranchName(branchName); @@ -100,12 +101,12 @@ function createHandlers(server, appendSafeOutput, config = {}) { const isInTmp = absolutePath.startsWith(tmpDir); if (!isInWorkspace && !isInTmp) { - throw new Error(`File path must be within workspace directory (${workspaceDir}) or /tmp directory. ` + `Provided path: ${filePath} (resolved to: ${absolutePath})`); + throw new Error(`${ERR_CONFIG}: File path must be within workspace directory (${workspaceDir}) or /tmp directory. ` + `Provided path: ${filePath} (resolved to: ${absolutePath})`); } // Validate file exists if (!fs.existsSync(filePath)) { - throw new Error(`File not found: ${filePath}`); + throw new Error(`${ERR_SYSTEM}: File not found: ${filePath}`); } // Get file stats @@ -116,7 +117,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Check file size - read from environment variable if available const maxSizeKB = process.env.GH_AW_ASSETS_MAX_SIZE_KB ? parseInt(process.env.GH_AW_ASSETS_MAX_SIZE_KB, 10) : 10240; // Default 10MB if (sizeKB > maxSizeKB) { - throw new Error(`File size ${sizeKB} KB exceeds maximum allowed size ${maxSizeKB} KB`); + throw new Error(`${ERR_VALIDATION}: File size ${sizeKB} KB exceeds maximum allowed size ${maxSizeKB} KB`); } // Check file extension - read from environment variable if available @@ -131,7 +132,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { ]; if (!allowedExts.includes(ext)) { - throw new Error(`File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`); + throw new Error(`${ERR_VALIDATION}: File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`); } // Create assets directory diff --git a/actions/setup/js/safe_outputs_mcp_server.cjs b/actions/setup/js/safe_outputs_mcp_server.cjs index d2900d2722..48c390ee7e 100644 --- a/actions/setup/js/safe_outputs_mcp_server.cjs +++ b/actions/setup/js/safe_outputs_mcp_server.cjs @@ -18,6 +18,7 @@ const { createHandlers } = require("./safe_outputs_handlers.cjs"); const { attachHandlers, registerPredefinedTools, registerDynamicTools } = require("./safe_outputs_tools_loader.cjs"); const { bootstrapSafeOutputsServer, cleanupConfigFile } = require("./safe_outputs_bootstrap.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Start the safe-outputs MCP server @@ -56,7 +57,7 @@ function startSafeOutputsServer(options = {}) { registerDynamicTools(server, toolsWithHandlers, safeOutputsConfig, outputFile, registerTool, normalizeTool); server.debug(` tools: ${Object.keys(server.tools).join(", ")}`); - if (!Object.keys(server.tools).length) throw new Error("No tools enabled in configuration"); + if (!Object.keys(server.tools).length) throw new Error(`${ERR_VALIDATION}: No tools enabled in configuration`); // Note: We do NOT cleanup the config file here because it's needed by the ingestion // phase (collect_ndjson_output.cjs) that runs after the MCP server completes. diff --git a/actions/setup/js/setup_threat_detection.cjs b/actions/setup/js/setup_threat_detection.cjs index 4a8089d3ac..becd292059 100644 --- a/actions/setup/js/setup_threat_detection.cjs +++ b/actions/setup/js/setup_threat_detection.cjs @@ -16,6 +16,7 @@ const fs = require("fs"); const path = require("path"); const { checkFileExists } = require("./file_helpers.cjs"); const { AGENT_OUTPUT_FILENAME } = require("./constants.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Main entry point for setting up threat detection @@ -26,7 +27,7 @@ async function main() { // At runtime, markdown files are copied to /opt/gh-aw/prompts/ by the setup action const templatePath = "/opt/gh-aw/prompts/threat_detection.md"; if (!fs.existsSync(templatePath)) { - core.setFailed(`Threat detection template not found at: ${templatePath}`); + core.setFailed(`${ERR_VALIDATION}: Threat detection template not found at: ${templatePath}`); return; } const templateContent = fs.readFileSync(templatePath, "utf-8"); @@ -66,7 +67,7 @@ async function main() { } if (patchFiles.length === 0 && hasPatch) { - core.setFailed(`Patch file(s) expected but not found in: ${threatDetectionDir}`); + core.setFailed(`${ERR_VALIDATION}: Patch file(s) expected but not found in: ${threatDetectionDir}`); return; } diff --git a/actions/setup/js/staged_preview.cjs b/actions/setup/js/staged_preview.cjs index ecae209208..e9c10df4fe 100644 --- a/actions/setup/js/staged_preview.cjs +++ b/actions/setup/js/staged_preview.cjs @@ -11,6 +11,7 @@ * @param {(item: any, index: number) => string} options.renderItem - Function to render each item as markdown * @returns {Promise} */ +const { ERR_SYSTEM } = require("./error_codes.cjs"); async function generateStagedPreview(options) { const { title, description, items, renderItem } = options; @@ -28,7 +29,7 @@ async function generateStagedPreview(options) { core.info(summaryContent); core.info(`📝 ${title} preview written to step summary`); } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); + core.setFailed(`${ERR_SYSTEM}: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/actions/setup/js/substitute_placeholders.cjs b/actions/setup/js/substitute_placeholders.cjs index 2cb955d2c9..42dbef95f5 100644 --- a/actions/setup/js/substitute_placeholders.cjs +++ b/actions/setup/js/substitute_placeholders.cjs @@ -1,5 +1,6 @@ const fs = require("fs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_SYSTEM } = require("./error_codes.cjs"); const substitutePlaceholders = async ({ file, substitutions }) => { if (typeof core !== "undefined") { @@ -46,7 +47,7 @@ const substitutePlaceholders = async ({ file, substitutions }) => { if (typeof core !== "undefined") { core.info(`[substitutePlaceholders] ERROR reading file: ${errorMessage}`); } - throw new Error(`Failed to read file ${file}: ${errorMessage}`); + throw new Error(`${ERR_SYSTEM}: Failed to read file ${file}: ${errorMessage}`); } // Perform substitutions @@ -108,7 +109,7 @@ const substitutePlaceholders = async ({ file, substitutions }) => { if (typeof core !== "undefined") { core.info(`[substitutePlaceholders] ERROR writing file: ${errorMessage}`); } - throw new Error(`Failed to write file ${file}: ${errorMessage}`); + throw new Error(`${ERR_SYSTEM}: Failed to write file ${file}: ${errorMessage}`); } return `Successfully substituted ${Object.keys(substitutions).length} placeholder(s) in ${file}`; diff --git a/actions/setup/js/unlock-issue.cjs b/actions/setup/js/unlock-issue.cjs index 44924b48ea..8732765417 100644 --- a/actions/setup/js/unlock-issue.cjs +++ b/actions/setup/js/unlock-issue.cjs @@ -8,6 +8,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); async function main() { // Log actor and event information for debugging @@ -17,7 +18,7 @@ async function main() { const issueNumber = context.issue.number; if (!issueNumber) { - core.setFailed("Issue number not found in context"); + core.setFailed(`${ERR_NOT_FOUND}: Issue number not found in context`); return; } @@ -59,7 +60,7 @@ async function main() { } catch (error) { const errorMessage = getErrorMessage(error); core.error(`Failed to unlock issue: ${errorMessage}`); - core.setFailed(`Failed to unlock issue #${issueNumber}: ${errorMessage}`); + core.setFailed(`${ERR_NOT_FOUND}: Failed to unlock issue #${issueNumber}: ${errorMessage}`); } } diff --git a/actions/setup/js/unlock-issue.test.cjs b/actions/setup/js/unlock-issue.test.cjs index 6a2540f2cc..fc6f995e70 100644 --- a/actions/setup/js/unlock-issue.test.cjs +++ b/actions/setup/js/unlock-issue.test.cjs @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn(), setFailed: vi.fn(), setOutput: vi.fn() }, mockGithub = { rest: { issues: { get: vi.fn(), unlock: vi.fn() } } }, mockContext = { eventName: "issues", runId: 12345, repo: { owner: "testowner", repo: "testrepo" }, issue: { number: 42 }, payload: { issue: { number: 42 }, repository: { html_url: "https://github.com/testowner/testrepo" } } }; @@ -48,7 +49,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn delete global.context.payload.issue, await eval(`(async () => { ${unlockIssueScript}; await main(); })()`), expect(mockGithub.rest.issues.unlock).not.toHaveBeenCalled(), - expect(mockCore.setFailed).toHaveBeenCalledWith("Issue number not found in context")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Issue number not found in context`)); }), it("should handle API errors gracefully", async () => { mockGithub.rest.issues.get.mockResolvedValue({ data: { number: 42, locked: !0 } }); @@ -57,14 +58,14 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn await eval(`(async () => { ${unlockIssueScript}; await main(); })()`), expect(mockGithub.rest.issues.unlock).toHaveBeenCalled(), expect(mockCore.error).toHaveBeenCalledWith("Failed to unlock issue: Issue was not locked"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to unlock issue #42: Issue was not locked")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to unlock issue #42: Issue was not locked`)); }), it("should handle non-Error exceptions", async () => { (mockGithub.rest.issues.get.mockResolvedValue({ data: { number: 42, locked: !0 } }), mockGithub.rest.issues.unlock.mockRejectedValue("String error"), await eval(`(async () => { ${unlockIssueScript}; await main(); })()`), expect(mockCore.error).toHaveBeenCalledWith("Failed to unlock issue: String error"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to unlock issue #42: String error")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to unlock issue #42: String error`)); }), it("should work with different issue numbers", async () => { ((global.context.issue = { number: 200 }), @@ -84,7 +85,7 @@ const mockCore = { debug: vi.fn(), info: vi.fn(), warning: vi.fn(), error: vi.fn await eval(`(async () => { ${unlockIssueScript}; await main(); })()`), expect(mockGithub.rest.issues.unlock).toHaveBeenCalled(), expect(mockCore.error).toHaveBeenCalledWith("Failed to unlock issue: Resource not accessible by integration"), - expect(mockCore.setFailed).toHaveBeenCalledWith("Failed to unlock issue #42: Resource not accessible by integration")); + expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_NOT_FOUND}: Failed to unlock issue #42: Resource not accessible by integration`)); }), it("should skip if issue is already unlocked (redundant test for completeness)", async () => { (mockGithub.rest.issues.get.mockResolvedValue({ data: { number: 42, locked: !1 } }), diff --git a/actions/setup/js/update_discussion.cjs b/actions/setup/js/update_discussion.cjs index f9ea5e744a..d7463339c8 100644 --- a/actions/setup/js/update_discussion.cjs +++ b/actions/setup/js/update_discussion.cjs @@ -8,6 +8,7 @@ const { isDiscussionContext, getDiscussionNumber } = require("./update_context_helpers.cjs"); const { createUpdateHandlerFactory, createStandardFormatResult } = require("./update_handler_factory.cjs"); const { sanitizeTitle } = require("./sanitize_title.cjs"); +const { ERR_NOT_FOUND } = require("./error_codes.cjs"); /** * Execute the discussion update API call using GraphQL @@ -40,7 +41,7 @@ async function executeDiscussionUpdate(github, context, discussionNumber, update const discussion = queryResult?.repository?.discussion; if (!discussion) { - throw new Error(`Discussion #${discussionNumber} not found`); + throw new Error(`${ERR_NOT_FOUND}: Discussion #${discussionNumber} not found`); } // Build mutation for updating discussion diff --git a/actions/setup/js/update_issue.cjs b/actions/setup/js/update_issue.cjs index fa1550aa63..6208744d96 100644 --- a/actions/setup/js/update_issue.cjs +++ b/actions/setup/js/update_issue.cjs @@ -14,6 +14,7 @@ const { updateBody } = require("./update_pr_description_helpers.cjs"); const { loadTemporaryProjectMap, replaceTemporaryProjectReferences } = require("./temporary_id.cjs"); const { sanitizeTitle } = require("./sanitize_title.cjs"); const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Maximum limits for issue update parameters to prevent resource exhaustion. @@ -56,7 +57,7 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) { if (titlePrefix) { const currentTitle = currentIssue.title || ""; if (!currentTitle.startsWith(titlePrefix)) { - throw new Error(`Issue title "${currentTitle}" does not start with required prefix "${titlePrefix}"`); + throw new Error(`${ERR_VALIDATION}: Issue title "${currentTitle}" does not start with required prefix "${titlePrefix}"`); } core.info(`✓ Title prefix validation passed: "${titlePrefix}"`); } diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index b874cdcfca..b176d9e17f 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { loadTemporaryIdMapFromResolved, resolveIssueNumber, isTemporaryId, normalizeTemporaryId } = require("./temporary_id.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Normalize agent output keys for update_project. @@ -76,12 +77,12 @@ function logGraphQLError(error, operation) { */ function parseProjectInput(projectUrl) { if (!projectUrl || typeof projectUrl !== "string") { - throw new Error(`Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`); + throw new Error(`${ERR_VALIDATION}: Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`); } const urlMatch = projectUrl.match(/^https:\/\/[^/]+\/(?:users|orgs)\/[^/]+\/projects\/(\d+)/); if (!urlMatch) { - throw new Error(`Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`); + throw new Error(`${ERR_VALIDATION}: Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`); } return urlMatch[1]; @@ -94,12 +95,12 @@ function parseProjectInput(projectUrl) { */ function parseProjectUrl(projectUrl) { if (!projectUrl || typeof projectUrl !== "string") { - throw new Error(`Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`); + throw new Error(`${ERR_VALIDATION}: Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`); } const match = projectUrl.match(/^https:\/\/[^/]+\/(users|orgs)\/([^/]+)\/projects\/(\d+)/); if (!match) { - throw new Error(`Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`); + throw new Error(`${ERR_VALIDATION}: Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`); } return { @@ -260,7 +261,9 @@ async function resolveProjectV2(projectInfo, projectNumberInt, github) { } catch (fallbackError) { // Both direct query and fallback list query failed - this could be a transient API error const who = projectInfo.scope === "orgs" ? `org ${projectInfo.ownerLogin}` : `user ${projectInfo.ownerLogin}`; - throw new Error(`Unable to resolve project #${projectNumberInt} for ${who}. Both direct projectV2 query and fallback projectsV2 list query failed. This may be a transient GitHub API error. Error: ${getErrorMessage(fallbackError)}`); + throw new Error( + `${ERR_NOT_FOUND}: Unable to resolve project #${projectNumberInt} for ${who}. Both direct projectV2 query and fallback projectsV2 list query failed. This may be a transient GitHub API error. Error: ${getErrorMessage(fallbackError)}` + ); } const nodes = Array.isArray(list.nodes) ? list.nodes : []; @@ -272,7 +275,7 @@ async function resolveProjectV2(projectInfo, projectNumberInt, github) { const total = typeof list.totalCount === "number" ? ` (totalCount=${list.totalCount})` : ""; const who = projectInfo.scope === "orgs" ? `org ${projectInfo.ownerLogin}` : `user ${projectInfo.ownerLogin}`; - throw new Error(`Project #${projectNumberInt} not found or not accessible for ${who}.${total} Accessible Projects v2: ${summary}`); + throw new Error(`${ERR_NOT_FOUND}: Project #${projectNumberInt} not found or not accessible for ${who}.${total} Accessible Projects v2: ${summary}`); } /** * Check if a field name conflicts with unsupported GitHub built-in field types @@ -399,7 +402,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = // @ts-ignore - global.github is set by setupGlobals() from github-script context const github = githubClient || global.github; if (!github) { - throw new Error("GitHub client is required but not provided. Either pass a github client to updateProject() or ensure global.github is set."); + throw new Error(`${ERR_CONFIG}: GitHub client is required but not provided. Either pass a github client to updateProject() or ensure global.github is set.`); } const { owner, repo } = context.repo; const projectInfo = parseProjectUrl(output.project); @@ -479,7 +482,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = try { const projectNumberInt = parseInt(projectNumberFromUrl, 10); if (!Number.isFinite(projectNumberInt)) { - throw new Error(`Invalid project number parsed from URL: ${projectNumberFromUrl}`); + throw new Error(`${ERR_PARSE}: Invalid project number parsed from URL: ${projectNumberFromUrl}`); } const project = await resolveProjectV2(projectInfo, projectNumberInt, github); projectId = project.id; @@ -495,17 +498,17 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = if (wantsCreateView) { const view = output?.view; if (!view || typeof view !== "object") { - throw new Error('Invalid view. When operation is "create_view", you must provide view: { name, layout, ... }.'); + throw new Error(`${ERR_VALIDATION}: Invalid view. When operation is "create_view", you must provide view: { name, layout, ... }.`); } const name = typeof view.name === "string" ? view.name.trim() : ""; if (!name) { - throw new Error('Invalid view.name. When operation is "create_view", view.name is required and must be a non-empty string.'); + throw new Error(`${ERR_VALIDATION}: Invalid view.name. When operation is "create_view", view.name is required and must be a non-empty string.`); } const layout = typeof view.layout === "string" ? view.layout.trim() : ""; if (!layout || !["table", "board", "roadmap"].includes(layout)) { - throw new Error("Invalid view.layout. Must be one of: table, board, roadmap."); + throw new Error(`${ERR_VALIDATION}: Invalid view.layout. Must be one of: table, board, roadmap.`); } const filter = typeof view.filter === "string" ? view.filter : undefined; @@ -514,7 +517,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = if (visibleFields) { const invalid = visibleFields.filter(v => typeof v !== "number" || !Number.isFinite(v)); if (invalid.length > 0) { - throw new Error(`Invalid view.visible_fields. Must be an array of numbers (field IDs). Invalid values: ${invalid.map(v => JSON.stringify(v)).join(", ")}`); + throw new Error(`${ERR_VALIDATION}: Invalid view.visible_fields. Must be an array of numbers (field IDs). Invalid values: ${invalid.map(v => JSON.stringify(v)).join(", ")}`); } } @@ -528,7 +531,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = } if (typeof github.request !== "function") { - throw new Error("GitHub client does not support github.request(); cannot call Projects Views REST API."); + throw new Error(`${ERR_API}: GitHub client does not support github.request(); cannot call Projects Views REST API.`); } const route = projectInfo.scope === "orgs" ? "POST /orgs/{org}/projectsV2/{project_number}/views" : "POST /users/{user_id}/projectsV2/{project_number}/views"; @@ -565,7 +568,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = if (wantsCreateFields) { const fieldsConfig = output?.field_definitions; if (!fieldsConfig || !Array.isArray(fieldsConfig)) { - throw new Error('Invalid field_definitions. When operation is "create_fields", you must provide field_definitions as an array.'); + throw new Error(`${ERR_VALIDATION}: Invalid field_definitions. When operation is "create_fields", you must provide field_definitions as an array.`); } core.info(`[3/4] Creating ${fieldsConfig.length} project field(s)...`); @@ -700,11 +703,11 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = // Validate IDs used for draft chaining. // Draft issue chaining must use strict temporary IDs to match the unified handler manager. if (temporaryId && !isTemporaryId(temporaryId)) { - throw new Error(`Invalid temporary_id format: "${temporaryId}". Expected format: aw_ followed by 3 to 8 alphanumeric characters (e.g., "aw_abc", "aw_Test123").`); + throw new Error(`${ERR_VALIDATION}: Invalid temporary_id format: "${temporaryId}". Expected format: aw_ followed by 3 to 8 alphanumeric characters (e.g., "aw_abc", "aw_Test123").`); } if (draftIssueId && !isTemporaryId(draftIssueId)) { - throw new Error(`Invalid draft_issue_id format: "${draftIssueId}". Expected format: aw_ followed by 3 to 8 alphanumeric characters (e.g., "aw_abc", "aw_Test123").`); + throw new Error(`${ERR_VALIDATION}: Invalid draft_issue_id format: "${draftIssueId}". Expected format: aw_ followed by 3 to 8 alphanumeric characters (e.g., "aw_abc", "aw_Test123").`); } const draftTitle = typeof output.draft_title === "string" ? output.draft_title.trim() : ""; @@ -734,17 +737,17 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = itemId = existingDraftItem.id; core.info(`✓ Found draft issue "${draftTitle}" by title fallback`); } else { - throw new Error(`draft_issue_id "${draftIssueId}" not found in temporary ID map and no draft with title "${draftTitle}" found`); + throw new Error(`${ERR_NOT_FOUND}: draft_issue_id "${draftIssueId}" not found in temporary ID map and no draft with title "${draftTitle}" found`); } } else { - throw new Error(`draft_issue_id "${draftIssueId}" not found in temporary ID map and no draft_title provided for fallback lookup`); + throw new Error(`${ERR_NOT_FOUND}: draft_issue_id "${draftIssueId}" not found in temporary ID map and no draft_title provided for fallback lookup`); } } } // Mode 2: Create new draft or find by title else { if (!draftTitle) { - throw new Error('Invalid draft_title. When content_type is "draft_issue" and draft_issue_id is not provided, draft_title is required and must be a non-empty string.'); + throw new Error(`${ERR_CONFIG}: Invalid draft_title. When content_type is "draft_issue" and draft_issue_id is not provided, draft_title is required and must be a non-empty string.`); } // Check for existing draft issue with the same title @@ -929,14 +932,14 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = if (resolved.wasTemporaryId) { if (resolved.errorMessage || !resolved.resolved) { - throw new Error(`Failed to resolve temporary ID in content_number: ${resolved.errorMessage || "Unknown error"}`); + throw new Error(`${ERR_API}: Failed to resolve temporary ID in content_number: ${resolved.errorMessage || "Unknown error"}`); } core.info(`✓ Resolved temporary ID ${sanitizedContentNumber} to issue #${resolved.resolved.number}`); contentNumber = resolved.resolved.number; } else { // Not a temporary ID - validate as numeric if (!/^\d+$/.test(sanitizedContentNumber)) { - throw new Error(`Invalid content number "${rawContentNumber}". Provide a positive integer or a valid temporary ID (format: aw_ followed by 3-8 alphanumeric characters).`); + throw new Error(`${ERR_VALIDATION}: Invalid content number "${rawContentNumber}". Provide a positive integer or a valid temporary ID (format: aw_ followed by 3-8 alphanumeric characters).`); } contentNumber = Number.parseInt(sanitizedContentNumber, 10); } @@ -1142,7 +1145,7 @@ async function main(config = {}, githubClient = null) { const github = githubClient || global.github; if (!github) { - throw new Error("GitHub client is required but not provided. Either pass a github client to main() or ensure global.github is set by github-script action."); + throw new Error(`${ERR_CONFIG}: GitHub client is required but not provided. Either pass a github client to main() or ensure global.github is set by github-script action.`); } // Extract configuration @@ -1237,7 +1240,7 @@ async function main(config = {}, githubClient = null) { core.info(`Resolved temporary project ID ${projectStr} to ${resolved.projectUrl}`); effectiveProjectUrl = resolved.projectUrl; } else { - throw new Error(`Temporary project ID '${projectStr}' not found. Ensure create_project was called before update_project.`); + throw new Error(`${ERR_NOT_FOUND}: Temporary project ID '${projectStr}' not found. Ensure create_project was called before update_project.`); } } } diff --git a/actions/setup/js/update_release.cjs b/actions/setup/js/update_release.cjs index 6fb48814c8..d69a0dd806 100644 --- a/actions/setup/js/update_release.cjs +++ b/actions/setup/js/update_release.cjs @@ -11,6 +11,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { updateBody } = require("./update_pr_description_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); // Content sanitization: message.body is sanitized by updateBody() helper /** @@ -76,7 +77,7 @@ async function main(config = {}) { } if (!releaseTag) { - throw new Error("Release tag is required but not provided and cannot be inferred from event context"); + throw new Error(`${ERR_CONFIG}: Release tag is required but not provided and cannot be inferred from event context`); } } @@ -128,10 +129,10 @@ async function main(config = {}) { // Check for specific error cases if (errorMessage.includes("Not Found")) { - throw new Error(`Release with tag '${tagInfo}' not found. Please ensure the tag exists.`); + throw new Error(`${ERR_VALIDATION}: Release with tag '${tagInfo}' not found. Please ensure the tag exists.`); } - throw new Error(`Failed to update release with tag ${tagInfo}: ${errorMessage}`); + throw new Error(`${ERR_API}: Failed to update release with tag ${tagInfo}: ${errorMessage}`); } }; } diff --git a/actions/setup/js/upload_assets.cjs b/actions/setup/js/upload_assets.cjs index e3f4936306..e32f89618f 100644 --- a/actions/setup/js/upload_assets.cjs +++ b/actions/setup/js/upload_assets.cjs @@ -6,6 +6,7 @@ const path = require("path"); const crypto = require("crypto"); const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_API, ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); /** * Normalizes a branch name to be a valid git branch name. @@ -62,7 +63,7 @@ async function main() { // Get the branch name from environment variable (required) const branchName = process.env.GH_AW_ASSETS_BRANCH; if (!branchName || typeof branchName !== "string") { - core.setFailed("GH_AW_ASSETS_BRANCH environment variable is required but not set"); + core.setFailed(`${ERR_CONFIG}: GH_AW_ASSETS_BRANCH environment variable is required but not set`); return; } @@ -121,14 +122,14 @@ async function main() { const { fileName, sha, size, targetFileName } = asset; if (!fileName || !sha || !targetFileName) { - core.setFailed(`Invalid asset entry missing required fields: ${JSON.stringify(asset)}`); + core.setFailed(`${ERR_VALIDATION}: Invalid asset entry missing required fields: ${JSON.stringify(asset)}`); return; } // Check if file exists in artifacts const assetSourcePath = path.join("/tmp/gh-aw/safeoutputs/assets", fileName); if (!fs.existsSync(assetSourcePath)) { - core.setFailed(`Asset file not found: ${assetSourcePath}`); + core.setFailed(`${ERR_SYSTEM}: Asset file not found: ${assetSourcePath}`); return; } @@ -137,7 +138,7 @@ async function main() { const computedSha = crypto.createHash("sha256").update(fileContent).digest("hex"); if (computedSha !== sha) { - core.setFailed(`SHA mismatch for ${fileName}: expected ${sha}, got ${computedSha}`); + core.setFailed(`${ERR_VALIDATION}: SHA mismatch for ${fileName}: expected ${sha}, got ${computedSha}`); return; } @@ -159,7 +160,7 @@ async function main() { core.info(`Added asset: ${targetFileName} (${size} bytes)`); } catch (error) { - core.setFailed(`Failed to process asset ${fileName}: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_API}: Failed to process asset ${fileName}: ${getErrorMessage(error)}`); return; } } @@ -186,7 +187,7 @@ async function main() { core.info("No new assets to upload"); } } catch (error) { - core.setFailed(`Failed to upload assets: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_API}: Failed to upload assets: ${getErrorMessage(error)}`); return; } diff --git a/actions/setup/js/validate_context_variables.cjs b/actions/setup/js/validate_context_variables.cjs index 6c4f5a0550..cd0a78b649 100644 --- a/actions/setup/js/validate_context_variables.cjs +++ b/actions/setup/js/validate_context_variables.cjs @@ -42,6 +42,7 @@ */ const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * List of numeric context variable paths to validate @@ -188,7 +189,7 @@ async function main() { core.info("✅ All context variables validated successfully"); } catch (error) { const errorMessage = getErrorMessage(error); - core.setFailed(`Context variable validation failed: ${errorMessage}`); + core.setFailed(`${ERR_VALIDATION}: Context variable validation failed: ${errorMessage}`); throw error; } } diff --git a/actions/setup/js/validate_lockdown_requirements.cjs b/actions/setup/js/validate_lockdown_requirements.cjs index ac97bfe334..ea03f5b2e8 100644 --- a/actions/setup/js/validate_lockdown_requirements.cjs +++ b/actions/setup/js/validate_lockdown_requirements.cjs @@ -14,6 +14,7 @@ * @param {any} core - GitHub Actions core library * @returns {void} */ +const { ERR_VALIDATION } = require("./error_codes.cjs"); function validateLockdownRequirements(core) { // Check if lockdown mode is explicitly enabled (set to "true" in frontmatter) const lockdownEnabled = process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT === "true"; diff --git a/actions/setup/js/validate_secrets.cjs b/actions/setup/js/validate_secrets.cjs index f7ca3a01f7..cf89507740 100644 --- a/actions/setup/js/validate_secrets.cjs +++ b/actions/setup/js/validate_secrets.cjs @@ -18,6 +18,7 @@ const { promisify } = require("util"); const { exec } = require("child_process"); const execAsync = promisify(exec); const { getErrorMessage } = require("./error_helpers.cjs"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); /** * Test result status @@ -741,7 +742,7 @@ async function main() { core.info("✅ All configured secrets validated successfully!"); } } catch (error) { - core.setFailed(`Secret validation failed: ${getErrorMessage(error)}`); + core.setFailed(`${ERR_VALIDATION}: Secret validation failed: ${getErrorMessage(error)}`); throw error; } } diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index a3cf8eb5f8..37023ad13b 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -157,6 +157,7 @@ SAFE_INPUTS_FILES=( "generate_safe_inputs_config.cjs" "setup_globals.cjs" "error_helpers.cjs" + "error_codes.cjs" "mcp_enhanced_errors.cjs" "shim.cjs" ) @@ -223,6 +224,7 @@ SAFE_OUTPUTS_FILES=( "generate_compact_schema.cjs" "setup_globals.cjs" "error_helpers.cjs" + "error_codes.cjs" "git_helpers.cjs" "mcp_enhanced_errors.cjs" "comment_limit_helpers.cjs"