Skip to content

Extract shared helper for close-older entity flows#15933

Merged
pelikhan merged 2 commits intomainfrom
copilot/refactor-close-older-entities
Feb 15, 2026
Merged

Extract shared helper for close-older entity flows#15933
pelikhan merged 2 commits intomainfrom
copilot/refactor-close-older-entities

Conversation

Copy link
Contributor

Copilot AI commented Feb 15, 2026

closeOlderIssues and closeOlderDiscussions contained ~140 lines of duplicate control flow (search → limit → iterate → close → error handling). Only entity-specific API calls differed.

Changes

  • New shared helper close_older_entities.cjs

    • Orchestrates complete flow: search, logging, MAX_CLOSE_COUNT limit, rate-limiting, error recovery
    • Entity-agnostic via callback configuration pattern (similar to expired_entity_main_flow.cjs)
  • Refactored callers to configuration-based invocation

    • close_older_issues.cjs: 308 → 222 lines (-28%)
    • close_older_discussions.cjs: 310 → 240 lines (-23%)
    • All exports and behavior unchanged

Before:

async function closeOlderIssues(...) {
  core.info("Starting operation...");
  const older = await searchOlderIssues(...);
  if (older.length === 0) return [];
  
  const toClose = older.slice(0, MAX_CLOSE_COUNT);
  // ... 100+ more lines of iteration, error handling, logging
}

After:

async function closeOlderIssues(...) {
  return closeOlderEntities(..., {
    entityType: "issue",
    searchOlderEntities: searchOlderIssues,
    addComment: addIssueComment,
    closeEntity: closeIssueAsNotPlanned,
    delayMs: API_DELAY_MS,
    getEntityId: entity => entity.number,
    getEntityUrl: entity => entity.html_url,
  });
}

Files

  • actions/setup/js/close_older_entities.cjs (new, 169 lines)
  • actions/setup/js/close_older_entities.test.cjs (new, 11 tests)
  • actions/setup/js/close_older_issues.cjs (refactored)
  • actions/setup/js/close_older_discussions.cjs (refactored)

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/github/gh-aw/contents/.github%2Fworkflows%2Faudit-workflows.md
    • Triggering command: /opt/hostedtoolcache/node/24.13.0/x64/bin/node /opt/hostedtoolcache/node/24.13.0/x64/bin/node --conditions node --conditions development --experimental-import-meta-resolve --require /home/REDACTED/work/gh-aw/gh-aw/actions/setup/js/node_modules/vitest/suppress-warnings.cjs /home/REDACTED/work/gh-aw/gh-aw/actions/setup/js/node_modules/vitest/dist/workers/forks.js (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Duplicate Code: closeOlder* issue/discussion cleanup flow</issue_title>
<issue_description># 🔍 Duplicate Code Detected: Close Older Entities Flow

Analysis of commit 56e1ce9

Assignee: @copilot

Summary

The closeOlderIssues and closeOlderDiscussions functions implement nearly identical control flow for searching older entities, logging results, rate-limiting, and iterating to close items. The bodies are structurally the same with only entity-specific naming and API calls changed.

Duplication Details

Pattern: Close older entities with shared flow

  • Severity: Medium
  • Occurrences: 2
  • Locations:
    • actions/setup/js/close_older_issues.cjs:195
    • actions/setup/js/close_older_discussions.cjs:199
  • Code Sample:
async function closeOlderIssues(github, owner, repo, workflowId, newIssue, workflowName, runUrl) {
  core.info("=".repeat(70));
  core.info("Starting closeOlderIssues operation");
  core.info("=".repeat(70));
  ...
  const olderIssues = await searchOlderIssues(...);
  if (olderIssues.length === 0) {
    core.info("✓ No older issues found to close - operation complete");
    core.info("=".repeat(70));
    return [];
  }
  ...
  for (let i = 0; i < issuesToClose.length; i++) {
    const issue = issuesToClose[i];
    ...
    await addIssueComment(...);
    await closeIssueAsNotPlanned(...);
    ...
    if (i < issuesToClose.length - 1) {
      core.info(`Waiting \$\{API_DELAY_MS}ms before processing next issue to avoid rate limiting...`);
      await delay(API_DELAY_MS);
    }
  }
  ...
}
async function closeOlderDiscussions(github, owner, repo, workflowId, categoryId, newDiscussion, workflowName, runUrl) {
  core.info("=".repeat(70));
  core.info("Starting closeOlderDiscussions operation");
  core.info("=".repeat(70));
  ...
  const olderDiscussions = await searchOlderDiscussions(...);
  if (olderDiscussions.length === 0) {
    core.info("✓ No older discussions found to close - operation complete");
    core.info("=".repeat(70));
    return [];
  }
  ...
  for (let i = 0; i < discussionsToClose.length; i++) {
    const discussion = discussionsToClose[i];
    ...
    await addDiscussionComment(...);
    await closeDiscussionAsOutdated(...);
    ...
    if (i < discussionsToClose.length - 1) {
      core.info(`Waiting \$\{GRAPHQL_DELAY_MS}ms before processing next discussion to avoid rate limiting...`);
      await delay(GRAPHQL_DELAY_MS);
    }
  }
  ...
}

Impact Analysis

  • Maintainability: Changes to logging, rate limit handling, or summary output must be duplicated and kept in sync.
  • Bug Risk: Divergence risk if one flow receives a fix or enhancement but the other does not.
  • Code Bloat: Large, repetitive bodies across two files.

Refactoring Recommendations

  1. Extract shared flow to a helper

    • Extract to: actions/setup/js/expired_entity_main_flow.cjs or a new actions/setup/js/close_older_entities.cjs
    • Estimated effort: Medium (2-4 hours)
    • Benefits: Single flow for search/limit/log/close logic with entity-specific callbacks
  2. Parameterize entity-specific actions

    • Accept callbacks for search, comment, close, and delay duration
    • Benefits: Reduce duplication while preserving behavioral differences

Implementation Checklist

  • Review duplication findings
  • Prioritize refactoring tasks
  • Create refactoring plan
  • Implement changes
  • Update tests
  • Verify no functionality broken

Analysis Metadata

  • Analyzed Files: 2
  • Detection Method: Serena semantic code analysis
  • Commit: 56e1ce9
  • Analysis Date: 2026-02-15

Generated by Duplicate Code Detector

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

- Created close_older_entities.cjs with shared control flow
- Refactored close_older_issues.cjs to use shared helper
- Refactored close_older_discussions.cjs to use shared helper
- Added comprehensive tests for shared helper
- All existing tests still passing (25 tests)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor duplicate closeOlder functions for streamlined code Extract shared helper for close-older entity flows Feb 15, 2026
Copilot AI requested a review from pelikhan February 15, 2026 15:11
@pelikhan pelikhan marked this pull request as ready for review February 15, 2026 15:14
Copilot AI review requested due to automatic review settings February 15, 2026 15:14
@pelikhan pelikhan merged commit f6aebac into main Feb 15, 2026
182 of 185 checks passed
@pelikhan pelikhan deleted the copilot/refactor-close-older-entities branch February 15, 2026 15:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Extracts the duplicated “search → limit → iterate → comment → close → log/errors” flow from closeOlderIssues and closeOlderDiscussions into a shared helper, keeping entity-specific behavior behind a configuration object.

Changes:

  • Added closeOlderEntities() helper to centralize close-older control flow with callback-based customization.
  • Refactored issue + discussion flows to call the shared helper (reducing duplicate logic and consolidating logging/error handling).
  • Added a new unit test suite for the shared helper.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
actions/setup/js/close_older_entities.cjs New shared orchestration helper for closing older entities via config callbacks.
actions/setup/js/close_older_entities.test.cjs New Vitest suite validating the shared helper behavior (limits, errors, args, etc.).
actions/setup/js/close_older_issues.cjs Refactors issues flow to use shared helper and maps helper result back to issue return type.
actions/setup/js/close_older_discussions.cjs Refactors discussions flow to use shared helper; adjusts GraphQL helpers to match shared callback signature.
Comments suppressed due to low confidence (1)

actions/setup/js/close_older_entities.cjs:85

  • closeOlderEntities logs entity.number and entity.title directly, but those fields are not part of the EntityCloseConfig contract (only getEntityId/getEntityUrl are). If you intend this to be reusable, add config callbacks for display label/number/title (or a formatEntityForLog hook) so the helper doesn’t silently depend on a specific object shape.
  core.info(`Found ${olderEntities.length} older ${config.entityType}(s) matching the criteria`);
  for (const entity of olderEntities) {
    core.info(`  - ${config.entityType.charAt(0).toUpperCase() + config.entityType.slice(1)} #${entity.number}: ${entity.title}`);
    if (entity.labels) {
      core.info(`    Labels: ${entity.labels.map(l => l.name).join(", ") || "(none)"}`);
    }
    core.info(`    URL: ${config.getEntityUrl(entity)}`);
  }

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

@@ -5,26 +5,18 @@ const { getCloseOlderDiscussionMessage } = require("./messages_close_discussion.
const { getErrorMessage } = require("./error_helpers.cjs");
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

getErrorMessage is imported but never used in this refactored file (error formatting now happens inside close_older_entities.cjs). Please remove the unused require to avoid dead code and keep lint/ts-check clean.

Suggested change
const { getErrorMessage } = require("./error_helpers.cjs");

Copilot uses AI. Check for mistakes.
// Map to issue-specific return type
return result.map(item => ({
number: item.number,
html_url: item.html_url || "",
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This mapping now falls back to an empty string for html_url, but the function’s return contract is html_url: string and callers typically expect a valid URL. Prefer returning item.html_url directly (and/or asserting it’s present) so missing URLs fail fast instead of silently returning invalid links.

Suggested change
html_url: item.html_url || "",
html_url: item.html_url,

Copilot uses AI. Check for mistakes.
// Map to discussion-specific return type
return result.map(item => ({
number: item.number,
url: item.url || "",
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This mapping now falls back to an empty string for url, which can silently produce invalid links while the function’s return type indicates url: string. Prefer returning item.url directly (and/or validating it’s present) so unexpected missing URLs don’t get masked.

Suggested change
url: item.url || "",
url: item.url,

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +70
core.info(`Search criteria: workflow-id marker: "${getWorkflowIdMarkerContent(workflowId)}"`);
core.info(`New ${config.entityType} reference: #${newEntity.number} (${newEntity.url || newEntity.html_url})`);
core.info(`Workflow: ${workflowName}`);
core.info(`Run URL: ${runUrl}`);
core.info("");

// Step 1: Search for older entities
const olderEntities = await config.searchOlderEntities(github, owner, repo, workflowId, ...extraArgs, newEntity.number);

Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The helper claims to be entity-agnostic, but it hard-requires newEntity.number (and passes it to searchOlderEntities) rather than using a config callback. Consider adding something like getNewEntityNumber (or reusing getEntityId semantics) so the helper doesn’t bake in an assumption that every entity has a numeric number field.

This issue also appears on line 78 of the same file.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +54
const start = Date.now();
await delay(50);
const elapsed = Date.now() - start;
expect(elapsed).toBeGreaterThanOrEqual(45); // Allow some tolerance
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

This test relies on wall-clock timing (Date.now() + a real setTimeout) which can be flaky under CI load and slows the suite unnecessarily. Prefer using fake timers (e.g., advance timers and assert setTimeout was scheduled) or spying on setTimeout to make the test deterministic.

Suggested change
const start = Date.now();
await delay(50);
const elapsed = Date.now() - start;
expect(elapsed).toBeGreaterThanOrEqual(45); // Allow some tolerance
vi.useFakeTimers();
const promise = delay(50);
await vi.advanceTimersByTimeAsync(50);
await expect(promise).resolves.toBeUndefined();
vi.useRealTimers();

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate Code: closeOlder* issue/discussion cleanup flow

2 participants

Comments