Skip to content

Commit a984013

Browse files
authored
safe-outputs: Apply labels when creating discussions (#15597)
1 parent e835351 commit a984013

4 files changed

Lines changed: 594 additions & 5 deletions

File tree

.github/workflows/smoke-copilot.lock.yml

Lines changed: 57 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-copilot.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ safe-outputs:
5454
expires: 2h
5555
group: true
5656
close-older-issues: true
57+
create-discussion:
58+
category: announcements
59+
labels: [ai-generated]
60+
expires: 1d
61+
close-older-discussions: true
62+
max: 1
5763
create-pull-request-review-comment:
5864
max: 5
5965
submit-pull-request-review:
@@ -123,8 +129,9 @@ strict: true
123129
- Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123)
124130
- Use the `add_comment` tool with `discussion_number: <extracted_number>` to add a fun, playful comment stating that the smoke test agent was here
125131
8. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project (both caches must be set to /tmp because the default cache locations are not writable). If the command fails, mark this test as ❌ and report the failure.
126-
9. **Workflow Dispatch Testing**: Use the `dispatch_workflow` safe output tool to trigger the `haiku-printer` workflow with a haiku as the message input. Create an original, creative haiku about software testing or automation.
127-
10. **PR Review Testing**: Review the diff of the current pull request. Leave 1-2 inline `create_pull_request_review_comment` comments on specific lines, then call `submit_pull_request_review` with a brief body summarizing your review and event `COMMENT`.
132+
9. **Discussion Creation Testing**: Use the `create_discussion` safe-output tool to create a discussion in the announcements category titled "copilot was here" with the label "ai-generated"
133+
10. **Workflow Dispatch Testing**: Use the `dispatch_workflow` safe output tool to trigger the `haiku-printer` workflow with a haiku as the message input. Create an original, creative haiku about software testing or automation.
134+
11. **PR Review Testing**: Review the diff of the current pull request. Leave 1-2 inline `create_pull_request_review_comment` comments on specific lines, then call `submit_pull_request_review` with a brief body summarizing your review and event `COMMENT`.
128135

129136
## Output
130137

actions/setup/js/create_discussion.cjs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_titl
1616
const { getErrorMessage } = require("./error_helpers.cjs");
1717
const { createExpirationLine, generateFooterWithExpiration } = require("./ephemerals.cjs");
1818
const { generateWorkflowIdMarker } = require("./generate_footer.cjs");
19+
const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
1920

2021
/**
2122
* Fetch repository ID and discussion categories for a repository
@@ -111,6 +112,113 @@ function resolveCategoryId(categoryConfig, itemCategory, categories) {
111112
return undefined;
112113
}
113114

115+
/**
116+
* Fetches label node IDs for the given label names
117+
* @param {string} owner - Repository owner
118+
* @param {string} repo - Repository name
119+
* @param {string[]} labelNames - Array of label names to fetch IDs for
120+
* @returns {Promise<Array<{name: string, id: string}>>} Array of label objects with name and ID
121+
*/
122+
async function fetchLabelIds(owner, repo, labelNames) {
123+
if (!labelNames || labelNames.length === 0) {
124+
return [];
125+
}
126+
127+
try {
128+
// Fetch first 100 labels from the repository
129+
const labelsQuery = `
130+
query($owner: String!, $repo: String!) {
131+
repository(owner: $owner, name: $repo) {
132+
labels(first: 100) {
133+
nodes {
134+
id
135+
name
136+
}
137+
}
138+
}
139+
}
140+
`;
141+
142+
const queryResult = await github.graphql(labelsQuery, {
143+
owner: owner,
144+
repo: repo,
145+
});
146+
147+
const repoLabels = queryResult?.repository?.labels?.nodes || [];
148+
const labelMap = new Map(repoLabels.map(label => [label.name.toLowerCase(), label]));
149+
150+
// Match requested labels (case-insensitive)
151+
const matchedLabels = [];
152+
const unmatchedLabels = [];
153+
154+
for (const requestedLabel of labelNames) {
155+
const normalizedName = requestedLabel.toLowerCase();
156+
const matchedLabel = labelMap.get(normalizedName);
157+
if (matchedLabel) {
158+
matchedLabels.push({ name: matchedLabel.name, id: matchedLabel.id });
159+
} else {
160+
unmatchedLabels.push(requestedLabel);
161+
}
162+
}
163+
164+
if (unmatchedLabels.length > 0) {
165+
core.warning(`Could not find label IDs for: ${unmatchedLabels.join(", ")}`);
166+
core.info(`These labels may not exist in the repository. Available labels: ${repoLabels.map(l => l.name).join(", ")}`);
167+
}
168+
169+
return matchedLabels;
170+
} catch (error) {
171+
core.warning(`Failed to fetch label IDs: ${getErrorMessage(error)}`);
172+
return [];
173+
}
174+
}
175+
176+
/**
177+
* Applies labels to a discussion using GraphQL
178+
* @param {string} discussionId - Discussion node ID
179+
* @param {string[]} labelIds - Array of label node IDs to add
180+
* @returns {Promise<boolean>} True if labels were applied successfully
181+
*/
182+
async function applyLabelsToDiscussion(discussionId, labelIds) {
183+
if (!labelIds || labelIds.length === 0) {
184+
return true; // Nothing to do
185+
}
186+
187+
try {
188+
const addLabelsMutation = `
189+
mutation($labelableId: ID!, $labelIds: [ID!]!) {
190+
addLabelsToLabelable(input: {
191+
labelableId: $labelableId,
192+
labelIds: $labelIds
193+
}) {
194+
labelable {
195+
... on Discussion {
196+
id
197+
labels(first: 10) {
198+
nodes {
199+
name
200+
}
201+
}
202+
}
203+
}
204+
}
205+
}
206+
`;
207+
208+
const mutationResult = await github.graphql(addLabelsMutation, {
209+
labelableId: discussionId,
210+
labelIds: labelIds,
211+
});
212+
213+
const appliedLabels = mutationResult?.addLabelsToLabelable?.labelable?.labels?.nodes || [];
214+
core.info(`Successfully applied ${appliedLabels.length} labels to discussion`);
215+
return true;
216+
} catch (error) {
217+
core.warning(`Failed to apply labels to discussion: ${getErrorMessage(error)}`);
218+
return false;
219+
}
220+
}
221+
114222
/**
115223
* Checks if an error is a permissions-related error
116224
* @param {string} errorMessage - The error message to check
@@ -349,6 +457,16 @@ async function main(config = {}) {
349457
const categoryId = resolvedCategory.id;
350458
core.info(`Using category: ${resolvedCategory.name} (${resolvedCategory.matchType})`);
351459

460+
// Build labels array (merge config labels with item-specific labels)
461+
const discussionLabels = [...labels, ...(Array.isArray(item.labels) ? item.labels : [])]
462+
.filter(Boolean)
463+
.map(label => String(label).trim())
464+
.filter(Boolean)
465+
.map(label => sanitizeLabelContent(label))
466+
.filter(Boolean)
467+
.map(label => (label.length > 64 ? label.substring(0, 64) : label))
468+
.filter((label, index, arr) => arr.indexOf(label) === index);
469+
352470
// Build title
353471
let title = item.title ? item.title.trim() : "";
354472
let processedBody = replaceTemporaryIdReferences(item.body || "", temporaryIdMap, qualifiedItemRepo);
@@ -439,6 +557,21 @@ async function main(config = {}) {
439557

440558
core.info(`Created discussion ${qualifiedItemRepo}#${discussion.number}: ${discussion.url}`);
441559

560+
// Apply labels if configured
561+
if (discussionLabels.length > 0) {
562+
core.info(`Applying ${discussionLabels.length} labels to discussion: ${discussionLabels.join(", ")}`);
563+
const labelIdsData = await fetchLabelIds(repoParts.owner, repoParts.repo, discussionLabels);
564+
if (labelIdsData.length > 0) {
565+
const labelIds = labelIdsData.map(l => l.id);
566+
const labelsApplied = await applyLabelsToDiscussion(discussion.id, labelIds);
567+
if (labelsApplied) {
568+
core.info(`✓ Applied labels: ${labelIdsData.map(l => l.name).join(", ")}`);
569+
}
570+
} else if (discussionLabels.length > 0) {
571+
core.warning(`⚠ No matching labels found in repository for: ${discussionLabels.join(", ")}`);
572+
}
573+
}
574+
442575
return {
443576
success: true,
444577
repo: qualifiedItemRepo,

0 commit comments

Comments
 (0)