Skip to content

Deduplicate closeOlder issue/PR wrapper wiring with a shared adapter#45045

Closed
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/duplicate-code-close-older-wrappers
Closed

Deduplicate closeOlder issue/PR wrapper wiring with a shared adapter#45045
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/duplicate-code-close-older-wrappers

Conversation

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

closeOlderIssues and closeOlderPullRequests had near-identical wrapper logic around closeOlderEntities, creating duplicated adapter wiring and drift risk. This change centralizes the shared orchestration while preserving entity-specific behavior and output shape.

  • Refactor: extract shared closeOlder adapter

    • Added actions/setup/js/close_older_adapter.cjs with closeOlderWithAdapter(...) to handle:
      • common closeOlderEntities(...) config wiring
      • forwarding of callerWorkflowId / closeOlderKey into search closures
      • consistent { number, html_url } result mapping
  • Wrapper simplification: issues + pull requests

    • Updated:
      • actions/setup/js/close_older_issues.cjs
      • actions/setup/js/close_older_pull_requests.cjs
    • Each wrapper now passes only entity-specific pieces (searchOlder*, message mapping, comment/close handlers, labels), removing duplicated plumbing.
  • Coverage for shared adapter behavior

    • Added actions/setup/js/close_older_adapter.test.cjs to validate:
      • forwarding of dedupe context (callerWorkflowId, closeOlderKey)
      • message-parameter transformation before getCloseMessage
      • normalization of missing html_url in mapped results
return closeOlderWithAdapter({
  github, owner, repo, workflowId, newEntity, workflowName, runUrl,
  callerWorkflowId, closeOlderKey,
  entityType: "issue",
  entityTypePlural: "issues",
  searchOlderEntities: searchOlderIssues,
  getCloseMessage: getCloseOlderIssueMessage,
  messageParams: p => ({
    newIssueUrl: p.newEntityUrl,
    newIssueNumber: p.newEntityNumber,
    workflowName: p.workflowName,
    runUrl: p.runUrl,
  }),
  addComment: addIssueComment,
  closeEntity: closeIssueAsNotPlanned,
  delayMs: API_DELAY_MS,
});

Copilot AI and others added 3 commits July 12, 2026 06:35
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 duplicate code in closeOlder adapter wrappers Deduplicate closeOlder issue/PR wrapper wiring with a shared adapter Jul 12, 2026
Copilot AI requested a review from pelikhan July 12, 2026 06:42
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk 🟢 low
Score 35/100 (impact:15 + urgency:10 + quality:10)
Action 📦 batch_review
Batch pr-batch:refactor-dedup (with #45044, #45018)

Rationale: Draft. Deduplicates closeOlder issue/PR wrapper wiring into shared adapter. Low risk, 4 files. Batch with other refactor/dedup PRs.

Triage run §29183606049

Generated by 🔧 PR Triage Agent · 171.6 AIC · ⌖ 5.63 AIC · ⊞ 5.6K ·

@github-actions

Copy link
Copy Markdown
Contributor

Hey @copilot-swe-agent 👋 — great work extracting the shared closeOlderWithAdapter adapter! Centralizing the duplicated orchestration logic across close_older_issues.cjs and close_older_pull_requests.cjs is a clean refactor that reduces drift risk.

This PR looks well-structured: it's focused on a single concern, includes a dedicated test file (close_older_adapter.test.cjs), and has a clear description of the change and its motivation. It looks ready for review. ✅

Generated by ✅ Contribution Check · 95.5 AIC · ⌖ 17.4 AIC · ⊞ 6.2K ·

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk 🟢 Low
Score 42/100 (impact: 18, urgency: 10, quality: 14)
Batch refactor-dedup (with #45044, #45018)
Action batch_review

Draft. Deduplication refactor with moderate size (+199/-42). Review together with the refactor-dedup batch.

Generated by 🔧 PR Triage Agent · 48.3 AIC · ⌖ 7.99 AIC · ⊞ 5.6K ·

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk 🟢 Low
Score 30/100 (impact:15 urgency:5 quality:10)
Action 📦 batch_review
Batch pr-batch:refactor-dedup

Rationale: DRAFT — deduplicates closeOlder adapter wiring into a shared module (4 files, includes tests). Blocked until undrafted. Group with PR #45044 for batch review of the refactor-dedup cluster.

Generated by 🔧 PR Triage Agent · 181 AIC · ⌖ 5.14 AIC · ⊞ 5.6K ·

@pelikhan pelikhan marked this pull request as ready for review July 13, 2026 14:32
Copilot AI review requested due to automatic review settings July 13, 2026 14:32
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

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

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 duplicated issue/PR close-older orchestration in a shared adapter while preserving entity-specific behavior.

Changes:

  • Adds a shared adapter for search context, message mapping, and result normalization.
  • Simplifies issue and pull-request wrappers.
  • Adds focused adapter tests.
Show a summary per file
File Description
actions/setup/js/close_older_adapter.cjs Implements shared adapter wiring.
actions/setup/js/close_older_adapter.test.cjs Tests forwarding, transformation, and normalization.
actions/setup/js/close_older_issues.cjs Migrates issue handling to the adapter.
actions/setup/js/close_older_pull_requests.cjs Migrates PR handling to the adapter.

Review details

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Medium

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@pelikhan pelikhan closed this Jul 13, 2026
@github-actions github-actions Bot mentioned this pull request Jul 13, 2026

@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: Deduplicate closeOlder issue/PR wrapper wiring

The refactor is clean and well-structured. The adapter correctly centralizes callerWorkflowId/closeOlderKey forwarding via the search closure, and the messageParams indirection is a good design.

One issue to fix: The new test file imports the CJS module under test with a bare ESM import (line 4), which diverges from the project convention. All peer test files use createRequire(import.meta.url) to share the CJS module cache — this matters for correct mock behaviour with vi. See the inline comment for details.

No other issues found in the adapter implementation or the wrapper simplifications.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 29.2 AIC · ⌖ 4.4 AIC · ⊞ 4.8K

// @ts-check

import { describe, it, expect, beforeEach, vi } from "vitest";
import { closeOlderWithAdapter } from "./close_older_adapter.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.

CJS module imported via ESM import — use createRequire instead.

All other .test.cjs files in this directory (e.g. action_conclusion_otlp.test.cjs) import CJS modules-under-test with the pattern:

import { createRequire } from "module";
const req = createRequire(import.meta.url);
const { closeOlderWithAdapter } = req("./close_older_adapter.cjs");

Using a bare ESM import for a .cjs file bypasses the shared CJS module cache. This can cause test isolation issues and inconsistent mock behaviour. It also diverges from the established project convention.

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

🔶 Test Quality Score: 43/100 — Needs Improvement

Analyzed 3 test(s): 1 design, 2 implementation, 0 violation(s).

📊 Metrics (3 tests)
Metric Value
Analyzed 3 (Go: 0, JS: 3)
✅ Design 1 (33%)
⚠️ Implementation 2 (67%)
Edge/error coverage 1 (33%)
Duplicate clusters 0
Inflation Yes (2.06:1, threshold 2:1)
🚨 Violations 0
Test File Classification Issues
forwards callerWorkflowId and closeOlderKey close_older_adapter.test.cjs:17 implementation_test Happy-path only; asserts internal mock calls, no error path
applies messageParams transformation close_older_adapter.test.cjs:52 implementation_test Happy-path only; verifies internal call wiring rather than user-visible output
normalizes missing html_url in mapped result close_older_adapter.test.cjs:89 design_test ✅ Edge case covered: missing html_url normalized to ""
⚠️ Flagged Tests (2)

forwards callerWorkflowId and closeOlderKey to searchOlderEntities (close_older_adapter.test.cjs:17) — implementation_test. Primarily asserts that searchOlderEntities was called with specific arguments and checks the return value passthrough. No error path or rejection scenario tested. Consider adding a test where searchOlderEntities returns an empty array or rejects.

applies messageParams transformation before getCloseMessage (close_older_adapter.test.cjs:52) — implementation_test. Verifies the internal call chain (getCloseMessageaddCommentcloseEntity) with specific arguments. This tests wiring, not a user-visible contract. The transformation logic could be a design test if it asserted observable outputs like comments posted or entities closed; instead it only checks mock invocation arguments.

Verdict

Failed. 67% implementation tests (threshold: 30%). The two wiring tests confirm internal call sequences but do not assert behavioral contracts (e.g., what happens when closing fails, when no older entities exist, or when addComment rejects). Refactoring these to focus on observable outcomes — or adding error-path tests — would bring the score above threshold.

References: §29258378051

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 25.8 AIC · ⌖ 10 AIC · ⊞ 6.8K ·
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: 43/100. 67% implementation tests (threshold: 30%). Review flagged tests in the comment above.

@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 and /tdd — clean refactor with good test coverage; a few suggestions to tighten the interface.

📋 Key Themes & Highlights

Key Themes

  • messageParams double-indirection — the adapter accepts both getCloseMessage and messageParams when one would suffice, widening the interface unnecessarily.
  • Silent empty-string coercion — missing html_url is silently normalised to "", which could produce broken links for callers that render URLs.
  • Import/require mismatch — test file uses ES module import in a .cjs file; works if vitest is configured for it, but the setup is undocumented.

Positive Highlights

  • ✅ Clean extraction of shared orchestration — both wrappers are noticeably simpler.
  • ✅ Closure-based forwarding of callerWorkflowId/closeOlderKey is well-reasoned and the comment explaining it is helpful.
  • ✅ Tests cover all three key adapter behaviours (forwarding context, message transformation, result normalisation).
  • ✅ PR description includes a clear code example of the new call site — great for reviewers.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 34.3 AIC · ⌖ 4.91 AIC · ⊞ 6.6K
Comment /matt to run again

* @param {(github: any, owner: string, repo: string, entityId: number) => Promise<any>} params.closeEntity
* @param {number} params.delayMs
* @param {(params: {newEntityUrl: string, newEntityNumber: number, workflowName: string, runUrl: string}) => any} params.messageParams
* @returns {Promise<Array<{number: number, html_url: 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.

{"pull_request_number":45045,"path":"actions/setup/js/close_older_adapter.cjs","line":26,"body":"[/codebase-design] The messageParams indirection adds an extra moving part without adding safety — callers already own the mapping, so the adapter interface is unnecessarily wide.

������ Simpler alternative

Instead of accepting both getCloseMessage and messageParams, accept a single already-adapted getCloseMessage from the caller:

// In close_older_issues.cjs
getCloseMessage: params => getCloseOlderIssueMessage({
  newIssueUrl: params.newEntityUrl,
  newIssueNumber: params.newEntityNumber,
  workflowName: params.workflowName,
  runUrl: params.runUrl,
}),

This removes messageParams from closeOlderWithAdapter entirely and eliminates the double-indirection on line 40.

@copilot please address this."}

* @param {(github: any, owner: string, repo: string, entityId: number) => Promise<any>} params.closeEntity
* @param {number} params.delayMs
* @param {(params: {newEntityUrl: string, newEntityNumber: number, workflowName: string, runUrl: string}) => any} params.messageParams
* @returns {Promise<Array<{number: number, html_url: 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.

test

* @param {(github: any, owner: string, repo: string, entityId: number) => Promise<any>} params.closeEntity
* @param {number} params.delayMs
* @param {(params: {newEntityUrl: string, newEntityNumber: number, workflowName: string, runUrl: string}) => any} params.messageParams
* @returns {Promise<Array<{number: number, html_url: 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] messageParams adds a double-indirection layer that widens the adapter interface without providing safety — callers already own the message-shape translation.

💡 Simpler alternative: merge into `getCloseMessage`

Drop messageParams from the adapter params and let callers adapt directly in getCloseMessage:

// close_older_issues.cjs
getCloseMessage: params => getCloseOlderIssueMessage({
  newIssueUrl: params.newEntityUrl,
  newIssueNumber: params.newEntityNumber,
  workflowName: params.workflowName,
  runUrl: params.runUrl,
}),

This removes one adapter param, eliminates the chained call on line 40 (params.getCloseMessage(params.messageParams(closeParams))), and makes the data flow easier to trace.

@copilot please address this.

// @ts-check

import { describe, it, expect, beforeEach, vi } from "vitest";
import { closeOlderWithAdapter } from "./close_older_adapter.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] The test file uses ES module import syntax but the implementation uses CommonJS require. This mismatch will cause the test suite to fail unless the vitest config handles CJS interop explicitly.

💡 Fix: use `require` to match the CJS module system
// `@ts-check`
const { describe, it, expect, beforeEach, vi } = require("vitest");
const { closeOlderWithAdapter } = require("./close_older_adapter.cjs");

Look at close_older_entities.test.cjs — it uses import statements too, so this may be intentional via a vitest ESM transform. If so, add a brief comment explaining the interop setup so future contributors don't trip over it.

@copilot please address this.


return result.map(item => ({
number: item.number,
html_url: item.html_url || "",

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] html_url: item.html_url || "" silently coerces a missing URL to an empty string. Callers that concatenate or render html_url will silently produce broken links.

💡 Make the sentinel explicit

Consider returning html_url: item.html_url ?? "" (same runtime behaviour) and adding a core.warning when html_url is absent:

if (!item.html_url) {
  core.warning(`closeOlderWithAdapter: entity #${item.number} has no html_url`);
}
return { number: item.number, html_url: item.html_url ?? "" };

This surfaces data quality issues in CI logs without changing the return type.

@copilot please address this.

@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.

Non-blocking review — one medium concern.

The refactor cleanly eliminates duplicated adapter wiring and the tests cover the key forwarding behavior. One issue worth addressing before further extension of this pattern:

** leaks internals.** Callers must know that the callback receives — a shape that lives entirely inside close_older_entities.cjs. If those field names change, all callers silently produce undefined-filled close messages with no compiler or test signal. Adding a @typedef CloseMessageParams in the adapter (or removing the indirection) would seal this gap.

🔎 Code quality review by PR Code Quality Reviewer · 50.3 AIC · ⌖ 5.13 AIC · ⊞ 5.4K
Comment /review to run again

* @param {object} params
* @param {any} params.github
* @param {string} params.owner
* @param {string} params.repo

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.

Leaky internal abstraction: messageParams forces callers to know closeOlderEntities's private {newEntityUrl, newEntityNumber} field names, coupling adapter consumers to an implementation detail they cannot see.

💡 Details

The adapter's getCloseMessage closure calls params.getCloseMessage(params.messageParams(closeParams)) where closeParams is the shape produced internally by closeOlderEntities:

// Inside close_older_entities.cjs (line 112):
config.getCloseMessage({
  newEntityUrl: newEntity.url || newEntity.html_url,
  newEntityNumber: newEntity.number,
  workflowName,
  runUrl,
});

That opaque shape leaks out as the required input to every messageParams callback in callers (close_older_issues.cjs, close_older_pull_requests.cjs). If closeOlderEntities ever renames those fields, all callers silently produce undefined in their close messages — no type error, no runtime guard, no test failure at the boundary.

Fix options:

  1. Add a @typedef CloseMessageParams JSDoc visible to callers documenting the expected shape.
  2. Pass newEntity + context directly to getCloseMessage and remove the messageParams indirection entirely.

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: closeOlder issue/PR adapter wrappers share the same wiring

3 participants