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},