diff --git a/actions/setup/js/create_discussion.cjs b/actions/setup/js/create_discussion.cjs index 0d6daf1424..c66173d0ab 100644 --- a/actions/setup/js/create_discussion.cjs +++ b/actions/setup/js/create_discussion.cjs @@ -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"); + + 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", @@ -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, diff --git a/actions/setup/js/create_discussion_category_normalization.test.cjs b/actions/setup/js/create_discussion_category_normalization.test.cjs index ed5e0abf30..b4d4010a86 100644 --- a/actions/setup/js/create_discussion_category_normalization.test.cjs +++ b/actions/setup/js/create_discussion_category_normalization.test.cjs @@ -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 + }); }); diff --git a/actions/setup/js/create_discussion_fallback.test.cjs b/actions/setup/js/create_discussion_fallback.test.cjs index f7793ab79c..38c0a456a9 100644 --- a/actions/setup/js/create_discussion_fallback.test.cjs +++ b/actions/setup/js/create_discussion_fallback.test.cjs @@ -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', diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index fae631480f..efcfea605d 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -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. + # + # Best Practice: Use announcement-capable categories (such as "announcements") + # for AI-generated content to ensure proper visibility and notification features. # (optional) category: null diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 67a595881a..46d7aa24ce 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -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) + 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