Skip to content

Commit d08ca84

Browse files
Copilotpelikhan
andauthored
Add issue intents runtime support
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 165beb4 commit d08ca84

13 files changed

Lines changed: 693 additions & 93 deletions

actions/setup/js/issue_intents.cjs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// @ts-check
2+
/// <reference types="@actions/github-script" />
3+
4+
const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
5+
const { hasRuntimeFeature, parseRuntimeFeatures } = require("./runtime_features.cjs");
6+
7+
const ISSUE_INTENTS_FEATURE = "issue_intents";
8+
const ISSUE_INTENT_CONFIDENCE_VALUES = new Set(["LOW", "MEDIUM", "HIGH"]);
9+
10+
function hasIssueIntentsRuntimeFeature() {
11+
if (typeof global.hasRuntimeFeature === "function") {
12+
return global.hasRuntimeFeature(ISSUE_INTENTS_FEATURE);
13+
}
14+
return hasRuntimeFeature(parseRuntimeFeatures(process.env.GH_AW_RUNTIME_FEATURES), ISSUE_INTENTS_FEATURE);
15+
}
16+
17+
function normalizeIssueIntentMetadata(source) {
18+
if (!source || typeof source !== "object") {
19+
return {};
20+
}
21+
22+
/** @type {{ rationale?: string, confidence?: "LOW"|"MEDIUM"|"HIGH", suggest?: boolean }} */
23+
const metadata = {};
24+
25+
if (typeof source.rationale === "string") {
26+
const rationale = source.rationale.trim();
27+
if (rationale) {
28+
metadata.rationale = rationale;
29+
}
30+
}
31+
32+
if (source.confidence !== undefined && source.confidence !== null && source.confidence !== "") {
33+
const confidence = String(source.confidence).trim().toUpperCase();
34+
if (!ISSUE_INTENT_CONFIDENCE_VALUES.has(confidence)) {
35+
throw new Error(`Invalid confidence ${JSON.stringify(source.confidence)}. Expected one of: LOW, MEDIUM, HIGH.`);
36+
}
37+
metadata.confidence = /** @type {"LOW"|"MEDIUM"|"HIGH"} */ confidence;
38+
}
39+
40+
if (source.suggest !== undefined) {
41+
if (typeof source.suggest !== "boolean") {
42+
throw new Error(`Invalid suggest ${JSON.stringify(source.suggest)}. Expected a boolean value.`);
43+
}
44+
if (source.suggest) {
45+
metadata.suggest = true;
46+
}
47+
}
48+
49+
return metadata;
50+
}
51+
52+
function normalizeIssueIntentLabelSpecs(labels) {
53+
if (!Array.isArray(labels)) {
54+
return [];
55+
}
56+
57+
return labels.map((label, index) => {
58+
if (typeof label === "string") {
59+
const name = sanitizeLabelContent(label);
60+
if (!name) {
61+
throw new Error(`Invalid labels[${index}] entry. Label names must be non-empty strings.`);
62+
}
63+
if (name.startsWith("-")) {
64+
throw new Error(`Label removal is not permitted. Found line starting with '-': ${name}`);
65+
}
66+
return { name };
67+
}
68+
69+
if (!label || typeof label !== "object" || typeof label.name !== "string") {
70+
throw new Error(`Invalid labels[${index}] entry. Expected a string label name or an object with a string "name" field.`);
71+
}
72+
73+
const name = sanitizeLabelContent(label.name);
74+
if (!name) {
75+
throw new Error(`Invalid labels[${index}] entry. Label names must be non-empty strings.`);
76+
}
77+
if (name.startsWith("-")) {
78+
throw new Error(`Label removal is not permitted. Found line starting with '-': ${name}`);
79+
}
80+
81+
return {
82+
name,
83+
...normalizeIssueIntentMetadata(label),
84+
};
85+
});
86+
}
87+
88+
function getIssueIntentLabelNames(labelSpecs) {
89+
return labelSpecs.map(label => label.name);
90+
}
91+
92+
function buildIssueIntentLabelUpdates(labelSpecs, labelIdByName) {
93+
return labelSpecs.map(spec => {
94+
const labelId = labelIdByName.get(spec.name.toLowerCase());
95+
if (!labelId) {
96+
throw new Error(`Label ${JSON.stringify(spec.name)} not found. Ensure the label exists in the target repository.`);
97+
}
98+
99+
return {
100+
labelId,
101+
...normalizeIssueIntentMetadata(spec),
102+
};
103+
});
104+
}
105+
106+
module.exports = {
107+
buildIssueIntentLabelUpdates,
108+
getIssueIntentLabelNames,
109+
hasIssueIntentsRuntimeFeature,
110+
normalizeIssueIntentLabelSpecs,
111+
normalizeIssueIntentMetadata,
112+
};

actions/setup/js/safe_output_type_validator.test.cjs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ const SAMPLE_VALIDATION_CONFIG = {
3838
},
3939
update_issue: {
4040
defaultMax: 1,
41-
customValidation: "requiresOneOf:status,title,body",
41+
customValidation: "requiresOneOf:status,title,body,labels,assignees,milestone",
4242
fields: {
4343
status: { type: "string", enum: ["open", "closed"] },
4444
title: { type: "string", sanitize: true, maxLength: 128 },
4545
body: { type: "string", sanitize: true, maxLength: 65000 },
46+
labels: { type: "array" },
47+
assignees: { type: "array", itemType: "string", itemSanitize: true, itemMaxLength: 39 },
48+
milestone: { optionalPositiveInteger: true },
4649
issue_number: { issueOrPRNumber: true },
4750
},
4851
},
@@ -94,6 +97,29 @@ const SAMPLE_VALIDATION_CONFIG = {
9497
repo: { type: "string", maxLength: 256 },
9598
},
9699
},
100+
set_issue_type: {
101+
defaultMax: 5,
102+
fields: {
103+
issue_number: { issueOrPRNumber: true },
104+
issue_type: { required: true, type: "string", sanitize: true, maxLength: 128 },
105+
rationale: { type: "string", sanitize: true, maxLength: 1024 },
106+
confidence: { type: "string", enum: ["LOW", "MEDIUM", "HIGH"] },
107+
suggest: { type: "boolean" },
108+
},
109+
},
110+
set_issue_field: {
111+
defaultMax: 5,
112+
customValidation: "requiresOneOf:field_name,field_node_id",
113+
fields: {
114+
issue_number: { issueOrPRNumber: true },
115+
field_name: { type: "string", sanitize: true, maxLength: 128 },
116+
field_node_id: { type: "string", maxLength: 256 },
117+
value: { required: true, type: "string", sanitize: true, maxLength: 256 },
118+
rationale: { type: "string", sanitize: true, maxLength: 1024 },
119+
confidence: { type: "string", enum: ["LOW", "MEDIUM", "HIGH"] },
120+
suggest: { type: "boolean" },
121+
},
122+
},
97123
link_sub_issue: {
98124
defaultMax: 5,
99125
customValidation: "parentAndSubDifferent",
@@ -691,6 +717,14 @@ describe("safe_output_type_validator", () => {
691717
expect(result.isValid).toBe(true);
692718
});
693719

720+
it("should pass when update_issue only includes labels", async () => {
721+
const { validateItem } = await import("./safe_output_type_validator.cjs");
722+
723+
const result = validateItem({ type: "update_issue", labels: [{ name: "bug", confidence: "HIGH" }] }, "update_issue", 1);
724+
725+
expect(result.isValid).toBe(true);
726+
});
727+
694728
it("should fail when none of the required fields are present", async () => {
695729
const { validateItem } = await import("./safe_output_type_validator.cjs");
696730

@@ -822,6 +856,18 @@ describe("safe_output_type_validator", () => {
822856
expect(result.isValid).toBe(false);
823857
expect(result.error).toContain("must be 'open' or 'closed'");
824858
});
859+
860+
it("should validate issue intent confidence enums", async () => {
861+
const { validateItem } = await import("./safe_output_type_validator.cjs");
862+
863+
const typeResult = validateItem({ type: "set_issue_type", issue_type: "Bug", confidence: "high", suggest: true }, "set_issue_type", 1);
864+
expect(typeResult.isValid).toBe(true);
865+
expect(typeResult.normalizedItem.confidence).toBe("HIGH");
866+
867+
const fieldResult = validateItem({ type: "set_issue_field", field_name: "Priority", value: "P1", confidence: "medium", rationale: "Customer escalation" }, "set_issue_field", 1);
868+
expect(fieldResult.isValid).toBe(true);
869+
expect(fieldResult.normalizedItem.confidence).toBe("MEDIUM");
870+
});
825871
});
826872

827873
describe("pattern validation", () => {

0 commit comments

Comments
 (0)