-
Notifications
You must be signed in to change notification settings - Fork 454
Deduplicate closeOlder issue/PR wrapper wiring with a shared adapter #45045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // @ts-check | ||
| /// <reference types="@actions/github-script" /> | ||
|
|
||
| const { closeOlderEntities } = require("./close_older_entities.cjs"); | ||
|
|
||
| /** | ||
| * Close older issues/PRs using shared adapter wiring. | ||
| * @param {object} params | ||
| * @param {any} params.github | ||
| * @param {string} params.owner | ||
| * @param {string} params.repo | ||
| * @param {string} params.workflowId | ||
| * @param {{number: number, html_url: string}} params.newEntity | ||
| * @param {string} params.workflowName | ||
| * @param {string} params.runUrl | ||
| * @param {string | undefined} params.callerWorkflowId | ||
| * @param {string | undefined} params.closeOlderKey | ||
| * @param {string} params.entityType | ||
| * @param {string} params.entityTypePlural | ||
| * @param {(github: any, owner: string, repo: string, workflowId: string, excludeNumber: number, callerWorkflowId?: string, closeOlderKey?: string) => Promise<Array<{number: number, html_url?: string}>>} params.searchOlderEntities | ||
| * @param {(params: any) => string} params.getCloseMessage | ||
| * @param {(github: any, owner: string, repo: string, entityId: number, message: string) => Promise<any>} params.addComment | ||
| * @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}>>} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ������ Simpler alternativeInstead of accepting both // In close_older_issues.cjs
getCloseMessage: params => getCloseOlderIssueMessage({
newIssueUrl: params.newEntityUrl,
newIssueNumber: params.newEntityNumber,
workflowName: params.workflowName,
runUrl: params.runUrl,
}),This removes @copilot please address this."}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. test
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] 💡 Simpler alternative: merge into `getCloseMessage`Drop // 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 ( @copilot please address this. |
||
| */ | ||
| async function closeOlderWithAdapter(params) { | ||
| const result = await closeOlderEntities(params.github, params.owner, params.repo, params.workflowId, params.newEntity, params.workflowName, params.runUrl, { | ||
| entityType: params.entityType, | ||
| entityTypePlural: params.entityTypePlural, | ||
| // Use a closure so callerWorkflowId and closeOlderKey are forwarded to searchOlderEntities | ||
| // without going through the closeOlderEntities extraArgs mechanism (which appends | ||
| // excludeNumber last) | ||
| searchOlderEntities: (gh, o, r, wid, excludeNumber) => params.searchOlderEntities(gh, o, r, wid, excludeNumber, params.callerWorkflowId, params.closeOlderKey), | ||
| getCloseMessage: closeParams => params.getCloseMessage(params.messageParams(closeParams)), | ||
| addComment: params.addComment, | ||
| closeEntity: params.closeEntity, | ||
| delayMs: params.delayMs, | ||
| getEntityId: entity => entity.number, | ||
| getEntityUrl: entity => entity.html_url, | ||
| }); | ||
|
|
||
| return result.map(item => ({ | ||
| number: item.number, | ||
| html_url: item.html_url || "", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] 💡 Make the sentinel explicitConsider returning 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. |
||
| })); | ||
| } | ||
|
|
||
| module.exports = { | ||
| closeOlderWithAdapter, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // @ts-check | ||
|
|
||
| import { describe, it, expect, beforeEach, vi } from "vitest"; | ||
| import { closeOlderWithAdapter } from "./close_older_adapter.cjs"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CJS module imported via ESM All other import { createRequire } from "module";
const req = createRequire(import.meta.url);
const { closeOlderWithAdapter } = req("./close_older_adapter.cjs");Using a bare ESM @copilot please address this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The test file uses ES module 💡 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 @copilot please address this. |
||
|
|
||
| describe("close_older_adapter", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| global.core = { | ||
| info: vi.fn(), | ||
| warning: vi.fn(), | ||
| error: vi.fn(), | ||
| }; | ||
| }); | ||
|
|
||
| it("forwards callerWorkflowId and closeOlderKey to searchOlderEntities", async () => { | ||
| const github = {}; | ||
| const searchOlderEntities = vi.fn().mockResolvedValue([ | ||
| { | ||
| number: 12, | ||
| title: "Older issue", | ||
| html_url: "https://example/12", | ||
| }, | ||
| ]); | ||
| const getCloseMessage = vi.fn().mockReturnValue("close message"); | ||
| const addComment = vi.fn().mockResolvedValue({ id: 1 }); | ||
| const closeEntity = vi.fn().mockResolvedValue({ number: 12, html_url: "https://example/12" }); | ||
|
|
||
| const result = await closeOlderWithAdapter({ | ||
| github, | ||
| owner: "owner", | ||
| repo: "repo", | ||
| workflowId: "workflow", | ||
| newEntity: { number: 55, html_url: "https://example/new" }, | ||
| workflowName: "Workflow", | ||
| runUrl: "https://example/run", | ||
| callerWorkflowId: "caller-id", | ||
| closeOlderKey: "close-key", | ||
| entityType: "issue", | ||
| entityTypePlural: "issues", | ||
| searchOlderEntities, | ||
| getCloseMessage, | ||
| addComment, | ||
| closeEntity, | ||
| delayMs: 1, | ||
| messageParams: params => ({ transformedEntityUrl: params.newEntityUrl, transformedEntityNumber: params.newEntityNumber }), | ||
| }); | ||
|
|
||
| expect(searchOlderEntities).toHaveBeenCalledWith(github, "owner", "repo", "workflow", 55, "caller-id", "close-key"); | ||
| expect(result).toEqual([{ number: 12, html_url: "https://example/12" }]); | ||
| }); | ||
|
|
||
| it("applies messageParams transformation before getCloseMessage", async () => { | ||
| const github = {}; | ||
| const getCloseMessage = vi.fn().mockReturnValue("close message"); | ||
| const addComment = vi.fn().mockResolvedValue({ id: 1 }); | ||
| const closeEntity = vi.fn().mockResolvedValue({ number: 12, html_url: "https://example/12" }); | ||
|
|
||
| await closeOlderWithAdapter({ | ||
| github, | ||
| owner: "owner", | ||
| repo: "repo", | ||
| workflowId: "workflow", | ||
| newEntity: { number: 55, html_url: "https://example/new" }, | ||
| workflowName: "Workflow", | ||
| runUrl: "https://example/run", | ||
| callerWorkflowId: "caller-id", | ||
| closeOlderKey: "close-key", | ||
| entityType: "issue", | ||
| entityTypePlural: "issues", | ||
| searchOlderEntities: vi.fn().mockResolvedValue([{ number: 12, title: "Older issue", html_url: "https://example/12" }]), | ||
| getCloseMessage, | ||
| addComment, | ||
| closeEntity, | ||
| delayMs: 1, | ||
| messageParams: params => ({ transformedEntityUrl: params.newEntityUrl, transformedEntityNumber: params.newEntityNumber }), | ||
| }); | ||
|
|
||
| expect(getCloseMessage).toHaveBeenCalledWith({ transformedEntityUrl: "https://example/new", transformedEntityNumber: 55 }); | ||
| expect(addComment).toHaveBeenCalledWith(github, "owner", "repo", 12, "close message"); | ||
| expect(closeEntity).toHaveBeenCalledWith(github, "owner", "repo", 12); | ||
| }); | ||
|
|
||
| it("normalizes missing html_url in mapped result", async () => { | ||
| const result = await closeOlderWithAdapter({ | ||
| github: {}, | ||
| owner: "owner", | ||
| repo: "repo", | ||
| workflowId: "workflow", | ||
| newEntity: { number: 10, html_url: "https://example/new" }, | ||
| workflowName: "Workflow", | ||
| runUrl: "https://example/run", | ||
| callerWorkflowId: undefined, | ||
| closeOlderKey: undefined, | ||
| entityType: "issue", | ||
| entityTypePlural: "issues", | ||
| searchOlderEntities: vi.fn().mockResolvedValue([{ number: 22, title: "Older issue" }]), | ||
| getCloseMessage: vi.fn().mockReturnValue("close message"), | ||
| addComment: vi.fn().mockResolvedValue({ id: 1 }), | ||
| closeEntity: vi.fn().mockResolvedValue({ number: 22 }), | ||
| delayMs: 1, | ||
| messageParams: params => params, | ||
| }); | ||
|
|
||
| expect(result).toEqual([{ number: 22, html_url: "" }]); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaky internal abstraction:
messageParamsforces callers to knowcloseOlderEntities's private{newEntityUrl, newEntityNumber}field names, coupling adapter consumers to an implementation detail they cannot see.💡 Details
The adapter's
getCloseMessageclosure callsparams.getCloseMessage(params.messageParams(closeParams))wherecloseParamsis the shape produced internally bycloseOlderEntities:That opaque shape leaks out as the required input to every
messageParamscallback in callers (close_older_issues.cjs,close_older_pull_requests.cjs). IfcloseOlderEntitiesever renames those fields, all callers silently produceundefinedin their close messages — no type error, no runtime guard, no test failure at the boundary.Fix options:
@typedef CloseMessageParamsJSDoc visible to callers documenting the expected shape.newEntity+ context directly togetCloseMessageand remove themessageParamsindirection entirely.