Skip to content

Refactor expired cleanup close/comment handlers - #50942

Merged
pelikhan merged 7 commits into
mainfrom
copilot/duplicate-code-expired-cleanup-handlers
Aug 7, 2026
Merged

Refactor expired cleanup close/comment handlers#50942
pelikhan merged 7 commits into
mainfrom
copilot/duplicate-code-expired-cleanup-handlers

Conversation

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Expired issue, pull request, and discussion cleanup shared the same comment/footer/close orchestration while reimplementing it per entity type. This made the scripts harder to maintain and left discussion dedupe behavior mixed into the duplicated closing flow.

  • Shared expired-entity handler

    • Added expired_entity_handler_factory.cjs to centralize:
      • expiration closing message construction
      • sanitized comment creation
      • entity close helpers
      • closed record shaping
      • comment-then-close sequencing
  • Declarative entity wrappers

    • Reduced issue and pull request cleanup scripts to entity-specific configuration.
    • Kept discussion-specific duplicate-comment and already-closed handling as a beforeComment hook.
  • Focused coverage

    • Added factory tests for the shared close/comment flow and hook-based skip behavior.

Example shape:

processEntity: createExpiredEntityHandler({
  workflowName,
  workflowId,
  runUrl,
  entityNoun: "pull request",
  entityLabel: "Pull Request",
  addComment: (pr, message) => addIssueThreadComment(github, owner, repo, pr.number, message),
  closeEntity: pr => closePullRequest(github, owner, repo, pr.number),
})

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 9.52 AIC · ⊞ 5.9K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.7 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Copilot AI and others added 3 commits August 7, 2026 00:03
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor expired cleanup handlers to eliminate code duplication Refactor expired cleanup close/comment handlers Aug 7, 2026
Copilot AI requested a review from pelikhan August 7, 2026 00:06
@pelikhan
pelikhan marked this pull request as ready for review August 7, 2026 00:16
Copilot AI balanced review requested due to automatic review settings August 7, 2026 00:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Centralizes expired-entity comment and close orchestration while preserving entity-specific behavior.

Changes:

  • Adds a shared handler factory and API helpers.
  • Converts issue, pull request, and discussion cleanup to declarative wrappers.
  • Adds focused factory tests for normal and skipped flows.
Show a summary per file
File Description
actions/setup/js/expired_entity_handler_factory.cjs Implements shared cleanup handling.
actions/setup/js/expired_entity_handler_factory.test.cjs Tests shared and skipped flows.
actions/setup/js/close_expired_issues.cjs Uses the shared handler for issues.
actions/setup/js/close_expired_pull_requests.cjs Uses the shared handler for pull requests.
actions/setup/js/close_expired_discussions.cjs Retains discussion-specific deduplication through a hook.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Security scanning failed for Matt Pocock Skills Reviewer. Review the logs for details.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Security scanning failed for Test Quality Sentinel. Review the logs for details.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Security scanning failed for Design Decision Gate 🏗️. Review the logs for details.

No ADR enforcement needed: PR does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Refactor expired cleanup close/comment handlers

The refactor is well-structured and the factory pattern cleanly eliminates the duplicated close/comment orchestration. Two issues to address:

  1. Blocking — The new test file expired_entity_handler_factory.test.cjs uses ESM import syntax inside a .cjs file, which will throw a SyntaxError at runtime (see inline comment).
  2. Non-blockinggithub, owner, and repo are declared in the factory options type but never used inside the factory itself; removing them simplifies the API (see inline comment).

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 29.3 AIC · ⊞ 5.3K

Comments that could not be inline-anchored

actions/setup/js/expired_entity_handler_factory.test.cjs:2

Bug: ESM import syntax in a .cjs file.

This file uses import { describe, it, expect, beforeEach, vi } from &quot;vitest&quot;; but .cjs files are treated as CommonJS by Node.js and will throw a SyntaxError at load time. Use require instead:

const { describe, it, expect, beforeEach, vi } = require(&quot;vitest&quot;);

Alternatively, rename the file to .test.mjs or .test.js if the package uses ESM.

@copilot please address this.

actions/setup/js/expired_entity_handler_factory.cjs:517

Unused fields in options type: github, owner, repo.

The JSDoc for createExpiredEntityHandler declares github, owner, and repo as required options, but the factory body never reads them — callers already close over those values in their addComment and closeEntity callbacks. Keeping them in the signature forces callers to repeat values they don't need to pass and creates a misleading API surface.

Remove these three fields from the JSDoc typedef and from the options destruc…

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel 🧪

Score: 85/100Excellent

Summary

PR adds a factory function for creating expired entity handlers (issues, PRs, discussions) with a comprehensive test suite covering core behaviors and edge cases.

Key Findings

Test Coverage Quality:

  • ✅ 2 new test functions, 100% design-test classification
  • ✅ Both tests verify user-facing handler behavior and hook mechanism
  • ✅ Edge-case coverage: pre-close hook bypass tested (status: "skipped")
  • ✅ 0% implementation-test ratio (threshold: 30%) — PASSES
  • ✅ Test:Prod line ratio 0.43:1 (well below 2:1 threshold)
  • ✅ No mock-library violations (clean vitest usage)

Tests Analyzed:

Test Classification Design Invariant Coverage
creates a handler that comments, closes, and returns a closed record Behavioral Handler comments, closes, and returns correct record 🟢 Happy path + mocks verify calls
allows a pre-close hook to skip the shared comment flow Behavioral beforeComment hook can bypass default flow 🟢 Edge case: hook returns early result

Per-Test Details

Test 1: creates a handler that comments, closes, and returns a closed record

Tests the standard flow: handler receives entity, invokes addComment() with message containing expiration date, invokes closeEntity(), and returns closed status with entity record. Mocks verify exact call signatures and result structure.

Quality: High-value design test — Verifies the handler's core contract.

Test 2: allows a pre-close hook to skip the shared comment flow

Tests the beforeComment hook mechanism: when hook returns a result, handler skips both addComment() and closeEntity() and returns the hook's result directly. Validates that neither external API is called and the status is "skipped".

Quality: High-value design test — Verifies the extension hook contract and error/bypass path.

Scoring Breakdown

Component Points Calculation
Design test ratio 40 (2/2) × 40
Edge-case coverage 15 (1/2) × 30
Duplicate assertions 20 20 − min(0, 5)
Test inflation 10 no ratio > 2:1
Total 85

Threshold Analysis:

  • Implementation tests: 0/2 = 0% (✅ < 30%)
  • No violations detected (✅)
  • Result: APPROVE

Test Quality Sentinel analyzed 2 behavioral tests in actions/setup/js/expired_entity_handler_factory.test.cjs.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 17.6 AIC · ⊞ 7.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 85/100 — Excellent. 2 behavioral tests added with 100% design-test classification, 0% implementation ratio (< 30% threshold), 0.43:1 test:prod ratio (< 2:1). No violations. APPROVE.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design, /tdd, and /diagnosing-bugs — requesting changes on a few concrete issues before merge.

📋 Key Themes & Highlights

Issues Found

  1. ESM import in a .cjs test file — syntax error in plain-Node execution (line 3 of the test file).
  2. core implicit global in factory — the factory calls core.info / core.warning via an undeclared ambient global, breaking testability. The test works around it via global.core assignment.
  3. Dead parameters (github, owner, repo) — accepted in options but never read by the factory; adds noise to every call site.
  4. status: "skipped" when discussion was actually closed — the hasComment=true branch closes the entity but reports skipped, hiding the close from the summary. Pre-existing bug preserved by the refactor.
  5. Missing test: beforeComment → undefined — the normal-flow pass-through case is not covered.

Positive Highlights

  • ✅ Clean extraction — three scripts reduced to pure configuration with no logic duplication.
  • beforeComment hook is a well-placed seam: keeps discussion-specific dedupe logic out of the factory without leaking concerns.
  • createClosedRecord / createExpiredEntityClosingMessage are good small helpers; easy to test and reason about.
  • ✅ Test structure is clear and follows Arrange/Act/Assert throughout.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 46.2 AIC · ⊞ 7.1K
Comment /matt to run again

@@ -0,0 +1,93 @@
// @ts-check
import { describe, it, expect, beforeEach, vi } from "vitest";
import { createExpiredEntityHandler } from "./expired_entity_handler_factory.cjs";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] ESM import syntax in a .cjs file will cause a syntax error at runtime — Node treats .cjs as CommonJS unconditionally.

💡 Fix: use require
const { describe, it, expect, beforeEach, vi } = require("vitest");

Or rename the file to .test.mjs. If Vitest's bundler transforms .cjs for you, add a comment saying so to avoid future confusion.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment at the top of the file explaining that Vitest's bundler transforms .cjs test files to ESM at test time, so the import syntax is intentional. Using require("vitest") is not an option — Vitest v4 only supports ESM and throws Vitest cannot be imported in a CommonJS module using require() when require() is used. This matches the pattern of all other .cjs test files in the project.

workflowId: options.workflowId,
runUrl: options.runUrl,
footerSuffix: options.footerSuffix,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] core is accessed as an implicit global inside the factory — this is invisible in the function signature and makes the module impossible to unit-test without monkey-patching global.core.

💡 Accept core as a parameter

Add core to the options object passed to createExpiredEntityHandler:

function createExpiredEntityHandler({ core, ...rest }) {
  return async entity => {
    // use injected core
    core.info(...)
  };
}

The test already sets global.core = mockCore as a workaround — passing it explicitly removes that fragility.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. core is now accepted as an explicit parameter in the options object and stored in a local variable at the top of the returned handler. All three call sites (close_expired_issues.cjs, close_expired_pull_requests.cjs, close_expired_discussions.cjs) now pass core directly. The test no longer needs global.core assignment.

* @param {{
* github: any,
* owner: string,
* repo: string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] github, owner, and repo are accepted in the options object but the factory body never reads them — they're dead parameters. This adds noise to every call site and misleads readers into thinking the factory performs its own API calls.

💡 Remove unused parameters

The actual API calls are delegated entirely to addComment and closeEntity callbacks, so github, owner, and repo serve no purpose inside the factory. Remove them from the options type and from all three call sites.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed github, owner, and repo from the JSDoc typedef and from all three call sites. The factory never needed them — the actual API calls are already fully encapsulated in the addComment and closeEntity callbacks.

const closingMessage = (cautionAlert ? cautionAlert + "\n\n" : "") + expirationText + generateExpiredEntityFooter(workflowName, runUrl, workflowId) + "\n\n<!-- gh-aw-closed -->";
if (hasComment) {
core.warning(` Discussion #${discussion.number} already has an expiration comment, skipping to avoid duplicate`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] When a discussion already has a comment (hasComment=true), the handler closes it and returns status: "skipped" — but the discussion was closed, so the record ends up in the skip list rather than the closed list, hiding real work from the summary.

💡 Return status: "closed"
if (hasComment) {
  core.warning(`  Discussion #${discussion.number} already has an expiration comment...`);
  core.info(`  Attempting to close discussion #${discussion.number} without adding another comment`);
  await closeDiscussionAsOutdated(github, discussion.id);
  core.info(`  ✓ Discussion closed successfully`);

  return {
-   status: "skipped",
+   status: "closed",   // the entity was actually closed
    record: createClosedRecord(discussion),
  };
}

This was the same behaviour in the original code — the refactor preserved the bug.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Changed status: "skipped" to status: "closed" in the hasComment=true branch. The discussion was actually being closed in that branch, so the record now correctly appears in the closed list rather than the skip list.

},
});
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test suite only covers two paths: full-flow and beforeComment returning an early result. The most important path — beforeComment returning undefined to allow the normal flow to proceed — is untested, which is exactly the case used by discussions when neither skip condition is met.

💡 Add a third test
it("runs normal flow when beforeComment returns undefined", async () => {
  const addComment = vi.fn().mockResolvedValue({});
  const closeEntity = vi.fn().mockResolvedValue({});

  const handler = createExpiredEntityHandler({
    github: {},
    owner: "o", repo: "r",
    workflowName: "W", workflowId: "wid",
    runUrl: "(example.com/redacted)",
    entityNoun: "discussion",
    entityLabel: "Discussion",
    beforeComment: async () => undefined, // explicitly pass through
    addComment,
    closeEntity,
  });

  const result = await handler({ number: 1, title: "T", url: "U", expirationDate: new Date() });

  expect(addComment).toHaveBeenCalledTimes(1);
  expect(closeEntity).toHaveBeenCalledTimes(1);
  expect(result.status).toBe("closed");
});

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the missing test. The third test case passes beforeComment: async () => undefined and verifies that addComment and closeEntity are both called once and result.status is "closed".

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Please triage this PR and run the pr-finisher skill before handing back to maintainers.

Address unresolved reviewer feedback:

  • github-actions: see requested changes in review history
  • github-actions: see requested changes in review history

Investigate failed checks:

Refresh the branch if needed.

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 9.52 AIC · ⊞ 5.9K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: refactor (shared expired-entity handler)
  • Risk: medium
  • Score: 58/100 (impact 20, urgency 18, quality 20)
  • Recommended action: batch_review (group with other refactor/chore PRs)
  • Note: needs-review label present; a github-actions review requested changes and security scans failed on related workflows — resolve before merge.

Generated by 🔧 PR Triage Agent · auto · 48.4 AIC · ⌖ 2.33 AIC · ⊞ 7.9K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Please triage this PR and run the pr-finisher skill before handing back to maintainers.

Address unresolved reviewer feedback (newest first):

Investigate failed checks:

Refresh the branch if needed.

Generated by 👨🍳 PR Sous Chef
Comment /souschef to run again

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.7 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Copilot AI commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Please triage this PR and run the pr-finisher skill before handing back to maintainers.

Address unresolved reviewer feedback (newest first):...

PR finisher complete.

  • Reviews — All 5 github-actions[bot] review threads were addressed in commit e685d47 with substantive replies. The threads are unresolved in the UI (no resolveReviewThread API available from the agent) but every concern has been handled:

    1. ESM import in .cjs — Added a comment explaining Vitest transforms .cjs files to ESM at test time; require("vitest") is unsupported by Vitest v4.
    2. core as implicit global — Fixed; core is now an explicit parameter in the options object; call sites updated; test no longer monkey-patches global.core.
    3. Dead parameters github/owner/repo — Fixed; removed from the JSDoc typedef and all three call sites (thread is outdated).
    4. status: "skipped" bug in hasComment branch — Fixed to status: "closed" since the discussion was actually being closed there.
    5. Missing beforeComment returning undefined test path — Added as a third test case; addComment and closeEntity both called once, result.status is "closed".
  • Checks (local)make fmt passes; vitest run expired_entity_handler_factory.test.cjs passes (3/3 tests).

  • Checks (CI) — Stale after agent push; needs human re-trigger. Prior failure (JS Tests shard 3/4) was ENOTEMPTY during temp-dir cleanup in git_helpers.test.cjs — a pre-existing flaky test unrelated to these changes. No fix needed for it.

  • Mergeable — Branch is up to date with main (merge commit 522cf56).

Hand-off: CI must be re-triggered by a maintainer (close/reopen, workflow_dispatch, or a push) before merge. Review threads should be resolved by a maintainer.

@pelikhan
pelikhan merged commit 3ce0276 into main Aug 7, 2026
10 checks passed
@pelikhan
pelikhan deleted the copilot/duplicate-code-expired-cleanup-handlers branch August 7, 2026 02:56
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[duplicate-code] Duplicate Code: expired cleanup handlers repeat close/comment flow

4 participants