Skip to content

Commit a5e4ba1

Browse files
authored
Extract shared close-older marker search/filter logic into helpers module (#26023)
1 parent a22ff15 commit a5e4ba1

4 files changed

Lines changed: 590 additions & 153 deletions

File tree

actions/setup/js/close_older_discussions.cjs

Lines changed: 37 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
const { getCloseOlderDiscussionMessage } = require("./messages_close_discussion.cjs");
55
const { getErrorMessage } = require("./error_helpers.cjs");
6-
const { getWorkflowIdMarkerContent, generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, getCloseKeyMarkerContent } = require("./generate_footer.cjs");
76
const { sanitizeContent } = require("./sanitize_content.cjs");
87
const { closeOlderEntities, MAX_CLOSE_COUNT: SHARED_MAX_CLOSE_COUNT } = require("./close_older_entities.cjs");
8+
const { buildMarkerSearchQuery, filterByMarker, logFilterSummary } = require("./close_older_search_helpers.cjs");
99

1010
/**
1111
* Maximum number of older discussions to close
@@ -44,27 +44,13 @@ async function searchOlderDiscussions(github, owner, repo, workflowId, categoryI
4444
return [];
4545
}
4646

47-
// Build GraphQL search query.
48-
// When a close-older-key is provided it becomes the primary search term; otherwise
49-
// fall back to the workflow-id marker.
50-
let searchQuery;
51-
let exactMarker;
52-
if (closeOlderKey) {
53-
const closeKeyMarkerContent = getCloseKeyMarkerContent(closeOlderKey);
54-
const escapedMarker = closeKeyMarkerContent.replace(/"/g, '\\"');
55-
searchQuery = `repo:${owner}/${repo} is:open "${escapedMarker}" in:body`;
56-
exactMarker = generateCloseKeyMarker(closeOlderKey);
57-
core.info(` Using close-older-key for search: "${escapedMarker}" in:body`);
58-
} else {
59-
// Build GraphQL search query
60-
// Search for open discussions with the workflow-id marker in the body
61-
const workflowIdMarker = getWorkflowIdMarkerContent(workflowId);
62-
// Escape quotes in workflow ID to prevent query injection
63-
const escapedMarker = workflowIdMarker.replace(/"/g, '\\"');
64-
searchQuery = `repo:${owner}/${repo} is:open "${escapedMarker}" in:body`;
65-
exactMarker = callerWorkflowId ? generateWorkflowCallIdMarker(callerWorkflowId) : generateWorkflowIdMarker(workflowId);
66-
core.info(` Added workflow ID marker filter to query: "${escapedMarker}" in:body`);
67-
}
47+
const { searchQuery, exactMarker } = buildMarkerSearchQuery({
48+
owner,
49+
repo,
50+
workflowId,
51+
callerWorkflowId,
52+
closeOlderKey,
53+
});
6854
core.info(`Executing GitHub search with query: ${searchQuery}`);
6955

7056
const result = await github.graphql(
@@ -96,73 +82,39 @@ async function searchOlderDiscussions(github, owner, repo, workflowId, categoryI
9682
return [];
9783
}
9884

99-
// Filter results:
100-
// 1. Must not be the excluded discussion (newly created one)
101-
// 2. Must not be already closed
102-
// 3. If categoryId is specified, must match
103-
// 4. Body must contain the exact marker. When closeOlderKey is set the close-key marker
104-
// is used. Otherwise, when callerWorkflowId is set, match `gh-aw-workflow-call-id` so
105-
// that callers sharing the same reusable workflow do not close each other's discussions.
106-
// Fall back to `gh-aw-workflow-id` for backward compat with older discussions.
10785
core.info("Filtering search results...");
108-
let filteredCount = 0;
109-
let excludedCount = 0;
110-
let closedCount = 0;
111-
let markerMismatchCount = 0;
11286

113-
const filtered = result.search.nodes
114-
.filter(
115-
/** @param {any} d */ d => {
116-
if (!d) {
117-
return false;
118-
}
119-
120-
// Exclude the newly created discussion
121-
if (d.number === excludeNumber) {
122-
excludedCount++;
123-
core.info(` Excluding discussion #${d.number} (the newly created discussion)`);
124-
return false;
125-
}
126-
127-
// Exclude already closed discussions
128-
if (d.closed) {
129-
closedCount++;
130-
return false;
131-
}
132-
133-
// Check category if specified
134-
if (categoryId && (!d.category || d.category.id !== categoryId)) {
135-
return false;
136-
}
137-
138-
// Exact-match the marker in the discussion body to prevent GitHub search
139-
// substring tokenization from matching related workflow IDs
140-
// (e.g. "foo" would otherwise match discussions from "foo-bar")
141-
if (!d.body?.includes(exactMarker)) {
142-
markerMismatchCount++;
143-
core.info(` Excluding discussion #${d.number} (body does not contain exact marker)`);
144-
return false;
145-
}
146-
147-
filteredCount++;
148-
core.info(` ✓ Discussion #${d.number} matches criteria: ${d.title}`);
149-
return true;
87+
const { filtered: filteredItems, counters } = filterByMarker({
88+
items: result.search.nodes,
89+
excludeNumber,
90+
exactMarker,
91+
entityType: "discussion",
92+
additionalFilter: (d, extra) => {
93+
if (d.closed) {
94+
extra.closedCount = (extra.closedCount || 0) + 1;
95+
return false;
96+
}
97+
if (categoryId && (!d.category || d.category.id !== categoryId)) {
98+
return false;
15099
}
151-
)
152-
.map(
153-
/** @param {any} d */ d => ({
154-
id: d.id,
155-
number: d.number,
156-
title: d.title,
157-
url: d.url,
158-
})
159-
);
100+
return true;
101+
},
102+
});
103+
104+
const filtered = filteredItems.map(
105+
/** @param {any} d */ d => ({
106+
id: d.id,
107+
number: d.number,
108+
title: d.title,
109+
url: d.url,
110+
})
111+
);
160112

161-
core.info(`Filtering complete:`);
162-
core.info(` - Matched discussions: ${filteredCount}`);
163-
core.info(` - Excluded new discussion: ${excludedCount}`);
164-
core.info(` - Excluded closed discussions: ${closedCount}`);
165-
core.info(` - Excluded marker mismatch: ${markerMismatchCount}`);
113+
logFilterSummary({
114+
entityTypePlural: "discussions",
115+
counters,
116+
extraLabels: [["closedCount", "Excluded closed discussions"]],
117+
});
166118

167119
return filtered;
168120
}

actions/setup/js/close_older_issues.cjs

Lines changed: 31 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// @ts-check
22
/// <reference types="@actions/github-script" />
33

4-
const { getWorkflowIdMarkerContent, generateWorkflowIdMarker, generateWorkflowCallIdMarker, generateCloseKeyMarker, getCloseKeyMarkerContent } = require("./generate_footer.cjs");
54
const { sanitizeContent } = require("./sanitize_content.cjs");
65
const { closeOlderEntities, MAX_CLOSE_COUNT: SHARED_MAX_CLOSE_COUNT } = require("./close_older_entities.cjs");
6+
const { buildMarkerSearchQuery, filterByMarker, logFilterSummary } = require("./close_older_search_helpers.cjs");
77

88
/**
99
* Maximum number of older issues to close
@@ -41,26 +41,14 @@ async function searchOlderIssues(github, owner, repo, workflowId, excludeNumber,
4141
return [];
4242
}
4343

44-
// Build REST API search query.
45-
// When a close-older-key is provided it becomes the primary search term; otherwise
46-
// fall back to the workflow-id marker.
47-
let searchQuery;
48-
let exactMarker;
49-
if (closeOlderKey) {
50-
const closeKeyMarkerContent = getCloseKeyMarkerContent(closeOlderKey);
51-
const escapedMarker = closeKeyMarkerContent.replace(/"/g, '\\"');
52-
searchQuery = `repo:${owner}/${repo} is:issue is:open "${escapedMarker}" in:body`;
53-
exactMarker = generateCloseKeyMarker(closeOlderKey);
54-
core.info(` Using close-older-key for search: "${escapedMarker}" in:body`);
55-
} else {
56-
// Search for open issues with the workflow-id marker in the body
57-
const workflowIdMarker = getWorkflowIdMarkerContent(workflowId);
58-
// Escape quotes in workflow ID to prevent query injection
59-
const escapedMarker = workflowIdMarker.replace(/"/g, '\\"');
60-
searchQuery = `repo:${owner}/${repo} is:issue is:open "${escapedMarker}" in:body`;
61-
exactMarker = callerWorkflowId ? generateWorkflowCallIdMarker(callerWorkflowId) : generateWorkflowIdMarker(workflowId);
62-
core.info(` Added workflow-id marker filter to query: "${escapedMarker}" in:body`);
63-
}
44+
const { searchQuery, exactMarker } = buildMarkerSearchQuery({
45+
owner,
46+
repo,
47+
workflowId,
48+
callerWorkflowId,
49+
closeOlderKey,
50+
entityQualifier: "is:issue",
51+
});
6452
core.info(`Executing GitHub search with query: ${searchQuery}`);
6553

6654
const result = await github.rest.search.issuesAndPullRequests({
@@ -75,60 +63,35 @@ async function searchOlderIssues(github, owner, repo, workflowId, excludeNumber,
7563
return [];
7664
}
7765

78-
// Filter results:
79-
// 1. Must not be the excluded issue (newly created one)
80-
// 2. Must not be a pull request
81-
// 3. Body must contain the exact marker. When closeOlderKey is set the close-key marker
82-
// is used. Otherwise, when callerWorkflowId is set, match `gh-aw-workflow-call-id` so
83-
// that callers sharing the same reusable workflow do not close each other's issues.
84-
// Fall back to `gh-aw-workflow-id` for backward compat with older issues.
8566
core.info("Filtering search results...");
86-
let filteredCount = 0;
87-
let pullRequestCount = 0;
88-
let excludedCount = 0;
89-
let markerMismatchCount = 0;
9067

91-
const filtered = result.data.items
92-
.filter(item => {
93-
// Exclude pull requests
68+
const { filtered: filteredItems, counters } = filterByMarker({
69+
items: result.data.items,
70+
excludeNumber,
71+
exactMarker,
72+
entityType: "issue",
73+
additionalFilter: (item, extra) => {
9474
if (item.pull_request) {
95-
pullRequestCount++;
96-
return false;
97-
}
98-
99-
// Exclude the newly created issue
100-
if (item.number === excludeNumber) {
101-
excludedCount++;
102-
core.info(` Excluding issue #${item.number} (the newly created issue)`);
103-
return false;
104-
}
105-
106-
// Exact-match the marker in the issue body to prevent GitHub search
107-
// substring tokenization from matching related workflow IDs
108-
// (e.g. "foo" would otherwise match issues from "foo-bar")
109-
if (!item.body?.includes(exactMarker)) {
110-
markerMismatchCount++;
111-
core.info(` Excluding issue #${item.number} (body does not contain exact marker)`);
75+
extra.pullRequestCount = (extra.pullRequestCount || 0) + 1;
11276
return false;
11377
}
114-
115-
filteredCount++;
116-
core.info(` ✓ Issue #${item.number} matches criteria: ${item.title}`);
11778
return true;
118-
})
119-
.map(item => ({
120-
number: item.number,
121-
title: item.title,
122-
html_url: item.html_url,
123-
labels: item.labels || [],
124-
created_at: item.created_at,
125-
}));
79+
},
80+
});
12681

127-
core.info(`Filtering complete:`);
128-
core.info(` - Matched issues: ${filteredCount}`);
129-
core.info(` - Excluded pull requests: ${pullRequestCount}`);
130-
core.info(` - Excluded new issue: ${excludedCount}`);
131-
core.info(` - Excluded marker mismatch: ${markerMismatchCount}`);
82+
const filtered = filteredItems.map(item => ({
83+
number: item.number,
84+
title: item.title,
85+
html_url: item.html_url,
86+
labels: item.labels || [],
87+
created_at: item.created_at,
88+
}));
89+
90+
logFilterSummary({
91+
entityTypePlural: "issues",
92+
counters,
93+
extraLabels: [["pullRequestCount", "Excluded pull requests"]],
94+
});
13295

13396
return filtered;
13497
}

0 commit comments

Comments
 (0)