1616 required : false
1717 default : ' true'
1818 type : boolean
19+ scan_closed_issues :
20+ description : ' Scan closed issues (may find previously closed spam)'
21+ required : false
22+ default : ' false'
23+ type : boolean
1924 scan_comments :
2025 description : ' Scan issue comments'
2126 required : false
@@ -45,41 +50,46 @@ jobs:
4550 script : |
4651 const dryRun = '${{ inputs.dry_run }}' === 'true';
4752 const scanIssues = '${{ inputs.scan_issues }}' === 'true';
53+ const scanClosedIssues = '${{ inputs.scan_closed_issues }}' === 'true';
4854 const scanComments = '${{ inputs.scan_comments }}' === 'true';
4955 const maxIssues = parseInt('${{ inputs.max_issues }}') || 0;
5056 const owner = context.repo.owner;
5157 const repo = context.repo.repo;
52-
58+
5359 core.info(`Starting cleanup with hidden detection logic`);
5460 core.info(` Dry run: ${dryRun}`);
55- core.info(` Scan issues: ${scanIssues}`);
61+ core.info(` Scan open issues: ${scanIssues}`);
62+ core.info(` Scan closed issues: ${scanClosedIssues}`);
5663 core.info(` Scan comments: ${scanComments}`);
5764 core.info(``);
58-
65+
5966 // Load detection script from secret
6067 const detectionScript = process.env.SPAM_DETECTION_SCRIPT;
61-
68+
6269 if (!detectionScript) {
6370 core.error("SPAM_DETECTION_SCRIPT secret not found!");
71+ core.error("The cleanup tool requires the SPAM_DETECTION_SCRIPT secret to be configured.");
72+ core.error("Please set up the secret before running cleanup.");
73+ core.error("See documentation for setup instructions.");
6474 core.setFailed("Spam detection not configured");
6575 return;
6676 }
67-
77+
6878 // Create analyzer function (all logic hidden in secret)
6979 let analyzeContent;
70-
80+
7181 try {
7282 // Wrap the detection script to create a reusable analyzer
7383 const analyzerWrapper = `
7484 (async function(github, context, core) {
7585 ${detectionScript}
7686 })
7787 `;
78-
88+
7989 // Execute to get the detection capabilities
8090 const detectionModule = eval(analyzerWrapper);
8191 await detectionModule(github, context, core);
82-
92+
8393 // Create a mock context for analysis
8494 analyzeContent = async function(body, actor, assoc) {
8595 // Create mock event for analysis
@@ -96,87 +106,87 @@ jobs:
96106 action: 'opened'
97107 }
98108 };
99-
109+
100110 // Run detection on mock event
101111 let isSpam = false;
102112 let reason = "Clean";
103113 let score = 0;
104-
114+
105115 // Capture the detection result by checking if action would be taken
106116 const originalUpdate = github.rest.issues.update;
107117 const originalDelete = github.rest.issues.deleteComment;
108-
118+
109119 let detectionResult = { isSpam: false };
110-
120+
111121 github.rest.issues.update = async (params) => {
112122 detectionResult = { isSpam: true, reason: "Would close", score: 2 };
113123 return { data: {} };
114124 };
115-
125+
116126 github.rest.issues.deleteComment = async (params) => {
117127 detectionResult = { isSpam: true, reason: "Would delete", score: 2 };
118128 return { data: {} };
119129 };
120-
130+
121131 github.rest.issues.createComment = async () => ({ data: {} });
122132 github.rest.pulls.update = async () => ({ data: {} });
123133 github.rest.pulls.deleteReviewComment = async () => ({ data: {} });
124-
134+
125135 try {
126136 await detectionModule(github, mockContext, core);
127137 } catch (e) {
128138 // Ignore errors from mock execution
129139 }
130-
140+
131141 // Restore original functions
132142 github.rest.issues.update = originalUpdate;
133143 github.rest.issues.deleteComment = originalDelete;
134-
144+
135145 return detectionResult;
136146 };
137-
147+
138148 core.info("✅ Loaded spam detection from secret");
139149 core.info("");
140-
150+
141151 } catch (err) {
142152 core.error(`Failed to load detection: ${err.message}`);
143153 core.setFailed(`Detection script error`);
144154 return;
145155 }
146-
156+
147157 let totalScanned = 0;
148158 let totalSpam = 0;
149159 let totalClosed = 0;
150160 let totalDeleted = 0;
151-
161+
152162 // Scan issues
153163 if (scanIssues) {
154164 core.info("Scanning open issues...");
155165 let page = 1;
156-
166+
157167 while (maxIssues === 0 || totalScanned < maxIssues) {
158168 const issues = await github.rest.issues.listForRepo({
159169 owner, repo, state: 'open', per_page: 100, page
160170 });
161-
171+
162172 if (issues.data.length === 0) break;
163-
173+
164174 for (const issue of issues.data) {
165175 if (issue.pull_request) continue;
166176 if (maxIssues > 0 && totalScanned >= maxIssues) break;
167-
177+
168178 totalScanned++;
169179 const analysis = await analyzeContent(
170180 issue.body || "",
171181 issue.user?.login || "unknown",
172182 issue.author_association || "NONE"
173183 );
174-
184+
175185 if (analysis.isSpam) {
176186 totalSpam++;
177187 core.warning(`[SPAM] Issue #${issue.number} by @${issue.user?.login}`);
178188 core.warning(` Preview: ${(issue.body || "").substring(0, 150)}...`);
179-
189+
180190 if (!dryRun) {
181191 try {
182192 await github.rest.issues.update({
@@ -198,33 +208,86 @@ jobs:
198208 page++;
199209 }
200210 }
201-
211+
212+ // Scan closed issues
213+ if (scanClosedIssues) {
214+ core.info("Scanning closed issues...");
215+ let page = 1;
216+ let closedScanned = 0;
217+
218+ while (maxIssues === 0 || closedScanned < maxIssues) {
219+ const issues = await github.rest.issues.listForRepo({
220+ owner, repo, state: 'closed', per_page: 100, page
221+ });
222+
223+ if (issues.data.length === 0) break;
224+
225+ for (const issue of issues.data) {
226+ if (issue.pull_request) continue;
227+ if (maxIssues > 0 && closedScanned >= maxIssues) break;
228+
229+ closedScanned++;
230+ const analysis = await analyzeContent(
231+ issue.body || "",
232+ issue.user?.login || "unknown",
233+ issue.author_association || "NONE"
234+ );
235+
236+ if (analysis.isSpam) {
237+ totalSpam++;
238+ core.warning(`[SPAM] Closed Issue #${issue.number} by @${issue.user?.login}`);
239+ core.warning(` Preview: ${(issue.body || "").substring(0, 150)}...`);
240+ core.warning(` Already closed, but content still visible`);
241+
242+ if (!dryRun) {
243+ try {
244+ // Rewrite the closed spam issue to remove content
245+ await github.rest.issues.update({
246+ owner, repo, issue_number: issue.number,
247+ title: "[MODERATED] Content Removed",
248+ body: "**This content has been automatically moderated and removed.**\n\n" +
249+ "The original content violated our spam policy and has been hidden.\n\n" +
250+ "_This was detected during a cleanup scan of existing content._"
251+ });
252+ totalClosed++;
253+ core.notice(`✓ Moderated closed spam issue #${issue.number}`);
254+ } catch (err) {
255+ core.error(`✗ Failed to moderate issue #${issue.number}: ${err.message}`);
256+ }
257+ }
258+ }
259+ }
260+ page++;
261+ }
262+ core.info(`Scanned ${closedScanned} closed issues`);
263+ }
264+
202265 // Scan comments
203266 if (scanComments) {
204267 core.info("Scanning issue comments...");
205268 let page = 1;
206269 let commentCount = 0;
207-
270+
208271 while (page <= 10) {
209272 const comments = await github.rest.issues.listCommentsForRepo({
210273 owner, repo, per_page: 100, page, sort: 'created', direction: 'desc'
211274 });
212-
275+
213276 if (comments.data.length === 0) break;
214-
277+
215278 for (const comment of comments.data) {
216279 commentCount++;
217280 const analysis = await analyzeContent(
218281 comment.body || "",
219282 comment.user?.login || "unknown",
220283 comment.author_association || "NONE"
221284 );
222-
285+
223286 if (analysis.isSpam) {
224287 totalSpam++;
225288 core.warning(`[SPAM] Comment #${comment.id} by @${comment.user?.login}`);
226289 core.warning(` Preview: ${(comment.body || "").substring(0, 150)}...`);
227-
290+
228291 if (!dryRun) {
229292 try {
230293 await github.rest.issues.deleteComment({
@@ -240,19 +303,25 @@ jobs:
240303 }
241304 page++;
242305 }
243-
306+
244307 core.info(`Scanned ${commentCount} comments`);
245308 }
246-
309+
247310 // Summary
248311 core.notice("=".repeat(60));
249312 core.notice(`Cleanup Summary ${dryRun ? '(DRY RUN)' : '(EXECUTED)'}`);
250- core.notice(`Total scanned: ${totalScanned} issues`);
313+ core.notice("=".repeat(60));
314+ if (scanIssues) core.notice(`✓ Scanned open issues: ${totalScanned}`);
315+ if (scanClosedIssues) core.notice(`✓ Scanned closed issues for content removal`);
316+ if (scanComments) core.notice(`✓ Scanned comments`);
317+ core.notice("");
251318 core.notice(`Total spam found: ${totalSpam}`);
252319 if (!dryRun) {
253- core.notice(`Issues closed: ${totalClosed}`);
254- core.notice(`Comments deleted: ${totalDeleted}`);
320+ core.notice(`Actions taken:`);
321+ core.notice(` - Issues closed/moderated: ${totalClosed}`);
322+ core.notice(` - Comments deleted: ${totalDeleted}`);
255323 } else {
256- core.notice("DRY RUN - No actions taken. Set dry_run to 'false' to execute.");
324+ core.notice("⚠️ DRY RUN - No actions taken");
325+ core.notice(" Set dry_run to 'false' to execute cleanup");
257326 }
258327 core.notice("=".repeat(60));
0 commit comments