diff --git a/.changeset/patch-add-set-issue-type-safe-output.md b/.changeset/patch-add-set-issue-type-safe-output.md
new file mode 100644
index 0000000000..946ae0f04b
--- /dev/null
+++ b/.changeset/patch-add-set-issue-type-safe-output.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": patch
+---
+
+Added a `set-issue-type` safe output type so workflows can set or clear GitHub issue types with optional allowlists.
diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml
index eafde05678..d73a795300 100644
--- a/.github/workflows/smoke-copilot.lock.yml
+++ b/.github/workflows/smoke-copilot.lock.yml
@@ -29,7 +29,7 @@
# - shared/github-queries-safe-input.md
# - shared/reporting.md
#
-# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"2db010ebae2dafeeecf613a4430a7b8159d150f79ab54eeddc32fce6b932ac3f"}
+# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"4627e6a885992491762dac46a6ad592a73d411faecda2b2d02be4895fa36c1dc"}
name: "Smoke Copilot"
"on":
@@ -171,7 +171,7 @@ jobs:
cat "/opt/gh-aw/prompts/safe_outputs_prompt.md"
cat << 'GH_AW_PROMPT_EOF'
- Tools: add_comment, create_issue, create_discussion, create_pull_request_review_comment, submit_pull_request_review, add_labels, remove_labels, dispatch_workflow, missing_tool, missing_data, noop
+ Tools: add_comment, create_issue, create_discussion, create_pull_request_review_comment, submit_pull_request_review, add_labels, remove_labels, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop
The following GitHub context information is available for this workflow:
@@ -456,7 +456,7 @@ jobs:
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
- {"add_comment":{"allowed_repos":["github/gh-aw"],"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"],"max":3},"create_discussion":{"expires":2,"max":1},"create_issue":{"expires":2,"group":true,"max":1},"create_pull_request_review_comment":{"max":5},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"default":null,"description":"The message to send","required":true,"type":"string"}},"output":"Slack message stub executed!"},"submit_pull_request_review":{"max":1}}
+ {"add_comment":{"allowed_repos":["github/gh-aw"],"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"],"max":3},"create_discussion":{"expires":2,"max":1},"create_issue":{"expires":2,"group":true,"max":1},"create_pull_request_review_comment":{"max":5},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"default":null,"description":"The message to send","required":true,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{"max":5},"submit_pull_request_review":{"max":1}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF'
[
@@ -711,6 +711,30 @@ jobs:
},
"name": "noop"
},
+ {
+ "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.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "issue_number": {
+ "description": "Issue number to set the type for. If omitted, sets the type on the issue that triggered this workflow.",
+ "type": [
+ "number",
+ "string"
+ ]
+ },
+ "issue_type": {
+ "description": "Issue type name to set (e.g., \"Bug\", \"Feature\", \"Task\"). Use an empty string \"\" to clear the current issue type.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "issue_type"
+ ],
+ "type": "object"
+ },
+ "name": "set_issue_type"
+ },
{
"description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.",
"inputSchema": {
@@ -983,6 +1007,24 @@ jobs:
}
}
},
+ "set_issue_type": {
+ "defaultMax": 5,
+ "fields": {
+ "issue_number": {
+ "issueOrPRNumber": true
+ },
+ "issue_type": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
"submit_pull_request_review": {
"defaultMax": 1,
"fields": {
@@ -2170,7 +2212,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,github.githubassets.com,go.dev,golang.org,googleapis.deno.dev,googlechromelabs.github.io,goproxy.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkg.go.dev,playwright.download.prss.microsoft.com,ppa.launchpad.net,proxy.golang.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,sum.golang.org,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
- GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"submit_pull_request_review\":{\"max\":1}}"
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md
index cafac347b6..368a2e4f40 100644
--- a/.github/workflows/smoke-copilot.md
+++ b/.github/workflows/smoke-copilot.md
@@ -70,6 +70,7 @@ safe-outputs:
allowed-repos: ["github/gh-aw"]
remove-labels:
allowed: [smoke]
+ set-issue-type:
dispatch-workflow:
workflows:
- haiku-printer
@@ -141,6 +142,7 @@ features:
## Output
1. **Create an issue** with a summary of the smoke test run:
+ - Use a temporary ID (e.g. `aw_smoke1`) for the issue so you can reference it later
- Title: "Smoke Test: Copilot - ${{ github.run_id }}"
- Body should include:
- Test results (✅ or ❌ for each test)
@@ -149,15 +151,17 @@ features:
- Timestamp
- Pull request author and assignees
-2. **Only if this workflow was triggered by a pull_request event**: Use the `add_comment` tool to add a **very brief** comment (max 5-10 lines) to the triggering pull request (omit the `item_number` parameter to auto-target the triggering PR) with:
+2. **Set Issue Type**: Use the `set_issue_type` safe-output tool with `issue_number: "aw_smoke1"` (the temporary ID from step 1) and `issue_type: "Bug"` to set the type of the just-created smoke test issue.
+
+3. **Only if this workflow was triggered by a pull_request event**: Use the `add_comment` tool to add a **very brief** comment (max 5-10 lines) to the triggering pull request (omit the `item_number` parameter to auto-target the triggering PR) with:
- PR titles only (no descriptions)
- ✅ or ❌ for each test result
- Overall status: PASS or FAIL
- Mention the pull request author and any assignees
-3. Use the `add_comment` tool to add a **fun and creative comment** to the latest discussion (using the `discussion_number` you extracted in step 8) - be playful and entertaining in your comment
+4. Use the `add_comment` tool to add a **fun and creative comment** to the latest discussion (using the `discussion_number` you extracted in step 8) - be playful and entertaining in your comment
-4. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅")
+5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅")
If all tests pass and this workflow was triggered by a pull_request event:
- Use the `add_labels` safe-output tool to add the label `smoke-copilot` to the pull request (omit the `item_number` parameter to auto-target the triggering PR)
diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs
index 5f7a2845f7..3a20d3a6f8 100644
--- a/actions/setup/js/safe_output_handler_manager.cjs
+++ b/actions/setup/js/safe_output_handler_manager.cjs
@@ -47,6 +47,7 @@ const HANDLER_MAP = {
close_pull_request: "./close_pull_request.cjs",
mark_pull_request_as_ready_for_review: "./mark_pull_request_as_ready_for_review.cjs",
hide_comment: "./hide_comment.cjs",
+ set_issue_type: "./set_issue_type.cjs",
add_reviewer: "./add_reviewer.cjs",
assign_milestone: "./assign_milestone.cjs",
assign_to_user: "./assign_to_user.cjs",
diff --git a/actions/setup/js/set_issue_type.cjs b/actions/setup/js/set_issue_type.cjs
new file mode 100644
index 0000000000..c9626ff0b1
--- /dev/null
+++ b/actions/setup/js/set_issue_type.cjs
@@ -0,0 +1,265 @@
+// @ts-check
+///
+
+/**
+ * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
+ */
+
+const { getErrorMessage } = require("./error_helpers.cjs");
+const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs");
+const { logStagedPreviewInfo } = require("./staged_preview.cjs");
+const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
+const { loadTemporaryIdMapFromResolved, resolveRepoIssueTarget } = require("./temporary_id.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} 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>} 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}
+ */
+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 { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);
+ 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(", ")}`);
+ }
+ core.info(`Default target repo: ${defaultTargetRepo}`);
+ if (allowedRepos.size > 0) {
+ core.info(`Allowed repos: ${Array.from(allowedRepos).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