From bcc2eff8bf0c24bb3b74392b7bb926e62cae6f48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:19:19 +0000 Subject: [PATCH 1/3] Add confidence plumbing for issue safe outputs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/safe_output_type_validator.cjs | 14 +++++++++++ .../js/safe_output_type_validator.test.cjs | 24 +++++++++++++++++++ actions/setup/js/safe_outputs_tools.json | 24 ++++++++++++++++--- actions/setup/js/set_issue_field.cjs | 13 ++++++---- actions/setup/js/set_issue_field.test.cjs | 3 +++ actions/setup/js/set_issue_type.cjs | 14 +++++++---- actions/setup/js/set_issue_type.test.cjs | 4 +++- actions/setup/js/types/safe-outputs.d.ts | 6 +++++ actions/setup/js/update_issue.cjs | 3 +++ .../setup/js/update_issue_generator.test.cjs | 15 ++++++++++++ pkg/workflow/js/safe_outputs_tools.json | 24 ++++++++++++++++--- .../safe_outputs_validation_config.go | 5 ++++ 12 files changed, 133 insertions(+), 16 deletions(-) diff --git a/actions/setup/js/safe_output_type_validator.cjs b/actions/setup/js/safe_output_type_validator.cjs index 0196d83d4b5..fff02bf7694 100644 --- a/actions/setup/js/safe_output_type_validator.cjs +++ b/actions/setup/js/safe_output_type_validator.cjs @@ -78,6 +78,8 @@ function normalizeIssueClosingKeywordBackticks(content) { * @property {string} [itemType] - For arrays, the type of items * @property {boolean} [itemSanitize] - For arrays, whether to sanitize items * @property {number} [itemMaxLength] - For arrays, max length per item + * @property {number} [minimum] - For numbers, minimum allowed value (inclusive) + * @property {number} [maximum] - For numbers, maximum allowed value (inclusive) * @property {string} [pattern] - Regex pattern the value must match * @property {string} [patternError] - Error message for pattern mismatch */ @@ -468,6 +470,18 @@ function validateField(value, fieldName, validation, itemType, lineNum, options) error: `Line ${lineNum}: ${itemType} '${fieldName}' must be a number`, }; } + if (validation.minimum !== undefined && value < validation.minimum) { + return { + isValid: false, + error: `Line ${lineNum}: ${itemType} '${fieldName}' must be >= ${validation.minimum}`, + }; + } + if (validation.maximum !== undefined && value > validation.maximum) { + return { + isValid: false, + error: `Line ${lineNum}: ${itemType} '${fieldName}' must be <= ${validation.maximum}`, + }; + } return { isValid: true, normalizedValue: value }; } diff --git a/actions/setup/js/safe_output_type_validator.test.cjs b/actions/setup/js/safe_output_type_validator.test.cjs index 1b9dfb9abf1..4f9f899baa3 100644 --- a/actions/setup/js/safe_output_type_validator.test.cjs +++ b/actions/setup/js/safe_output_type_validator.test.cjs @@ -43,6 +43,7 @@ const SAMPLE_VALIDATION_CONFIG = { status: { type: "string", enum: ["open", "closed"] }, title: { type: "string", sanitize: true, maxLength: 128 }, body: { type: "string", sanitize: true, maxLength: 65000 }, + confidence: { type: "number", minimum: 0, maximum: 100 }, issue_number: { issueOrPRNumber: true }, }, }, @@ -705,6 +706,29 @@ describe("safe_output_type_validator", () => { expect(result.normalizedItem.status).toBe("open"); }); + describe("number range validation", () => { + it("should accept confidence within range", async () => { + const { validateItem } = await import("./safe_output_type_validator.cjs"); + const result = validateItem({ type: "update_issue", status: "open", confidence: 75 }, "update_issue", 1); + expect(result.isValid).toBe(true); + expect(result.normalizedItem.confidence).toBe(75); + }); + + it("should reject confidence below minimum", async () => { + const { validateItem } = await import("./safe_output_type_validator.cjs"); + const result = validateItem({ type: "update_issue", status: "open", confidence: -1 }, "update_issue", 1); + expect(result.isValid).toBe(false); + expect(result.error).toContain("must be >= 0"); + }); + + it("should reject confidence above maximum", async () => { + const { validateItem } = await import("./safe_output_type_validator.cjs"); + const result = validateItem({ type: "update_issue", status: "open", confidence: 101 }, "update_issue", 1); + expect(result.isValid).toBe(false); + expect(result.error).toContain("must be <= 100"); + }); + }); + it("should reject invalid enum value", async () => { const { validateItem } = await import("./safe_output_type_validator.cjs"); diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index 99f51f7eb91..ed33f500f20 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -759,7 +759,7 @@ }, { "name": "update_issue", - "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", + "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. Include `confidence` (0-100) when possible to indicate certainty in the update. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", "inputSchema": { "type": "object", "properties": { @@ -803,6 +803,12 @@ "type": ["number", "string"], "description": "Issue number to update. This is the numeric ID from the GitHub URL (e.g., 789 in github.com/owner/repo/issues/789). ONLY effective when the workflow is configured with `update-issue: target: '*'` in the frontmatter. When the workflow uses `target: triggering` (the default), this field is ignored and the tool updates the issue that triggered the workflow instead. If you need to update a specific issue in a scheduled or workflow_dispatch workflow, the workflow frontmatter must include `update-issue: target: '*'`." }, + "confidence": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Optional confidence score from 0 to 100 that expresses how certain you are this issue update is correct. Use lower values (0-40) for weak evidence, mid-range values (41-79) for partial confidence, and high values (80-100) for strong confidence." + }, "secrecy": { "type": "string", "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." @@ -1150,7 +1156,7 @@ }, { "name": "set_issue_type", - "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned.", + "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned. Include `confidence` (0-100) when possible to indicate certainty in the selected type.", "inputSchema": { "type": "object", "required": ["issue_type"], @@ -1163,6 +1169,12 @@ "type": "string", "description": "Issue type name to set (e.g., \"Bug\", \"Feature\", \"Task\"). Use an empty string \"\" to clear the current issue type." }, + "confidence": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Optional confidence score from 0 to 100 that expresses how certain you are this issue type assignment is correct. Use lower values (0-40) for weak evidence, mid-range values (41-79) for partial confidence, and high values (80-100) for strong confidence." + }, "secrecy": { "type": "string", "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." @@ -1177,7 +1189,7 @@ }, { "name": "set_issue_field", - "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name).", + "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name). Include `confidence` (0-100) when possible to indicate certainty in the field update.", "inputSchema": { "type": "object", "required": ["value"], @@ -1198,6 +1210,12 @@ "type": "string", "description": "Field value to set. For single-select fields, this must match an existing option name (e.g., \"P1\" or \"High\"). For date fields, use format: YYYY-MM-DD (for example, 2026-05-08)." }, + "confidence": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Optional confidence score from 0 to 100 that expresses how certain you are this field update is correct. Use lower values (0-40) for weak evidence, mid-range values (41-79) for partial confidence, and high values (80-100) for strong confidence." + }, "secrecy": { "type": "string", "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." diff --git a/actions/setup/js/set_issue_field.cjs b/actions/setup/js/set_issue_field.cjs index 73928fbf8a0..efea5162b9d 100644 --- a/actions/setup/js/set_issue_field.cjs +++ b/actions/setup/js/set_issue_field.cjs @@ -182,12 +182,13 @@ function buildFieldUpdatePayload(field, rawValue) { * @param {Object} githubClient - Authenticated GitHub client * @param {string} issueNodeId - GraphQL node ID of the issue * @param {{fieldId: string, singleSelectOptionId?: string, numberValue?: number, dateValue?: string, textValue?: string}} fieldUpdate + * @param {number|undefined} confidence - Optional confidence score (0-100) * @returns {Promise} */ -async function setIssueFieldValue(githubClient, issueNodeId, fieldUpdate) { +async function setIssueFieldValue(githubClient, issueNodeId, fieldUpdate, confidence) { await githubClient.graphql( - `mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!) { - setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields }) { + `mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!, $confidence: Int) { + setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields, confidence: $confidence }) { issue { id } @@ -196,6 +197,7 @@ async function setIssueFieldValue(githubClient, issueNodeId, fieldUpdate) { { issueId: issueNodeId, issueFields: [fieldUpdate], + confidence, } ); } @@ -300,6 +302,7 @@ async function main(config = {}) { field_name: fieldName, field_node_id: fieldNodeId, value, + confidence: typeof item.confidence === "number" ? item.confidence : undefined, repo: itemRepo, }, }; @@ -374,7 +377,8 @@ async function main(config = {}) { ...fieldUpdateResult.update, }; - await setIssueFieldValue(githubClient, issueNodeId, fieldUpdate); + const confidence = typeof item.confidence === "number" ? item.confidence : undefined; + await setIssueFieldValue(githubClient, issueNodeId, fieldUpdate, confidence); core.info(`Successfully set issue field ${JSON.stringify(fieldName || fieldNodeId)} to ${JSON.stringify(value)} on issue #${issueNumber}`); @@ -384,6 +388,7 @@ async function main(config = {}) { field_name: fieldName, field_node_id: fieldNodeId, value, + confidence, repo: itemRepo, }; } catch (error) { diff --git a/actions/setup/js/set_issue_field.test.cjs b/actions/setup/js/set_issue_field.test.cjs index e3b7c8b5bb5..c2ee7df57ac 100644 --- a/actions/setup/js/set_issue_field.test.cjs +++ b/actions/setup/js/set_issue_field.test.cjs @@ -105,6 +105,7 @@ describe("set_issue_field (Handler Factory Architecture)", () => { issue_number: 42, field_name: "Customer Impact", value: "High", + confidence: 72, }; const result = await handler(message, {}); @@ -113,11 +114,13 @@ describe("set_issue_field (Handler Factory Architecture)", () => { expect(result.issue_number).toBe(42); expect(result.field_name).toBe("Customer Impact"); expect(result.field_node_id).toBe(textFieldId); + expect(result.confidence).toBe(72); expect(mockGraphql).toHaveBeenCalledWith( expect.stringContaining("setIssueFieldValue"), expect.objectContaining({ issueId: issueNodeId, issueFields: [expect.objectContaining({ fieldId: textFieldId, textValue: "High" })], + confidence: 72, }) ); }); diff --git a/actions/setup/js/set_issue_type.cjs b/actions/setup/js/set_issue_type.cjs index f3ec956c878..25e40e34892 100644 --- a/actions/setup/js/set_issue_type.cjs +++ b/actions/setup/js/set_issue_type.cjs @@ -72,18 +72,19 @@ async function fetchIssueTypes(githubClient, owner, repo) { * @param {Object} githubClient - Authenticated GitHub client * @param {string} issueNodeId - GraphQL node ID of the issue * @param {string|null} typeId - GraphQL node ID of the issue type, or null to clear + * @param {number|undefined} confidence - Optional confidence score (0-100) * @returns {Promise} */ -async function setIssueTypeById(githubClient, issueNodeId, typeId) { +async function setIssueTypeById(githubClient, issueNodeId, typeId, confidence) { await githubClient.graphql( - `mutation($issueId: ID!, $typeId: ID) { - updateIssue(input: { id: $issueId, issueTypeId: $typeId }) { + `mutation($issueId: ID!, $typeId: ID, $confidence: Int) { + updateIssue(input: { id: $issueId, issueTypeId: $typeId, confidence: $confidence }) { issue { id } } }`, - { issueId: issueNodeId, typeId } + { issueId: issueNodeId, typeId, confidence } ); } @@ -197,6 +198,7 @@ async function main(config = {}) { previewInfo: { issue_number: issueNumber, issue_type: issueTypeName, + confidence: typeof item.confidence === "number" ? item.confidence : undefined, repo: itemRepo, }, }; @@ -231,7 +233,8 @@ async function main(config = {}) { core.info(`Resolved issue type ${JSON.stringify(issueTypeName)} to node ID: ${typeId}`); } - await setIssueTypeById(githubClient, issueNodeId, typeId); + const confidence = typeof item.confidence === "number" ? item.confidence : undefined; + await setIssueTypeById(githubClient, issueNodeId, typeId, confidence); const successMsg = isClear ? `Successfully cleared issue type on issue #${issueNumber}` : `Successfully set issue type to ${JSON.stringify(issueTypeName)} on issue #${issueNumber}`; core.info(successMsg); @@ -240,6 +243,7 @@ async function main(config = {}) { success: true, issue_number: issueNumber, issue_type: issueTypeName, + confidence, repo: itemRepo, }; } catch (error) { diff --git a/actions/setup/js/set_issue_type.test.cjs b/actions/setup/js/set_issue_type.test.cjs index ae07f9cfa20..10e20b546ad 100644 --- a/actions/setup/js/set_issue_type.test.cjs +++ b/actions/setup/js/set_issue_type.test.cjs @@ -88,6 +88,7 @@ describe("set_issue_type (Handler Factory Architecture)", () => { type: "set_issue_type", issue_number: 42, issue_type: "Bug", + confidence: 88, }; const result = await handler(message, {}); @@ -95,12 +96,13 @@ describe("set_issue_type (Handler Factory Architecture)", () => { expect(result.success).toBe(true); expect(result.issue_number).toBe(42); expect(result.issue_type).toBe("Bug"); + expect(result.confidence).toBe(88); expect(mockGithub.rest.issues.get).toHaveBeenCalledWith({ owner: "test-owner", repo: "test-repo", issue_number: 42, }); - expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("updateIssue"), expect.objectContaining({ issueId: issueNodeId, typeId: bugTypeId })); + expect(mockGraphql).toHaveBeenCalledWith(expect.stringContaining("updateIssue"), expect.objectContaining({ issueId: issueNodeId, typeId: bugTypeId, confidence: 88 })); }); it("should clear issue type when issue_type is empty string", async () => { diff --git a/actions/setup/js/types/safe-outputs.d.ts b/actions/setup/js/types/safe-outputs.d.ts index 4666139befd..2ecbb4370f5 100644 --- a/actions/setup/js/types/safe-outputs.d.ts +++ b/actions/setup/js/types/safe-outputs.d.ts @@ -246,6 +246,8 @@ interface UpdateIssueItem extends BaseSafeOutputItem { body?: string; /** Optional issue number for target "*" */ issue_number?: number | string; + /** Optional confidence score (0-100) indicating certainty for this issue update. */ + confidence?: number; } /** @@ -322,6 +324,8 @@ interface SetIssueTypeItem extends BaseSafeOutputItem { issue_type: string; /** Issue number (optional - uses triggering issue if not provided) */ issue_number?: number | string; + /** Optional confidence score (0-100) indicating certainty in the issue type selection. */ + confidence?: number; } /** @@ -337,6 +341,8 @@ interface SetIssueFieldItem extends BaseSafeOutputItem { value: string; /** Issue number (optional - uses triggering issue if not provided) */ issue_number?: number | string; + /** Optional confidence score (0-100) indicating certainty in the field update value. */ + confidence?: number; } /** diff --git a/actions/setup/js/update_issue.cjs b/actions/setup/js/update_issue.cjs index 5abe7f11317..d13560387e3 100644 --- a/actions/setup/js/update_issue.cjs +++ b/actions/setup/js/update_issue.cjs @@ -164,6 +164,9 @@ function buildIssueUpdateData(item, config) { if (item.milestone !== undefined) { updateData.milestone = item.milestone; } + if (item.confidence !== undefined) { + updateData.confidence = item.confidence; + } // Enforce max limits on labels and assignees before API calls const labelsLimitResult = tryEnforceArrayLimit(updateData.labels, MAX_LABELS, "labels"); diff --git a/actions/setup/js/update_issue_generator.test.cjs b/actions/setup/js/update_issue_generator.test.cjs index 1e1f670b8a2..5b97176fb6e 100644 --- a/actions/setup/js/update_issue_generator.test.cjs +++ b/actions/setup/js/update_issue_generator.test.cjs @@ -59,6 +59,21 @@ describe("update_issue.cjs - generator payload", () => { expect(data.state).toBe("closed"); }); + it("passes confidence through to issue update payload", async () => { + const updateIssueModule = await import("./update_issue.cjs"); + + const { success, data } = updateIssueModule.buildIssueUpdateData( + { + status: "open", + confidence: 91, + }, + {} + ); + + expect(success).toBe(true); + expect(data.confidence).toBe(91); + }); + it("respects explicit operation when provided", async () => { const updateIssueModule = await import("./update_issue.cjs"); diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 2c0ec5098dc..1898a77aded 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -903,7 +903,7 @@ }, { "name": "update_issue", - "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", + "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. Include `confidence` (0-100) when possible to indicate certainty in the update. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", "inputSchema": { "type": "object", "properties": { @@ -961,6 +961,12 @@ ], "description": "Issue number to update. This is the numeric ID from the GitHub URL (e.g., 789 in github.com/owner/repo/issues/789). ONLY effective when the workflow is configured with `update-issue: target: '*'` in the frontmatter. When the workflow uses `target: triggering` (the default), this field is ignored and the tool updates the issue that triggered the workflow instead. If you need to update a specific issue in a scheduled or workflow_dispatch workflow, the workflow frontmatter must include `update-issue: target: '*'`." }, + "confidence": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Optional confidence score from 0 to 100 that expresses how certain you are this issue update is correct. Use lower values (0-40) for weak evidence, mid-range values (41-79) for partial confidence, and high values (80-100) for strong confidence." + }, "secrecy": { "type": "string", "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." @@ -1365,7 +1371,7 @@ }, { "name": "set_issue_type", - "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned.", + "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned. Include `confidence` (0-100) when possible to indicate certainty in the selected type.", "inputSchema": { "type": "object", "required": [ @@ -1383,6 +1389,12 @@ "type": "string", "description": "Issue type name to set (e.g., \"Bug\", \"Feature\", \"Task\"). Use an empty string \"\" to clear the current issue type." }, + "confidence": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Optional confidence score from 0 to 100 that expresses how certain you are this issue type assignment is correct. Use lower values (0-40) for weak evidence, mid-range values (41-79) for partial confidence, and high values (80-100) for strong confidence." + }, "secrecy": { "type": "string", "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." @@ -1397,7 +1409,7 @@ }, { "name": "set_issue_field", - "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name).", + "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name). Include `confidence` (0-100) when possible to indicate certainty in the field update.", "inputSchema": { "type": "object", "required": [ @@ -1423,6 +1435,12 @@ "type": "string", "description": "Field value to set. For single-select fields, this must match an existing option name (e.g., \"P1\" or \"High\"). For date fields, use format: YYYY-MM-DD (for example, 2026-05-08)." }, + "confidence": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Optional confidence score from 0 to 100 that expresses how certain you are this field update is correct. Use lower values (0-40) for weak evidence, mid-range values (41-79) for partial confidence, and high values (80-100) for strong confidence." + }, "secrecy": { "type": "string", "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\")." diff --git a/pkg/workflow/safe_outputs_validation_config.go b/pkg/workflow/safe_outputs_validation_config.go index b799600a862..f04ea367d90 100644 --- a/pkg/workflow/safe_outputs_validation_config.go +++ b/pkg/workflow/safe_outputs_validation_config.go @@ -27,6 +27,8 @@ type FieldValidation struct { ItemType string `json:"itemType,omitempty"` ItemSanitize bool `json:"itemSanitize,omitempty"` ItemMaxLength int `json:"itemMaxLength,omitempty"` + Minimum int `json:"minimum,omitempty"` + Maximum int `json:"maximum,omitempty"` Pattern string `json:"pattern,omitempty"` PatternError string `json:"patternError,omitempty"` TemporaryID bool `json:"temporaryId,omitempty"` @@ -130,6 +132,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ Fields: map[string]FieldValidation{ "issue_number": {IssueOrPRNumber: true}, "issue_type": {Required: true, Type: "string", Sanitize: true, MaxLength: 128}, // Empty string clears the type + "confidence": {Type: "number", Minimum: 0, Maximum: 100}, "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" }, }, @@ -141,6 +144,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "field_name": {Type: "string", Sanitize: true, MaxLength: 128}, "field_node_id": {Type: "string", MaxLength: 256}, "value": {Required: true, Type: "string", Sanitize: true, MaxLength: 256}, + "confidence": {Type: "number", Minimum: 0, Maximum: 100}, "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" }, }, @@ -175,6 +179,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "labels": {Type: "array", ItemType: "string", ItemSanitize: true, ItemMaxLength: 128}, "assignees": {Type: "array", ItemType: "string", ItemSanitize: true, ItemMaxLength: MaxGitHubUsernameLength}, "milestone": {OptionalPositiveInteger: true}, + "confidence": {Type: "number", Minimum: 0, Maximum: 100}, "issue_number": {IssueOrPRNumber: true}, "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" }, From 61dea34cf33e12323a9d534c656ecf2d1af5dbb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:20:12 +0000 Subject: [PATCH 2/3] Validate confidence ranges in safe output config Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/safe_outputs_validation_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/safe_outputs_validation_config.go b/pkg/workflow/safe_outputs_validation_config.go index f04ea367d90..a3edaca9b3f 100644 --- a/pkg/workflow/safe_outputs_validation_config.go +++ b/pkg/workflow/safe_outputs_validation_config.go @@ -133,7 +133,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "issue_number": {IssueOrPRNumber: true}, "issue_type": {Required: true, Type: "string", Sanitize: true, MaxLength: 128}, // Empty string clears the type "confidence": {Type: "number", Minimum: 0, Maximum: 100}, - "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" + "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" }, }, "set_issue_field": { From d81dd4548b91143afe59ae2951bd5097ab477f5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:23:51 +0000 Subject: [PATCH 3/3] Harden confidence handling with compatibility fallbacks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_tools.json | 6 +-- actions/setup/js/set_issue_field.cjs | 61 +++++++++++++++++------ actions/setup/js/set_issue_field.test.cjs | 3 +- actions/setup/js/set_issue_type.cjs | 39 +++++++++++---- actions/setup/js/update_issue.cjs | 21 +++++++- pkg/workflow/js/safe_outputs_tools.json | 6 +-- 6 files changed, 101 insertions(+), 35 deletions(-) diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index ed33f500f20..dab602f2eae 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -759,7 +759,7 @@ }, { "name": "update_issue", - "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. Include `confidence` (0-100) when possible to indicate certainty in the update. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", + "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. Include `confidence` (0-100) when the update is inferred from analysis or heuristics rather than explicit user instruction. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", "inputSchema": { "type": "object", "properties": { @@ -1156,7 +1156,7 @@ }, { "name": "set_issue_type", - "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned. Include `confidence` (0-100) when possible to indicate certainty in the selected type.", + "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned. Include `confidence` (0-100) when the selected type is inferred from issue signals rather than explicit user instruction.", "inputSchema": { "type": "object", "required": ["issue_type"], @@ -1189,7 +1189,7 @@ }, { "name": "set_issue_field", - "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name). Include `confidence` (0-100) when possible to indicate certainty in the field update.", + "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name). Include `confidence` (0-100) when the field value is inferred rather than explicitly requested.", "inputSchema": { "type": "object", "required": ["value"], diff --git a/actions/setup/js/set_issue_field.cjs b/actions/setup/js/set_issue_field.cjs index efea5162b9d..e69eeebfc52 100644 --- a/actions/setup/js/set_issue_field.cjs +++ b/actions/setup/js/set_issue_field.cjs @@ -181,25 +181,51 @@ function buildFieldUpdatePayload(field, rawValue) { * Sets one issue field via GraphQL mutation. * @param {Object} githubClient - Authenticated GitHub client * @param {string} issueNodeId - GraphQL node ID of the issue - * @param {{fieldId: string, singleSelectOptionId?: string, numberValue?: number, dateValue?: string, textValue?: string}} fieldUpdate - * @param {number|undefined} confidence - Optional confidence score (0-100) + * @param {{fieldId: string, singleSelectOptionId?: string, numberValue?: number, dateValue?: string, textValue?: string, confidence?: number}} fieldUpdate * @returns {Promise} */ -async function setIssueFieldValue(githubClient, issueNodeId, fieldUpdate, confidence) { - await githubClient.graphql( - `mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!, $confidence: Int) { - setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields, confidence: $confidence }) { - issue { - id +async function setIssueFieldValue(githubClient, issueNodeId, fieldUpdate) { + const variables = { + issueId: issueNodeId, + issueFields: [fieldUpdate], + }; + + try { + await githubClient.graphql( + `mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!) { + setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields }) { + issue { + id + } } - } - }`, - { - issueId: issueNodeId, - issueFields: [fieldUpdate], - confidence, + }`, + variables + ); + } catch (error) { + const message = getErrorMessage(error).toLowerCase(); + const hasConfidence = typeof fieldUpdate.confidence === "number"; + const mentionsConfidence = message.includes("confidence"); + if (!hasConfidence || !mentionsConfidence) { + throw error; } - ); + + const retryFieldUpdate = { ...fieldUpdate }; + delete retryFieldUpdate.confidence; + core.warning("Issue field confidence is not supported by this API; retrying without confidence."); + await githubClient.graphql( + `mutation($issueId: ID!, $issueFields: [IssueFieldCreateOrUpdateInput!]!) { + setIssueFieldValue(input: { issueId: $issueId, issueFields: $issueFields }) { + issue { + id + } + } + }`, + { + issueId: issueNodeId, + issueFields: [retryFieldUpdate], + } + ); + } } /** @@ -378,7 +404,10 @@ async function main(config = {}) { }; const confidence = typeof item.confidence === "number" ? item.confidence : undefined; - await setIssueFieldValue(githubClient, issueNodeId, fieldUpdate, confidence); + if (confidence !== undefined) { + fieldUpdate.confidence = confidence; + } + await setIssueFieldValue(githubClient, issueNodeId, fieldUpdate); core.info(`Successfully set issue field ${JSON.stringify(fieldName || fieldNodeId)} to ${JSON.stringify(value)} on issue #${issueNumber}`); diff --git a/actions/setup/js/set_issue_field.test.cjs b/actions/setup/js/set_issue_field.test.cjs index c2ee7df57ac..814586606f8 100644 --- a/actions/setup/js/set_issue_field.test.cjs +++ b/actions/setup/js/set_issue_field.test.cjs @@ -119,8 +119,7 @@ describe("set_issue_field (Handler Factory Architecture)", () => { expect.stringContaining("setIssueFieldValue"), expect.objectContaining({ issueId: issueNodeId, - issueFields: [expect.objectContaining({ fieldId: textFieldId, textValue: "High" })], - confidence: 72, + issueFields: [expect.objectContaining({ fieldId: textFieldId, textValue: "High", confidence: 72 })], }) ); }); diff --git a/actions/setup/js/set_issue_type.cjs b/actions/setup/js/set_issue_type.cjs index 25e40e34892..39aa0c48482 100644 --- a/actions/setup/js/set_issue_type.cjs +++ b/actions/setup/js/set_issue_type.cjs @@ -76,16 +76,37 @@ async function fetchIssueTypes(githubClient, owner, repo) { * @returns {Promise} */ async function setIssueTypeById(githubClient, issueNodeId, typeId, confidence) { - await githubClient.graphql( - `mutation($issueId: ID!, $typeId: ID, $confidence: Int) { - updateIssue(input: { id: $issueId, issueTypeId: $typeId, confidence: $confidence }) { - issue { - id + try { + await githubClient.graphql( + `mutation($issueId: ID!, $typeId: ID, $confidence: Int) { + updateIssue(input: { id: $issueId, issueTypeId: $typeId, confidence: $confidence }) { + issue { + id + } } - } - }`, - { issueId: issueNodeId, typeId, confidence } - ); + }`, + { issueId: issueNodeId, typeId, confidence } + ); + } catch (error) { + const message = getErrorMessage(error).toLowerCase(); + const hasConfidence = typeof confidence === "number"; + const mentionsConfidence = message.includes("confidence"); + if (!hasConfidence || !mentionsConfidence) { + throw error; + } + + core.warning("Issue type confidence is not supported by this API; retrying without confidence."); + await githubClient.graphql( + `mutation($issueId: ID!, $typeId: ID) { + updateIssue(input: { id: $issueId, issueTypeId: $typeId }) { + issue { + id + } + } + }`, + { issueId: issueNodeId, typeId } + ); + } } /** diff --git a/actions/setup/js/update_issue.cjs b/actions/setup/js/update_issue.cjs index d13560387e3..0123ed4cec2 100644 --- a/actions/setup/js/update_issue.cjs +++ b/actions/setup/js/update_issue.cjs @@ -103,12 +103,29 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) { } } - const { data: issue } = await github.rest.issues.update({ + const issueUpdateParams = { owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, ...apiData, - }); + }; + + let issue; + try { + ({ data: issue } = await github.rest.issues.update(issueUpdateParams)); + } catch (error) { + const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); + const hasConfidence = typeof apiData.confidence === "number"; + const mentionsConfidence = message.includes("confidence"); + if (!hasConfidence || !mentionsConfidence) { + throw error; + } + + core.warning("Issue confidence is not supported by this API; retrying without confidence."); + const retryParams = { ...issueUpdateParams }; + delete retryParams.confidence; + ({ data: issue } = await github.rest.issues.update(retryParams)); + } return issue; } diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 1898a77aded..63df821eed5 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -903,7 +903,7 @@ }, { "name": "update_issue", - "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. Include `confidence` (0-100) when possible to indicate certainty in the update. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", + "description": "Update an existing GitHub issue's title, body, labels, assignees, or milestone WITHOUT closing it. This tool is primarily for editing issue metadata and content. While it supports changing status between 'open' and 'closed', use close_issue instead when you want to close an issue with a closing comment. Body updates support replacing, appending to, prepending content, or updating a per-run \"island\" section. Include `confidence` (0-100) when the update is inferred from analysis or heuristics rather than explicit user instruction. IMPORTANT: The behavior of this tool depends on the workflow's `update-issue: target:` configuration. When `target: triggering` (the default), the tool always updates the issue that triggered the workflow and `issue_number` is ignored. When `target: '*'`, the `issue_number` field controls which issue is updated. The tool will fail (not skip silently) when `target: triggering` and there is no triggering issue (e.g., in scheduled or workflow_dispatch workflows).", "inputSchema": { "type": "object", "properties": { @@ -1371,7 +1371,7 @@ }, { "name": "set_issue_type", - "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned. Include `confidence` (0-100) when possible to indicate certainty in the selected type.", + "description": "Set the type of a GitHub issue. Pass an empty string \"\" to clear the issue type. Issue types must be configured in the repository or organization settings before they can be assigned. Include `confidence` (0-100) when the selected type is inferred from issue signals rather than explicit user instruction.", "inputSchema": { "type": "object", "required": [ @@ -1409,7 +1409,7 @@ }, { "name": "set_issue_field", - "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name). Include `confidence` (0-100) when possible to indicate certainty in the field update.", + "description": "Set a single GitHub issue field by name and value. Use field_name for discovery by field label (for example, \"Priority\"), or provide field_node_id to skip discovery. Supports text, number, date (YYYY-MM-DD), and single-select fields (value must match an option name). Include `confidence` (0-100) when the field value is inferred rather than explicitly requested.", "inputSchema": { "type": "object", "required": [