diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index d3741f254b..cb5a06bb0d 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -4098,8 +4098,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index 359b22f735..8068c55cb9 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -4130,8 +4130,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 43a89bd21c..e176fb0651 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -3097,8 +3097,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 4f535615ac..edef114dfb 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -3338,8 +3338,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 3b3c0c8f73..4f8aae27e9 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -754,8 +754,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 9d1e401e7a..cb816ac043 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -4200,8 +4200,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index e89c50582e..77be92d0da 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -1194,8 +1194,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` @@ -5074,8 +5074,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 117e67e6d8..7ee78f385f 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -3738,8 +3738,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 7eb0a0ef97..aabe8b46a0 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -3188,8 +3188,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index a3e19d3fad..20813ee614 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -2837,8 +2837,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index e2ebd07366..a3d6fcdbae 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -3895,8 +3895,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index ed302af855..5c1f10e669 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -4329,8 +4329,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/test-ollama-threat-detection.lock.yml b/.github/workflows/test-ollama-threat-detection.lock.yml index d6a351535b..f11db69597 100644 --- a/.github/workflows/test-ollama-threat-detection.lock.yml +++ b/.github/workflows/test-ollama-threat-detection.lock.yml @@ -3469,8 +3469,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index c995132421..d76282fea6 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -3760,8 +3760,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` diff --git a/pkg/workflow/add_labels.go b/pkg/workflow/add_labels.go index 4c00c8f2ad..7a0452d5fb 100644 --- a/pkg/workflow/add_labels.go +++ b/pkg/workflow/add_labels.go @@ -62,7 +62,7 @@ func (c *Compiler) buildAddLabelsJob(data *WorkflowData, mainJobName string) (*J StepID: "add_labels", MainJobName: mainJobName, CustomEnvVars: customEnvVars, - Script: addLabelsScript, + Script: getAddLabelsScript(), Token: token, }) diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index 96bd3246af..243168af6b 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -112,7 +112,7 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str StepID: "create_issue", MainJobName: mainJobName, CustomEnvVars: customEnvVars, - Script: createIssueScript, + Script: getCreateIssueScript(), Token: token, }) steps = append(steps, scriptSteps...) diff --git a/pkg/workflow/create_issue_subissue_test.go b/pkg/workflow/create_issue_subissue_test.go index 02cb18a333..0d931308dc 100644 --- a/pkg/workflow/create_issue_subissue_test.go +++ b/pkg/workflow/create_issue_subissue_test.go @@ -9,58 +9,59 @@ import ( // TestCreateIssueSubissueFeature tests that the create_issue.js script includes subissue functionality func TestCreateIssueSubissueFeature(t *testing.T) { + script := getCreateIssueScript() // Test that the script contains the subissue detection logic - if !strings.Contains(createIssueScript, "context.payload?.issue?.number") { + if !strings.Contains(script, "context.payload?.issue?.number") { t.Error("Expected create_issue.js to check for parent issue context") } // Test that the script modifies the body when in issue context - if !strings.Contains(createIssueScript, "Related to #${effectiveParentIssueNumber}") { + if !strings.Contains(script, "Related to #${effectiveParentIssueNumber}") { t.Error("Expected create_issue.js to add parent issue reference to body") } // Test that the script supports explicit parent field - if !strings.Contains(createIssueScript, "createIssueItem.parent") { + if !strings.Contains(script, "createIssueItem.parent") { t.Error("Expected create_issue.js to support explicit parent field") } // Test that the script uses effectiveParentIssueNumber - if !strings.Contains(createIssueScript, "effectiveParentIssueNumber") { + if !strings.Contains(script, "effectiveParentIssueNumber") { t.Error("Expected create_issue.js to use effectiveParentIssueNumber variable") } // Test that the script includes GraphQL sub-issue linking - if !strings.Contains(createIssueScript, "addSubIssue") { + if !strings.Contains(script, "addSubIssue") { t.Error("Expected create_issue.js to include addSubIssue GraphQL mutation") } // Test that the script calls github.graphql for sub-issue linking - if !strings.Contains(createIssueScript, "github.graphql(addSubIssueMutation") { + if !strings.Contains(script, "github.graphql(addSubIssueMutation") { t.Error("Expected create_issue.js to call github.graphql for sub-issue linking") } // Test that the script fetches node IDs before linking - if !strings.Contains(createIssueScript, "getIssueNodeIdQuery") { + if !strings.Contains(script, "getIssueNodeIdQuery") { t.Error("Expected create_issue.js to fetch issue node IDs before linking") } // Test that the script creates a comment on the parent issue - if !strings.Contains(createIssueScript, "github.rest.issues.createComment") { + if !strings.Contains(script, "github.rest.issues.createComment") { t.Error("Expected create_issue.js to create comment on parent issue") } // Test that the script has proper error handling for sub-issue linking - if !strings.Contains(createIssueScript, "Warning: Could not link sub-issue to parent") { + if !strings.Contains(script, "Warning: Could not link sub-issue to parent") { t.Error("Expected create_issue.js to have error handling for sub-issue linking") } // Test console logging for debugging - if !strings.Contains(createIssueScript, "Detected issue context, parent issue") { + if !strings.Contains(script, "Detected issue context, parent issue") { t.Error("Expected create_issue.js to log when issue context is detected") } // Test that it logs successful sub-issue linking - if !strings.Contains(createIssueScript, "Successfully linked issue #") { + if !strings.Contains(script, "Successfully linked issue #") { t.Error("Expected create_issue.js to log successful sub-issue linking") } } diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index 15ee957754..038dee317e 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -10,9 +10,6 @@ import ( //go:embed js/create_pull_request.cjs var createPullRequestScript string -//go:embed js/create_issue.cjs -var createIssueScript string - //go:embed js/create_agent_task.cjs var createAgentTaskScript string @@ -28,9 +25,6 @@ var createPRReviewCommentScript string //go:embed js/create_code_scanning_alert.cjs var createCodeScanningAlertScript string -//go:embed js/add_labels.cjs -var addLabelsScript string - //go:embed js/assign_issue.cjs var assignIssueScript string @@ -91,6 +85,9 @@ var notifyCommentErrorScript string //go:embed js/sanitize.cjs var sanitizeLibScript string +//go:embed js/sanitize_label_content.cjs +var sanitizeLabelContentScript string + //go:embed js/sanitize_workflow_name.cjs var sanitizeWorkflowNameScript string @@ -105,6 +102,12 @@ var computeTextScriptSource string //go:embed js/sanitize_output.cjs var sanitizeOutputScriptSource string +//go:embed js/create_issue.cjs +var createIssueScriptSource string + +//go:embed js/add_labels.cjs +var addLabelsScriptSource string + //go:embed js/parse_firewall_logs.cjs var parseFirewallLogsScriptSource string @@ -119,6 +122,12 @@ var ( sanitizeOutputScript string sanitizeOutputScriptOnce sync.Once + createIssueScript string + createIssueScriptOnce sync.Once + + addLabelsScript string + addLabelsScriptOnce sync.Once + parseFirewallLogsScript string parseFirewallLogsScriptOnce sync.Once ) @@ -171,6 +180,38 @@ func getSanitizeOutputScript() string { return sanitizeOutputScript } +// getCreateIssueScript returns the bundled create_issue script +// Bundling is performed on first access and cached for subsequent calls +func getCreateIssueScript() string { + createIssueScriptOnce.Do(func() { + sources := GetJavaScriptSources() + bundled, err := BundleJavaScriptFromSources(createIssueScriptSource, sources, "") + if err != nil { + // If bundling fails, use the source as-is + createIssueScript = createIssueScriptSource + } else { + createIssueScript = bundled + } + }) + return createIssueScript +} + +// getAddLabelsScript returns the bundled add_labels script +// Bundling is performed on first access and cached for subsequent calls +func getAddLabelsScript() string { + addLabelsScriptOnce.Do(func() { + sources := GetJavaScriptSources() + bundled, err := BundleJavaScriptFromSources(addLabelsScriptSource, sources, "") + if err != nil { + // If bundling fails, use the source as-is + addLabelsScript = addLabelsScriptSource + } else { + addLabelsScript = bundled + } + }) + return addLabelsScript +} + // getParseFirewallLogsScript returns the bundled parse_firewall_logs script // Bundling is performed on first access and cached for subsequent calls func getParseFirewallLogsScript() string { @@ -191,7 +232,8 @@ func getParseFirewallLogsScript() string { // The keys are the relative paths from the js directory func GetJavaScriptSources() map[string]string { return map[string]string{ - "sanitize.cjs": sanitizeLibScript, + "sanitize.cjs": sanitizeLibScript, + "sanitize_label_content.cjs": sanitizeLabelContentScript, "sanitize_workflow_name.cjs": sanitizeWorkflowNameScript, } } diff --git a/pkg/workflow/js/add_labels.cjs b/pkg/workflow/js/add_labels.cjs index b371354ab0..ce3cfdfcef 100644 --- a/pkg/workflow/js/add_labels.cjs +++ b/pkg/workflow/js/add_labels.cjs @@ -1,20 +1,7 @@ // @ts-check /// -function sanitizeLabelContent(content) { - if (!content || typeof content !== "string") { - return ""; - } - let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); - sanitized = sanitized.replace( - /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\`` - ); - sanitized = sanitized.replace(/[<>&'"]/g, ""); - return sanitized.trim(); -} +const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); async function main() { const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; if (!agentOutputFile) { diff --git a/pkg/workflow/js/create_issue.cjs b/pkg/workflow/js/create_issue.cjs index bd832624e2..51d55dcbb1 100644 --- a/pkg/workflow/js/create_issue.cjs +++ b/pkg/workflow/js/create_issue.cjs @@ -1,20 +1,7 @@ // @ts-check /// -function sanitizeLabelContent(content) { - if (!content || typeof content !== "string") { - return ""; - } - let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); - sanitized = sanitized.replace( - /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\`` - ); - sanitized = sanitized.replace(/[<>&'"]/g, ""); - return sanitized.trim(); -} +const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); /** * Generate footer with AI attribution and workflow installation instructions diff --git a/pkg/workflow/js/sanitize_label_content.cjs b/pkg/workflow/js/sanitize_label_content.cjs new file mode 100644 index 0000000000..04041069b3 --- /dev/null +++ b/pkg/workflow/js/sanitize_label_content.cjs @@ -0,0 +1,32 @@ +// @ts-check +/** + * Sanitize label content for GitHub API + * Removes control characters, ANSI codes, and neutralizes @mentions + * @module sanitize_label_content + */ + +/** + * Sanitizes label content by removing control characters, ANSI escape codes, + * and neutralizing @mentions to prevent unintended notifications. + * + * @param {string} content - The label content to sanitize + * @returns {string} The sanitized label content + */ +function sanitizeLabelContent(content) { + if (!content || typeof content !== "string") { + return ""; + } + let sanitized = content.trim(); + // Remove ANSI escape sequences FIRST (before removing control chars) + sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + // Then remove control characters (except newlines and tabs) + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); + sanitized = sanitized.replace( + /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, + (_m, p1, p2) => `${p1}\`@${p2}\`` + ); + sanitized = sanitized.replace(/[<>&'"]/g, ""); + return sanitized.trim(); +} + +module.exports = { sanitizeLabelContent }; diff --git a/pkg/workflow/js/sanitize_label_content.test.cjs b/pkg/workflow/js/sanitize_label_content.test.cjs new file mode 100644 index 0000000000..57edec6ec8 --- /dev/null +++ b/pkg/workflow/js/sanitize_label_content.test.cjs @@ -0,0 +1,142 @@ +import { describe, it, expect } from "vitest"; + +// Import the function to test +const { sanitizeLabelContent } = require("./sanitize_label_content.cjs"); + +describe("sanitize_label_content.cjs", () => { + describe("sanitizeLabelContent", () => { + it("should return empty string for null input", () => { + expect(sanitizeLabelContent(null)).toBe(""); + }); + + it("should return empty string for undefined input", () => { + expect(sanitizeLabelContent(undefined)).toBe(""); + }); + + it("should return empty string for non-string input", () => { + expect(sanitizeLabelContent(123)).toBe(""); + expect(sanitizeLabelContent({})).toBe(""); + expect(sanitizeLabelContent([])).toBe(""); + }); + + it("should trim whitespace from input", () => { + expect(sanitizeLabelContent(" test ")).toBe("test"); + expect(sanitizeLabelContent("\n\ttest\n\t")).toBe("test"); + }); + + it("should remove control characters", () => { + const input = "test\x00\x01\x02\x03\x04\x05\x06\x07\x08label"; + expect(sanitizeLabelContent(input)).toBe("testlabel"); + }); + + it("should remove DEL character (0x7F)", () => { + const input = "test\x7Flabel"; + expect(sanitizeLabelContent(input)).toBe("testlabel"); + }); + + it("should preserve newline character", () => { + const input = "test\nlabel"; + expect(sanitizeLabelContent(input)).toBe("test\nlabel"); + }); + + it("should remove ANSI escape codes", () => { + const input = "\x1b[31mred text\x1b[0m"; + expect(sanitizeLabelContent(input)).toBe("red text"); + }); + + it("should remove various ANSI codes", () => { + const input = "\x1b[1;32mBold Green\x1b[0m\x1b[4mUnderline\x1b[0m"; + expect(sanitizeLabelContent(input)).toBe("Bold GreenUnderline"); + }); + + it("should neutralize @mentions by wrapping in backticks", () => { + expect(sanitizeLabelContent("Hello @user")).toBe("Hello `@user`"); + expect(sanitizeLabelContent("@user said something")).toBe("`@user` said something"); + }); + + it("should neutralize @org/team mentions", () => { + expect(sanitizeLabelContent("Hello @myorg/myteam")).toBe("Hello `@myorg/myteam`"); + }); + + it("should not neutralize @mentions already in backticks", () => { + const input = "Already `@user` handled"; + expect(sanitizeLabelContent(input)).toBe("Already `@user` handled"); + }); + + it("should neutralize multiple @mentions", () => { + const input = "@user1 and @user2 are here"; + expect(sanitizeLabelContent(input)).toBe("`@user1` and `@user2` are here"); + }); + + it("should remove HTML special characters", () => { + expect(sanitizeLabelContent("test<>&'\"label")).toBe("testlabel"); + }); + + it("should remove less-than signs", () => { + expect(sanitizeLabelContent("a < b")).toBe("a b"); + }); + + it("should remove greater-than signs", () => { + expect(sanitizeLabelContent("a > b")).toBe("a b"); + }); + + it("should remove ampersands", () => { + expect(sanitizeLabelContent("test & label")).toBe("test label"); + }); + + it("should remove single and double quotes", () => { + expect(sanitizeLabelContent('test\'s "label"')).toBe("tests label"); + }); + + it("should handle complex input with multiple sanitizations", () => { + const input = " @user \x1b[31mred\x1b[0m test&label "; + expect(sanitizeLabelContent(input)).toBe("`@user` red tag testlabel"); + }); + + it("should handle empty string input", () => { + expect(sanitizeLabelContent("")).toBe(""); + }); + + it("should handle whitespace-only input", () => { + expect(sanitizeLabelContent(" \n\t ")).toBe(""); + }); + + it("should preserve normal alphanumeric characters", () => { + expect(sanitizeLabelContent("bug123")).toBe("bug123"); + expect(sanitizeLabelContent("feature-request")).toBe("feature-request"); + }); + + it("should preserve hyphens and underscores", () => { + expect(sanitizeLabelContent("test-label_123")).toBe("test-label_123"); + }); + + it("should handle consecutive control characters", () => { + const input = "test\x00\x01\x02\x03\x04\x05label"; + expect(sanitizeLabelContent(input)).toBe("testlabel"); + }); + + it("should handle @mentions at various positions", () => { + expect(sanitizeLabelContent("start @user end")).toBe("start `@user` end"); + expect(sanitizeLabelContent("@user at start")).toBe("`@user` at start"); + expect(sanitizeLabelContent("at end @user")).toBe("at end `@user`"); + }); + + it("should not treat email-like patterns as @mentions after alphanumerics", () => { + const input = "email@example.com"; + // The regex has [^\w`] which requires non-word character before @ + // so 'email@' won't match because 'l' is a word character + expect(sanitizeLabelContent(input)).toBe("email@example.com"); + }); + + it("should handle username edge cases", () => { + // Valid GitHub usernames can be 1-39 chars, alphanumeric + hyphens + expect(sanitizeLabelContent("@a")).toBe("`@a`"); + expect(sanitizeLabelContent("@user-name-123")).toBe("`@user-name-123`"); + }); + + it("should combine all sanitization rules correctly", () => { + const input = ' \x1b[31m@user\x1b[0m says & "goodbye" '; + expect(sanitizeLabelContent(input)).toBe("`@user` says hello goodbye"); + }); + }); +}); diff --git a/pkg/workflow/js_test.go b/pkg/workflow/js_test.go index 42ed932467..fe0c05804f 100644 --- a/pkg/workflow/js_test.go +++ b/pkg/workflow/js_test.go @@ -238,11 +238,11 @@ func TestEmbeddedScriptsNotEmpty(t *testing.T) { script string }{ {"createPullRequestScript", createPullRequestScript}, - {"createIssueScript", createIssueScript}, + {"createIssueScript", getCreateIssueScript()}, {"createDiscussionScript", createDiscussionScript}, {"createCommentScript", createCommentScript}, {"collectJSONLOutputScript", getCollectJSONLOutputScript()}, - {"addLabelsScript", addLabelsScript}, + {"addLabelsScript", getAddLabelsScript()}, {"updateIssueScript", updateIssueScript}, {"addReactionAndEditCommentScript", addReactionAndEditCommentScript}, {"missingToolScript", missingToolScript},