Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions actions/setup/js/create_discussion.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,21 @@ function resolveCategoryId(categoryConfig, itemCategory, categories) {
}
}

// Fall back to first category if available
// Fall back to announcement-capable category if available, otherwise first category
if (categories.length > 0) {
// Try to find an "Announcements" category (case-insensitive)
const announcementCategory = categories.find(cat => cat.name.toLowerCase() === "announcements" || cat.slug.toLowerCase() === "announcements");

Comment on lines +88 to +92
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this fallback path, requestedCategory comes from categoryToMatch, but categoryToMatch is never trim()’d before ID/name/slug comparisons. If an agent outputs category: "announcements " (trailing space), the match will fail and you’ll unexpectedly fall into this fallback logic. Consider normalizing category inputs with String(...).trim() before matching/fallback.

Copilot uses AI. Check for mistakes.
if (announcementCategory) {
return {
id: announcementCategory.id,
matchType: "fallback-announcement",
name: announcementCategory.name,
requestedCategory: categoryToMatch,
};
}

// Otherwise use first category
return {
id: categories[0].id,
matchType: "fallback",
Expand Down Expand Up @@ -127,7 +140,7 @@ function isPermissionsError(errorMessage) {
async function handleFallbackToIssue(createIssueHandler, item, qualifiedItemRepo, resolvedTemporaryIds, contextMessage) {
try {
// Prepare issue message with a note about the fallback
const fallbackNote = `\n\n---\n\n> **Note:** This was intended to be a discussion, but discussions could not be created due to permissions issues. This issue was created as a fallback.\n`;
const fallbackNote = `\n\n---\n\n> **Note:** This was intended to be a discussion, but discussions could not be created due to permissions issues. This issue was created as a fallback.\n>\n> **Tip:** Discussion creation may fail if the specified category is not announcement-capable. Consider using the "Announcements" category or another announcement-capable category in your workflow configuration.\n`;
const issueMessage = {
...item,
body: (item.body || "") + fallbackNote,
Expand Down
132 changes: 132 additions & 0 deletions actions/setup/js/create_discussion_category_normalization.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,136 @@ describe("create_discussion category normalization", () => {
expect(createMutationCall).toBeDefined();
expect(createMutationCall[1].categoryId).toBe("DIC_kwDOGFsHUM4BsUn1"); // General (first)
});

it("should prefer Announcements category when no category specified", async () => {
// Mock categories with Announcements available
mockGithub.graphql = vi.fn().mockImplementation((query, variables) => {
if (query.includes("discussionCategories")) {
return Promise.resolve({
repository: {
id: "R_test123",
discussionCategories: {
nodes: [
{
id: "DIC_kwDOGFsHUM4BsUn1",
name: "General",
slug: "general",
description: "General discussions",
},
{
id: "DIC_kwDOGFsHUM4BsUn4",
name: "Announcements",
slug: "announcements",
description: "Announcements",
},
{
id: "DIC_kwDOGFsHUM4BsUn2",
name: "Audits",
slug: "audits",
description: "Audit reports",
},
],
},
},
});
}
if (query.includes("createDiscussion")) {
return Promise.resolve({
createDiscussion: {
discussion: {
id: "D_test456",
number: 42,
title: variables.title,
url: "https://github.com/test-owner/test-repo/discussions/42",
},
},
});
}
return Promise.reject(new Error("Unknown GraphQL query"));
});

const handler = await createDiscussionMain({
max: 5,
// No category specified
});

const result = await handler(
{
title: "Test Discussion",
body: "This is a test discussion.",
},
{}
);

expect(result.success).toBe(true);
expect(result.number).toBe(42);

// Verify Announcements category was used (not General which is first)
const createMutationCall = mockGithub.graphql.mock.calls.find(call => call[0].includes("createDiscussion"));
expect(createMutationCall).toBeDefined();
expect(createMutationCall[1].categoryId).toBe("DIC_kwDOGFsHUM4BsUn4"); // Announcements
});

it("should prefer Announcements category when non-existent category specified", async () => {
// Mock categories with Announcements available
mockGithub.graphql = vi.fn().mockImplementation((query, variables) => {
if (query.includes("discussionCategories")) {
return Promise.resolve({
repository: {
id: "R_test123",
discussionCategories: {
nodes: [
{
id: "DIC_kwDOGFsHUM4BsUn1",
name: "General",
slug: "general",
description: "General discussions",
},
{
id: "DIC_kwDOGFsHUM4BsUn4",
name: "Announcements",
slug: "announcements",
description: "Announcements",
},
],
},
},
});
}
if (query.includes("createDiscussion")) {
return Promise.resolve({
createDiscussion: {
discussion: {
id: "D_test456",
number: 42,
title: variables.title,
url: "https://github.com/test-owner/test-repo/discussions/42",
},
},
});
}
return Promise.reject(new Error("Unknown GraphQL query"));
});

const handler = await createDiscussionMain({
max: 5,
category: "NonExistentCategory",
});

const result = await handler(
{
title: "Test Discussion",
body: "This is a test discussion.",
},
{}
);

expect(result.success).toBe(true);
expect(result.number).toBe(42);

// Verify Announcements category was used (not General which is first)
const createMutationCall = mockGithub.graphql.mock.calls.find(call => call[0].includes("createDiscussion"));
expect(createMutationCall).toBeDefined();
expect(createMutationCall[1].categoryId).toBe("DIC_kwDOGFsHUM4BsUn4"); // Announcements
});
});
6 changes: 6 additions & 0 deletions actions/setup/js/create_discussion_fallback.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ describe("create_discussion fallback with close_older_discussions", () => {
})
);

// Verify the fallback note includes the announcement category tip
const createCallArgs = mockGithub.rest.issues.create.mock.calls[0][0];
expect(createCallArgs.body).toContain("announcement-capable");
expect(createCallArgs.body).toContain("Announcements");
expect(createCallArgs.body).toContain("category");

// Verify search for older issues was performed
expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledWith({
q: 'repo:test-owner/test-repo is:issue is:open "gh-aw-workflow-id: test-workflow" in:body',
Expand Down
3 changes: 3 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,9 @@ safe-outputs:
# available category. Matched first against category IDs, then against category
# names, then against category slugs. Numeric values are automatically converted
# to strings at runtime.
#
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line # introduces trailing whitespace in the YAML comment block. Please change it to # (no trailing space) or remove the blank comment line to avoid whitespace/lint issues.

Suggested change
#
#

Copilot uses AI. Check for mistakes.
# Best Practice: Use announcement-capable categories (such as "announcements")
# for AI-generated content to ensure proper visibility and notification features.
Comment on lines +2195 to +2196
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section still says “If not specified, uses the first available category.” With the new behavior preferring the “Announcements” category when present, update the description above to match (e.g., prefer Announcements, else first available) so the frontmatter reference stays accurate.

See below for a potential fix:

    # category name, or category slug/route. If not specified, prefers an
    # announcement-capable category (such as "Announcements") when available;
    # otherwise uses the first available category. Matched first against category
    # IDs, then against category names, then against category slugs. Numeric values
    # are automatically converted to strings at runtime.
    # 
    # Best Practice: Use announcement-capable categories (such as "Announcements")

Copilot uses AI. Check for mistakes.
# (optional)
category: null

Expand Down
12 changes: 6 additions & 6 deletions docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,12 +853,12 @@ Creates discussions with optional `category` (slug, name, or ID; defaults to fir
```yaml wrap
safe-outputs:
create-discussion:
title-prefix: "[ai] " # prefix for titles
category: "general" # category slug, name, or ID (use lowercase)
expires: 3 # auto-close after 3 days (or false to disable)
max: 3 # max discussions (default: 1)
target-repo: "owner/repo" # cross-repository
fallback-to-issue: true # fallback to issue creation on permission errors (default: true)
title-prefix: "[ai] " # prefix for titles
category: "announcements" # category slug, name, or ID (use lowercase, prefer announcement-capable)
expires: 3 # auto-close after 3 days (or false to disable)
Comment on lines +856 to +858
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create-discussion docs still describe the category as “defaults to first available”, but the implementation now prefers the “Announcements” category when present. Please update the surrounding description to reflect the new fallback order so the example/comment doesn’t contradict actual behavior.

Copilot uses AI. Check for mistakes.
max: 3 # max discussions (default: 1)
target-repo: "owner/repo" # cross-repository
fallback-to-issue: true # fallback to issue creation on permission errors (default: true)
```

#### Fallback to Issue Creation
Expand Down
Loading