Repository navigation
Add set-issue-type safe output type
#18989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
23ac7ee
Initial plan
Copilot fefbf9c
Add set-issue-type safe output type
Copilot e0f020e
Add set-issue-type to smoke-copilot and create test workflow
Copilot b676cb0
Add changeset [skip-ci]
54a075f
ci: trigger CI checks
github-actions[bot] 9a96ab2
Fix set_issue_type handler: add temporary ID support and repo targeting
Copilot 414e71d
Fix set_issue_type missing from prompt tools list; add compiled lock β¦
Copilot 3957d91
Fix set_issue_type missing from safe outputs config.json generation
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| // @ts-check | ||
| /// <reference types="@actions/github-script" /> | ||
|
|
||
| /** | ||
| * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction | ||
| */ | ||
|
|
||
| const { getErrorMessage } = require("./error_helpers.cjs"); | ||
| const { logStagedPreviewInfo } = require("./staged_preview.cjs"); | ||
| const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); | ||
|
|
||
| /** @type {string} Safe output type handled by this module */ | ||
| const HANDLER_TYPE = "set_issue_type"; | ||
|
|
||
| /** | ||
| * Fetches the node ID of an issue for use in GraphQL mutations. | ||
| * @param {Object} authClient - Authenticated GitHub client | ||
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @param {number} issueNumber - Issue number | ||
| * @returns {Promise<string>} Issue node ID | ||
| */ | ||
| async function getIssueNodeId(authClient, owner, repo, issueNumber) { | ||
| const { data } = await authClient.rest.issues.get({ | ||
| owner, | ||
| repo, | ||
| issue_number: issueNumber, | ||
| }); | ||
| return data.node_id; | ||
| } | ||
|
|
||
| /** | ||
| * Fetches the available issue types for the given repository via GraphQL. | ||
| * Returns an array of { id, name } objects, or an empty array if not supported. | ||
| * @param {Object} authClient - Authenticated GitHub client | ||
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @returns {Promise<Array<{id: string, name: string}>>} Available issue types | ||
| */ | ||
| async function fetchIssueTypes(authClient, owner, repo) { | ||
| try { | ||
| const result = await authClient.graphql( | ||
| `query($owner: String!, $repo: String!) { | ||
| repository(owner: $owner, name: $repo) { | ||
| issueTypes(first: 100) { | ||
| nodes { | ||
| id | ||
| name | ||
| } | ||
| } | ||
| } | ||
| }`, | ||
| { owner, repo } | ||
| ); | ||
| return result?.repository?.issueTypes?.nodes ?? []; | ||
| } catch (error) { | ||
| // Issue types may not be enabled for this repository/organization | ||
| // Log at debug level to aid debugging without being noisy | ||
| if (typeof core !== "undefined") { | ||
| core.debug(`Could not fetch issue types (may not be enabled): ${error instanceof Error ? error.message : String(error)}`); | ||
| } | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Sets the issue type via GraphQL mutation. | ||
| * Passing null for typeId clears the type. | ||
| * @param {Object} authClient - 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 | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async function setIssueTypeById(authClient, issueNodeId, typeId) { | ||
| await authClient.graphql( | ||
| `mutation($issueId: ID!, $typeId: ID) { | ||
| updateIssue(input: { id: $issueId, issueTypeId: $typeId }) { | ||
| issue { | ||
| id | ||
| } | ||
| } | ||
| }`, | ||
| { issueId: issueNodeId, typeId } | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Main handler factory for set_issue_type | ||
| * Returns a message handler function that processes individual set_issue_type messages | ||
| * @type {HandlerFactoryFunction} | ||
| */ | ||
| async function main(config = {}) { | ||
| // Extract configuration | ||
| const allowedTypes = config.allowed || []; | ||
| const maxCount = config.max || 5; | ||
| const authClient = await createAuthenticatedGitHubClient(config); | ||
|
|
||
| // Check if we're in staged mode | ||
| const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; | ||
|
|
||
| core.info(`Set issue type configuration: max=${maxCount}`); | ||
| if (allowedTypes.length > 0) { | ||
| core.info(`Allowed issue types: ${allowedTypes.join(", ")}`); | ||
| } | ||
|
|
||
| // Track how many items we've processed for max limit | ||
| let processedCount = 0; | ||
|
|
||
| /** | ||
| * Message handler function that processes a single set_issue_type message | ||
| * @param {Object} message - The set_issue_type message to process | ||
| * @param {Object} resolvedTemporaryIds - Map of temporary IDs to {repo, number} | ||
| * @returns {Promise<Object>} Result with success/error status | ||
| */ | ||
| return async function handleSetIssueType(message, resolvedTemporaryIds) { | ||
| // Check if we've hit the max limit | ||
| if (processedCount >= maxCount) { | ||
| core.warning(`Skipping set_issue_type: max count of ${maxCount} reached`); | ||
| return { | ||
| success: false, | ||
| error: `Max count of ${maxCount} reached`, | ||
| }; | ||
| } | ||
|
|
||
| processedCount++; | ||
|
|
||
| const item = message; | ||
|
|
||
| // Determine target issue number | ||
| const issueNumber = item.issue_number !== undefined ? parseInt(String(item.issue_number), 10) : (context.payload?.issue?.number ?? null); | ||
|
|
||
| if (!issueNumber || isNaN(issueNumber) || issueNumber <= 0) { | ||
| const error = item.issue_number !== undefined ? `Invalid issue_number: ${item.issue_number}` : "No issue number available"; | ||
| core.warning(error); | ||
| return { success: false, error }; | ||
| } | ||
|
|
||
| const issueTypeName = item.issue_type ?? ""; | ||
| const isClear = issueTypeName === ""; | ||
|
|
||
| core.info(`Setting issue type on issue #${issueNumber}: ${isClear ? "(clear)" : JSON.stringify(issueTypeName)}`); | ||
|
|
||
| // Validate against allowed list if configured (empty string always allowed to clear) | ||
| if (allowedTypes.length > 0 && !isClear) { | ||
| const normalizedAllowed = allowedTypes.map(t => t.toLowerCase()); | ||
| if (!normalizedAllowed.includes(issueTypeName.toLowerCase())) { | ||
| const error = `Issue type ${JSON.stringify(issueTypeName)} is not in the allowed list: ${JSON.stringify(allowedTypes)}`; | ||
| core.warning(error); | ||
| return { success: false, error }; | ||
| } | ||
| } | ||
|
|
||
| // If in staged mode, preview without executing | ||
| if (isStaged) { | ||
| const description = isClear ? `Would clear issue type on issue #${issueNumber}` : `Would set issue type to ${JSON.stringify(issueTypeName)} on issue #${issueNumber}`; | ||
| logStagedPreviewInfo(description); | ||
| return { | ||
| success: true, | ||
| staged: true, | ||
| previewInfo: { | ||
| issue_number: issueNumber, | ||
| issue_type: issueTypeName, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| const owner = context.repo.owner; | ||
| const repo = context.repo.repo; | ||
|
|
||
|
github-actions[bot] marked this conversation as resolved.
|
||
| // Get the issue's node ID for GraphQL | ||
| const issueNodeId = await getIssueNodeId(authClient, owner, repo, issueNumber); | ||
|
|
||
| let typeId = null; | ||
| if (!isClear) { | ||
| // Fetch available issue types and find the matching one | ||
| const issueTypes = await fetchIssueTypes(authClient, owner, repo); | ||
|
|
||
| if (issueTypes.length === 0) { | ||
| const error = "No issue types are available for this repository. Issue types must be configured in the repository or organization settings."; | ||
| core.error(error); | ||
| return { success: false, error }; | ||
| } | ||
|
|
||
| const matchedType = issueTypes.find(t => t.name.toLowerCase() === issueTypeName.toLowerCase()); | ||
| if (!matchedType) { | ||
| const availableNames = issueTypes.map(t => t.name).join(", "); | ||
| const error = `Issue type ${JSON.stringify(issueTypeName)} not found. Available types: ${availableNames}`; | ||
| core.error(error); | ||
| return { success: false, error }; | ||
| } | ||
|
|
||
| typeId = matchedType.id; | ||
| core.info(`Resolved issue type ${JSON.stringify(issueTypeName)} to node ID: ${typeId}`); | ||
| } | ||
|
|
||
| await setIssueTypeById(authClient, issueNodeId, typeId); | ||
|
|
||
| const successMsg = isClear ? `Successfully cleared issue type on issue #${issueNumber}` : `Successfully set issue type to ${JSON.stringify(issueTypeName)} on issue #${issueNumber}`; | ||
| core.info(successMsg); | ||
|
|
||
| return { | ||
| success: true, | ||
| issue_number: issueNumber, | ||
| issue_type: issueTypeName, | ||
| }; | ||
| } catch (error) { | ||
| const errorMessage = getErrorMessage(error); | ||
| core.error(`Failed to set issue type on issue #${issueNumber}: ${errorMessage}`); | ||
| return { success: false, error: errorMessage }; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { main }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.