Visitar URL original
chore: sync actions from gh-aw@v0.86.0 (#210) · github/gh-aw-actions@19356ac · GitHub
Skip to content

Commit 19356ac

Browse files
chore: sync actions from gh-aw@v0.86.0 (#210)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 2709137 commit 19356ac

33 files changed

Lines changed: 1252 additions & 546 deletions

‎setup/js/add_labels.cjs‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ const main = createCountGatedHandler({
278278
throw new Error(`Failed to resolve GraphQL node ID for ${contextType} #${itemNumber}`);
279279
}
280280

281+
// Detect whether the item is a pull request. The REST issues endpoint returns a
282+
// `pull_request` field for PRs, and PR node IDs start with "PR_". The GraphQL
283+
// updateIssue mutation only accepts Issue node IDs; PRs must use updatePullRequest.
284+
const itemIsPR = Boolean(issueData?.pull_request) || issueNodeId.startsWith("PR_");
285+
281286
const repoLabels = await fetchAllRepoLabels(githubClient, repoParts.owner, repoParts.repo);
282287
const labelIdByName = new Map(repoLabels.map(label => [label.name.toLowerCase(), label.id]));
283288

@@ -290,9 +295,27 @@ const main = createCountGatedHandler({
290295
const labelIntentUpdates = buildIssueIntentLabelUpdates(mergedSpecs, labelIdByName);
291296

292297
core.info(`Adding ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo} via GraphQL intent mutation`);
293-
const result = await withRetry(
294-
() =>
295-
githubClient.graphql(
298+
// Both updateIssue and updatePullRequest use LabelUpdateInput (rationale/confidence/suggest),
299+
// which is gated by the "update_issue_suggestions" GraphQL feature flag.
300+
const intentHeaders = { "GraphQL-Features": "update_issue_suggestions" };
301+
const [mutationQuery, mutationVars, getResultLabels] = itemIsPR
302+
? [
303+
`mutation($prId: ID!, $labels: [LabelUpdateInput!]!) {
304+
updatePullRequest(input: { pullRequestId: $prId, labels: $labels }) {
305+
pullRequest {
306+
id
307+
labels(first: 100) {
308+
nodes {
309+
name
310+
}
311+
}
312+
}
313+
}
314+
}`,
315+
{ prId: issueNodeId, labels: labelIntentUpdates, headers: intentHeaders },
316+
r => r?.updatePullRequest?.pullRequest?.labels?.nodes,
317+
]
318+
: [
296319
`mutation($issueId: ID!, $labels: [LabelUpdateInput!]!) {
297320
updateIssue(input: { id: $issueId, labels: $labels }) {
298321
issue {
@@ -305,14 +328,13 @@ const main = createCountGatedHandler({
305328
}
306329
}
307330
}`,
308-
{ issueId: issueNodeId, labels: labelIntentUpdates, headers: { "GraphQL-Features": "update_issue_suggestions" } }
309-
),
310-
RATE_LIMIT_RETRY_CONFIG,
311-
`add_labels to ${contextType} #${itemNumber} in ${itemRepo}`
312-
);
331+
{ issueId: issueNodeId, labels: labelIntentUpdates, headers: intentHeaders },
332+
r => r?.updateIssue?.issue?.labels?.nodes,
333+
];
334+
const result = await withRetry(() => githubClient.graphql(mutationQuery, mutationVars), RATE_LIMIT_RETRY_CONFIG, `add_labels to ${contextType} #${itemNumber} in ${itemRepo}`);
313335

314336
core.info(`Successfully added ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo}`);
315-
const afterLabels = result?.updateIssue?.issue?.labels?.nodes || [];
337+
const afterLabels = getResultLabels(result) || [];
316338
return attachExecutionState(
317339
{
318340
success: true,

‎setup/js/artifact_client.cjs‎

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -368,14 +368,23 @@ class DefaultArtifactClient {
368368
const zipLike = isZipResponse(location, contentType);
369369
if (zipLike && !options.skipDecompress) {
370370
ensureUnzipAvailable();
371-
const tempZip = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-download-")), "artifact.zip");
372-
digest = await streamToFile(blobResponse, tempZip);
373-
const unzipResult = spawnSync("unzip", ["-q", tempZip, "-d", destination], { encoding: "utf8" });
374-
if (unzipResult.error) {
375-
throw unzipResult.error;
376-
}
377-
if (unzipResult.status !== 0) {
378-
throw new Error(`unzip failed: ${unzipResult.stderr || unzipResult.stdout || "unknown error"}`);
371+
const tempDownloadDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-download-"));
372+
const tempZip = path.join(tempDownloadDir, "artifact.zip");
373+
try {
374+
digest = await streamToFile(blobResponse, tempZip);
375+
const unzipResult = spawnSync("unzip", ["-q", tempZip, "-d", destination], { encoding: "utf8" });
376+
if (unzipResult.error) {
377+
throw unzipResult.error;
378+
}
379+
if (unzipResult.status !== 0) {
380+
throw new Error(`unzip failed: ${unzipResult.stderr || unzipResult.stdout || "unknown error"}`);
381+
}
382+
} finally {
383+
try {
384+
fs.rmSync(tempDownloadDir, { recursive: true, force: true });
385+
} catch {
386+
// Ignore cleanup errors — best effort only.
387+
}
379388
}
380389
} else {
381390
const fileName = parseFilenameFromContentDisposition(blobResponse.headers.get("content-disposition") || "");
@@ -398,6 +407,7 @@ class DefaultArtifactClient {
398407
let artifactName = String(name || "").trim();
399408
let uploadPath = "";
400409
let contentType = "application/zip";
410+
let tmpDir = "";
401411

402412
if (options.skipArchive) {
403413
if (files.length !== 1) {
@@ -406,52 +416,62 @@ class DefaultArtifactClient {
406416
uploadPath = files[0];
407417
contentType = "application/octet-stream";
408418
} else {
409-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-upload-"));
419+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-upload-"));
410420
uploadPath = path.join(tmpDir, `${artifactName || "artifact"}.zip`);
411421
createZipFromFiles(files, rootDirectory, uploadPath);
412422
}
413423

414-
const { workflowRunBackendId, workflowJobRunBackendId } = getBackendIdsFromRuntimeToken();
415-
const createRequest = {
416-
workflowRunBackendId,
417-
workflowJobRunBackendId,
418-
name: artifactName,
419-
version: 7,
420-
mimeType: contentType,
421-
};
422-
const expiresAt = formatRetentionTimestamp(options.retentionDays);
423-
if (expiresAt) {
424-
createRequest.expiresAt = expiresAt;
425-
}
424+
try {
425+
const { workflowRunBackendId, workflowJobRunBackendId } = getBackendIdsFromRuntimeToken();
426+
const createRequest = {
427+
workflowRunBackendId,
428+
workflowJobRunBackendId,
429+
name: artifactName,
430+
version: 7,
431+
mimeType: contentType,
432+
};
433+
const expiresAt = formatRetentionTimestamp(options.retentionDays);
434+
if (expiresAt) {
435+
createRequest.expiresAt = expiresAt;
436+
}
426437

427-
/** @type {any} */
428-
const createResponse = await twirpRequest("CreateArtifact", createRequest);
429-
const signedUploadUrl = createResponse?.signedUploadUrl || createResponse?.signed_upload_url;
430-
if (!createResponse?.ok || !signedUploadUrl) {
431-
throw new Error("CreateArtifact returned an invalid response");
432-
}
438+
/** @type {any} */
439+
const createResponse = await twirpRequest("CreateArtifact", createRequest);
440+
const signedUploadUrl = createResponse?.signedUploadUrl || createResponse?.signed_upload_url;
441+
if (!createResponse?.ok || !signedUploadUrl) {
442+
throw new Error("CreateArtifact returned an invalid response");
443+
}
433444

434-
const uploadSize = await uploadFileToSignedURL(uploadPath, signedUploadUrl, contentType);
435-
const sha256 = await hashFile(uploadPath);
445+
const uploadSize = await uploadFileToSignedURL(uploadPath, signedUploadUrl, contentType);
446+
const sha256 = await hashFile(uploadPath);
436447

437-
const finalizeRequest = {
438-
workflowRunBackendId,
439-
workflowJobRunBackendId,
440-
name: artifactName,
441-
size: String(uploadSize),
442-
hash: `sha256:${sha256}`,
443-
};
444-
/** @type {any} */
445-
const finalizeResponse = await twirpRequest("FinalizeArtifact", finalizeRequest);
446-
if (!finalizeResponse?.ok) {
447-
throw new Error("FinalizeArtifact returned an invalid response");
448-
}
448+
const finalizeRequest = {
449+
workflowRunBackendId,
450+
workflowJobRunBackendId,
451+
name: artifactName,
452+
size: String(uploadSize),
453+
hash: `sha256:${sha256}`,
454+
};
455+
/** @type {any} */
456+
const finalizeResponse = await twirpRequest("FinalizeArtifact", finalizeRequest);
457+
if (!finalizeResponse?.ok) {
458+
throw new Error("FinalizeArtifact returned an invalid response");
459+
}
449460

450-
return {
451-
id: Number(finalizeResponse.artifactId ?? finalizeResponse.artifact_id ?? 0) || undefined,
452-
size: uploadSize,
453-
digest: sha256,
454-
};
461+
return {
462+
id: Number(finalizeResponse.artifactId ?? finalizeResponse.artifact_id ?? 0) || undefined,
463+
size: uploadSize,
464+
digest: sha256,
465+
};
466+
} finally {
467+
if (tmpDir) {
468+
try {
469+
fs.rmSync(tmpDir, { recursive: true, force: true });
470+
} catch {
471+
// Ignore cleanup errors — best effort only.
472+
}
473+
}
474+
}
455475
}
456476
}
457477

‎setup/js/check_command_position.cjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ async function main() {
117117
return;
118118
}
119119

120-
// Normalize whitespace and resolve the matched slash command at the start of the text.
121-
const trimmedText = text.trim();
122-
const matchedCommand = resolveMatchedCommand(trimmedText, commands);
123-
const firstWord = trimmedText.split(/\s+/)[0];
120+
// Resolve the matched slash command at the start of the text.
121+
// Commands must appear at position zero to match the compile-time activation conditions.
122+
const matchedCommand = resolveMatchedCommand(text, commands);
123+
const firstWord = text.trimStart().split(/\s+/)[0];
124124

125125
core.info(`Checking command position. First word in text: ${firstWord}`);
126126
core.info(`Looking for commands: ${commands.map(c => `/${c}`).join(", ")}`);

‎setup/js/close_expired_discussions.cjs‎

Lines changed: 37 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,10 @@
22
// <reference types="@actions/github-script" />
33

44
const { executeExpiredEntityCleanup } = require("./expired_entity_main_flow.cjs");
5-
const { generateExpiredEntityFooter, getExpiredEntityCautionAlert } = require("./generate_footer.cjs");
6-
const { formatDateInProjectTimeZone } = require("./project_timezone.cjs");
7-
const { sanitizeContent } = require("./sanitize_content.cjs");
5+
const { addDiscussionComment, closeDiscussionAsOutdated, createClosedRecord, createExpiredEntityHandler } = require("./expired_entity_handler_factory.cjs");
86
const { getWorkflowMetadata } = require("./workflow_metadata_helpers.cjs");
97
const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs");
108

11-
/**
12-
* Add comment to a GitHub Discussion using GraphQL
13-
* @param {any} github - GitHub GraphQL instance
14-
* @param {string} discussionId - Discussion node ID
15-
* @param {string} message - Comment body
16-
* @returns {Promise<{id: string, url: string}>} Comment details
17-
*/
18-
async function addDiscussionComment(github, discussionId, message) {
19-
const result = await github.graphql(
20-
`
21-
mutation($dId: ID!, $body: String!) {
22-
addDiscussionComment(input: { discussionId: $dId, body: $body }) {
23-
comment {
24-
id
25-
url
26-
}
27-
}
28-
}`,
29-
{ dId: discussionId, body: sanitizeContent(message) }
30-
);
31-
32-
return result.addDiscussionComment.comment;
33-
}
34-
35-
/**
36-
* Close a GitHub Discussion as OUTDATED using GraphQL
37-
* @param {any} github - GitHub GraphQL instance
38-
* @param {string} discussionId - Discussion node ID
39-
* @returns {Promise<{id: string, url: string}>} Discussion details
40-
*/
41-
async function closeDiscussionAsOutdated(github, discussionId) {
42-
const result = await github.graphql(
43-
`
44-
mutation($dId: ID!) {
45-
closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
46-
discussion {
47-
id
48-
url
49-
}
50-
}
51-
}`,
52-
{ dId: discussionId }
53-
);
54-
55-
return result.closeDiscussion.discussion;
56-
}
57-
589
/**
5910
* Check if a discussion already has an expiration comment and fetch its closed state
6011
* @param {any} github - GitHub GraphQL instance
@@ -106,60 +57,46 @@ async function main() {
10657
summaryHeading: "Expired Discussions Cleanup",
10758
enableDedupe: true, // Discussions may have duplicates across pages
10859
includeSkippedHeading: true,
109-
processEntity: async discussion => {
110-
core.info(` Checking for existing expiration comment and closed state on discussion #${discussion.number}`);
111-
const { hasComment, isClosed } = await hasExpirationComment(github, discussion.id);
112-
113-
if (isClosed) {
114-
core.warning(` Discussion #${discussion.number} is already closed, skipping`);
115-
return {
116-
status: "skipped",
117-
record: {
118-
number: discussion.number,
119-
url: discussion.url,
120-
title: discussion.title,
121-
},
122-
};
123-
}
124-
125-
if (hasComment) {
126-
core.warning(` Discussion #${discussion.number} already has an expiration comment, skipping to avoid duplicate`);
127-
128-
core.info(` Attempting to close discussion #${discussion.number} without adding another comment`);
129-
await closeDiscussionAsOutdated(github, discussion.id);
130-
core.info(` ✓ Discussion closed successfully`);
131-
132-
return {
133-
status: "skipped",
134-
record: {
135-
number: discussion.number,
136-
url: discussion.url,
137-
title: discussion.title,
138-
},
139-
};
140-
}
60+
processEntity: createExpiredEntityHandler({
61+
workflowName,
62+
workflowId,
63+
runUrl,
64+
entityNoun: "discussion",
65+
entityLabel: "Discussion",
66+
core,
67+
footerSuffix: "\n\n<!-- gh-aw-closed -->",
68+
beforeComment: async discussion => {
69+
core.info(` Checking for existing expiration comment and closed state on discussion #${discussion.number}`);
70+
const { hasComment, isClosed } = await hasExpirationComment(github, discussion.id);
71+
72+
if (isClosed) {
73+
core.warning(` Discussion #${discussion.number} is already closed, skipping`);
74+
return {
75+
status: "skipped",
76+
record: createClosedRecord(discussion),
77+
};
78+
}
14179

142-
const cautionAlert = getExpiredEntityCautionAlert(workflowName, runUrl);
143-
const expirationText = `This discussion was automatically closed because it expired on ${formatDateInProjectTimeZone(discussion.expirationDate) || discussion.expirationDate.toISOString()}.`;
144-
const closingMessage = (cautionAlert ? cautionAlert + "\n\n" : "") + expirationText + generateExpiredEntityFooter(workflowName, runUrl, workflowId) + "\n\n<!-- gh-aw-closed -->";
80+
if (hasComment) {
81+
core.warning(` Discussion #${discussion.number} already has an expiration comment, skipping to avoid duplicate`);
14582

146-
core.info(` Adding closing comment to discussion #${discussion.number}`);
147-
await addDiscussionComment(github, discussion.id, closingMessage);
148-
core.info(` ✓ Comment added successfully`);
83+
core.info(` Attempting to close discussion #${discussion.number} without adding another comment`);
84+
await closeDiscussionAsOutdated(github, discussion.id);
85+
core.info(` ✓ Discussion closed successfully`);
14986

150-
core.info(` Closing discussion #${discussion.number} as outdated`);
151-
await closeDiscussionAsOutdated(github, discussion.id);
152-
core.info(` ✓ Discussion closed successfully`);
87+
return {
88+
status: "closed",
89+
record: createClosedRecord(discussion),
90+
};
91+
}
15392

154-
return {
155-
status: "closed",
156-
record: {
157-
number: discussion.number,
158-
url: discussion.url,
159-
title: discussion.title,
160-
},
161-
};
162-
},
93+
return undefined;
94+
},
95+
beforeCommentLog: discussion => ` Adding closing comment to discussion #${discussion.number}`,
96+
beforeCloseLog: discussion => ` Closing discussion #${discussion.number} as outdated`,
97+
addComment: (discussion, message) => addDiscussionComment(github, discussion.id, message),
98+
closeEntity: discussion => closeDiscussionAsOutdated(github, discussion.id),
99+
}),
163100
});
164101
}
165102

0 commit comments

Comments
 (0)