Skip to content

feat(close-issue): add duplicate_of for native GitHub duplicate marking#43152

Open
pelikhan with Copilot wants to merge 9 commits into
mainfrom
copilot/support-native-duplicate-marking
Open

feat(close-issue): add duplicate_of for native GitHub duplicate marking#43152
pelikhan with Copilot wants to merge 9 commits into
mainfrom
copilot/support-native-duplicate-marking

Conversation

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

When close-issue closes an issue as a duplicate, it previously could only record state_reason: duplicate and post a plain text comment — the native GitHub "marked this as a duplicate of #X" timeline relationship was never created.

Changes

close_issue.cjs

  • parseDuplicateOf() — resolves duplicate_of to {owner, repo, issueNumber} from bare numbers, #N, owner/repo#N, or full GitHub URLs
  • markIssueAsDuplicate() — fetches node IDs via REST then calls the markAsDuplicate GraphQL mutation
  • closeEntity callback: when item.duplicate_of is set and effective state_reason is DUPLICATE, calls the mutation after closing; mutation failures are non-fatal (warning logged, close still succeeds)

MCP tool schema (safe_outputs_tools.json — both runtime and compiler copies)

  • Added optional duplicate_of: number | string property to close_issue

Types (safe-outputs.d.ts)

  • Added duplicate_of?: number | string to CloseIssueItem

Docs — reference page and spec updated with semantics, accepted formats, and the non-fatal fallback behaviour

Usage

{"type": "close_issue", "issue_number": 1234, "state_reason": "DUPLICATE", "duplicate_of": "github/gh-aw#22853", "body": "Duplicate of #22853."}

This produces a native marked_as_duplicate timeline event linking the canonical issue instead of a free-text comment carrying the linkage.


Generated by 👨‍🍳 PR Sous Chef · 7.05 AIC · ⌖ 5.18 AIC · ⊞ 6.6K ·
Comment /souschef to run again

When `duplicate_of` is set and `state_reason` is DUPLICATE, the handler
now calls the GitHub `markAsDuplicate` GraphQL mutation to create a
native "marked this as a duplicate of #X" timeline event, instead of
relying solely on a text comment.

Supports bare numbers, #NUMBER, owner/repo#NUMBER, and full URLs.
Failures in the mutation are non-fatal (logged as warnings).

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Support native duplicate marking in close-issue safe output feat(close-issue): add duplicate_of for native GitHub duplicate marking Jul 3, 2026
Copilot AI requested a review from pelikhan July 3, 2026 09:11
@pelikhan pelikhan marked this pull request as ready for review July 3, 2026 09:34
Copilot AI review requested due to automatic review settings July 3, 2026 09:34

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

Adds first-class native GitHub duplicate relationships to the close_issue safe-output by introducing a duplicate_of field, so “marked as duplicate of #X” timeline events can be created via GraphQL (with a non-fatal fallback on failure).

Changes:

  • Add duplicate_of to the close-issue safe-output schema (runtime + compiler copies) and TypeScript types.
  • Implement parsing + GraphQL markAsDuplicate mutation in close_issue.cjs, and add targeted tests.
  • Update safe-outputs docs/spec to describe the new field, accepted formats, and fallback behavior.
Show a summary per file
File Description
pkg/workflow/js/safe_outputs_tools.json Adds duplicate_of to the close_issue tool schema (compiler/runtime copy).
actions/setup/js/safe_outputs_tools.json Adds duplicate_of to the close_issue tool schema (action/runtime copy).
actions/setup/js/types/safe-outputs.d.ts Extends CloseIssueItem with duplicate_of?: number | string.
actions/setup/js/close_issue.cjs Implements parseDuplicateOf() and native duplicate marking via GraphQL mutation.
actions/setup/js/close_issue.test.cjs Adds unit coverage for parsing and native duplicate marking behavior.
docs/src/content/docs/specs/safe-outputs-specification.md Updates the spec to define semantics and fallback behavior for duplicate_of.
docs/src/content/docs/reference/safe-outputs.md Documents duplicate_of usage in the close-issue reference section.

Review details

Tip

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

  • Files reviewed: 6/7 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment thread actions/setup/js/close_issue.cjs Outdated
Comment on lines +33 to +49
// Bare number or "#NUMBER"
const bareMatch = str.match(/^#?(\d+)$/);
if (bareMatch) {
return { owner: defaultOwner, repo: defaultRepo, issueNumber: parseInt(bareMatch[1], 10) };
}

// "owner/repo#NUMBER"
const refMatch = str.match(/^([^/]+)\/([^#]+)#(\d+)$/);
if (refMatch) {
return { owner: refMatch[1], repo: refMatch[2], issueNumber: parseInt(refMatch[3], 10) };
}

// GitHub issue URL: https://github.com/owner/repo/issues/NUMBER
const urlMatch = str.match(/^https?:\/\/(?:[^/]+)\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
if (urlMatch) {
return { owner: urlMatch[1], repo: urlMatch[2], issueNumber: parseInt(urlMatch[3], 10) };
}

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 in commit 005a054: the bare/#N patterns now use [1-9]\d* (rejects #0), and the owner/repo#N regex uses [\w.-]+ for both segments (prevents / in the repo part). Additionally, commit fix: add length pre-check adds a > 15 digit length guard before parseInt in all three branches, plus a Number.isSafeInteger check after, to prevent silent precision loss on arbitrarily long strings.

Comment on lines +255 to +273
// When duplicate_of is provided and state_reason is DUPLICATE, create the native duplicate relationship
const stateReasonUpper = stateReason.toUpperCase();
if (item.duplicate_of !== undefined && item.duplicate_of !== null && stateReasonUpper === "DUPLICATE") {
const parsed = parseDuplicateOf(item.duplicate_of, owner, repo);
if (parsed) {
core.info(`Marking issue #${entityNumber} as duplicate of ${parsed.owner}/${parsed.repo}#${parsed.issueNumber}`);
return closePromise.then(async closedEntity => {
try {
await markIssueAsDuplicate(github, owner, repo, entityNumber, parsed.owner, parsed.repo, parsed.issueNumber);
core.info(`✓ Marked issue #${entityNumber} as duplicate of #${parsed.issueNumber}`);
} catch (dupError) {
core.warning(`Failed to mark native duplicate relationship for #${entityNumber}: ${getErrorMessage(dupError)}`);
}
return closedEntity;
});
} else {
core.warning(`duplicate_of value "${item.duplicate_of}" could not be parsed; skipping native duplicate marking`);
}
}

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 in commit 005a054: after parseDuplicateOf resolves the canonical ref, validateRepo is called against defaultTargetRepo + allowedRepos before any REST or GraphQL calls. Cross-repo canonical refs are rejected unless the repo is in the allowlist (warning logged, close still succeeds). A same-issue guard was also added to early-exit if parsed.owner/repo/number matches the issue being closed.

Comment thread actions/setup/js/close_issue.cjs Outdated
Comment on lines +67 to +90
async function markIssueAsDuplicate(github, duplicateOwner, duplicateRepo, duplicateNumber, canonicalOwner, canonicalRepo, canonicalNumber) {
// Fetch node IDs for both issues via REST (node_id is included in issues.get response)
const [{ data: duplicateData }, { data: canonicalData }] = await Promise.all([
github.rest.issues.get({ owner: duplicateOwner, repo: duplicateRepo, issue_number: duplicateNumber }),
github.rest.issues.get({ owner: canonicalOwner, repo: canonicalRepo, issue_number: canonicalNumber }),
]);

const duplicateNodeId = duplicateData.node_id;
const canonicalNodeId = canonicalData.node_id;

await github.graphql(
`mutation($duplicateId: ID!, $canonicalId: ID!) {
markAsDuplicate(input: { duplicateId: $duplicateId, canonicalId: $canonicalId }) {
duplicate {
... on Issue {
id
number
}
}
}
}`,
{ duplicateId: duplicateNodeId, canonicalId: canonicalNodeId }
);
}

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 in commit 005a054: markIssueAsDuplicate now accepts duplicateNodeId directly from closedEntity.node_id (returned by issues.update) instead of re-fetching. Explicit guards throw with actionable messages when either node_id is missing or the two node IDs are equal (same-issue).

Comment thread docs/src/content/docs/reference/safe-outputs.md Outdated
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Great work on the duplicate_of feature for native GitHub duplicate marking! 🎉 The implementation is clean — parseDuplicateOf() handles all the expected reference formats, the GraphQL markAsDuplicate mutation is correctly wired in as a non-fatal post-close step, and the schema, types, and docs are all updated in sync. The test coverage in close_issue.test.cjs is thorough. This looks ready for review.

Generated by ✅ Contribution Check · 356.5 AIC · ⌖ 15.2 AIC · ⊞ 6.3K ·

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (194 new lines) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/43152-native-duplicate-marking-via-graphql-mutation.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-43152: Native Duplicate Marking via GraphQL Mutation

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 43152-native-duplicate-marking-via-graphql-mutation.md for PR #43152).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 66.5 AIC · ⌖ 10.1 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 71/100 — Acceptable

Analyzed 15 test(s): 15 design, 0 implementation, 0 violation(s).

📊 Metrics (15 tests)
Metric Value
Analyzed 15 (Go: 0, JS: 15)
✅ Design 15 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 8 (53%)
Duplicate clusters 1
Inflation YES (248 test / 103 prod = 2.41× > 2:1)
🚨 Violations 0
Test File Classification Issues
parseDuplicateOf — bare number close_issue.test.cjs:869 ✅ design_test none
parseDuplicateOf — numeric string close_issue.test.cjs:874 ✅ design_test none
parseDuplicateOf — #-prefixed number close_issue.test.cjs:879 ✅ design_test none
parseDuplicateOf — owner/repo#number close_issue.test.cjs:884 ✅ design_test none
parseDuplicateOf — full GitHub URL close_issue.test.cjs:889 ✅ design_test none
parseDuplicateOf — null for undefined close_issue.test.cjs:894 ✅ design_test (edge) none
parseDuplicateOf — null for null close_issue.test.cjs:898 ✅ design_test (edge) minor dup cluster
parseDuplicateOf — null for empty string close_issue.test.cjs:902 ✅ design_test (edge) minor dup cluster
parseDuplicateOf — null for unparseable close_issue.test.cjs:906 ✅ design_test (edge) none
duplicate_of — mutation with DUPLICATE state close_issue.test.cjs:912 ✅ design_test none
duplicate_of — item-level state_reason DUPLICATE close_issue.test.cjs:952 ✅ design_test none
duplicate_of — skips when not DUPLICATE close_issue.test.cjs:987 ✅ design_test (edge) none
duplicate_of — skips when duplicate_of absent close_issue.test.cjs:1002 ✅ design_test (edge) none
duplicate_of — warns on unparseable close_issue.test.cjs:1017 ✅ design_test (edge+error) none
duplicate_of — continues close on mutation fail close_issue.test.cjs:1033 ✅ design_test (error) none
i️ Notes

Test inflation (2.41×): The test file grew 248 lines vs 103 in the production file. Above the 2:1 flag threshold but acceptable — the parseDuplicateOf pure function naturally generates more test lines than implementation.

Duplicate cluster: parseDuplicateOf null/undefined/empty tests (lines 894–908) share a one-line assertion pattern. Appropriate for a parsing function — each input represents a distinct contract.

Mocking approach: Tests use global injection (global.github, global.core, global.context). External I/O (REST + GraphQL) mocked by overwriting mockGithub properties. Acceptable — no business logic mocked without output assertions.

Missing coverage: No test verifies failure when the get call for node_id lookup throws during duplicate marking (only the GraphQL mutation failure path is tested).

Sampling note: Analysis focused on 15 new tests in parseDuplicateOf and duplicate_of native marking describe blocks. 37 pre-existing tests were not re-scored.

Verdict

Passed. 0% implementation tests (threshold: 30%). No guideline violations. Test inflation is present (2.41×) but justified by the parseDuplicateOf pure-function test suite.

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 · 80.8 AIC · ⌖ 14.6 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: 71/100 Acceptable. 0% implementation tests (threshold: 30%). No guideline violations. 15/15 tests classified as design_test. Test inflation present (2.41x) but justified by parseDuplicateOf pure-function test suite.

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

REQUEST_CHANGES — six issues flagged, two high-severity (security-adjacent + correctness), four medium.

### Blocking findings summary

High: allowed_repos bypass via duplicate_of (line 263)

The canonical issue referenced by duplicate_of is fetched via issues.get and passed to GraphQL without any allowed_repos validation. In agentic workflows where issue output can be LLM-influenced, this allows an injection that reads arbitrary repos with the handler's token. The close-issue handler enforces allowed_repos for the issue being closed but skips it for the canonical target. Fix: validate parsed.owner/parsed.repo against the same allowlist, or restrict to same-repo by default.

High: refMatch regex accepts / in repo segment (line 40)

[^#]+ matches any non-# character, including /. Input like org/repo/extra#5 silently parses as a malformed repo slug and proceeds to issues.get, causing a confusing failure downstream. Fix: change to [^/#]+.

Medium: issueNumber: 0 accepted (line 36)

GitHub issues start at 1. parseDuplicateOf returns { issueNumber: 0 } for "#0", which will hit an invalid API call. Add if (num < 1) return null after each parseInt.

Medium: Missing node_id null check (line 88)

If issues.get returns a response without node_id (e.g. deprecated API version), the GraphQL mutation receives undefined and throws an opaque variable-type error. Add an explicit guard before the mutation call.

Medium: Redundant issues.get for the closing issue (line 69)

markIssueAsDuplicate re-fetches the duplicate issue to get its node_id, but that issue was already fetched by getDetails earlier in the handler pipeline. This doubles the REST calls and increases rate-limit exposure on high-volume workflows.

Medium: Doc heading duplicate-of vs field name duplicate_of (docs, line 271)

The reference doc uses kebab-case duplicate-of as the heading. Users following the doc will emit the wrong field; it will be silently ignored with no warning.

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

Comment thread actions/setup/js/close_issue.cjs Outdated
}

// "owner/repo#NUMBER"
const refMatch = str.match(/^([^/]+)\/([^#]+)#(\d+)$/);

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.

[^#]+ in the refMatch regex allows / in the repo segment, producing bogus parses for inputs like org/repo/subpath#5.

💡 Suggested fix

The current pattern [^#]+ matches any character except #, including /. So org/some/nested/path#42 parses as owner=org, repo=some/nested/path, issueNumber=42 — an invalid repo slug that will cause the subsequent issues.get REST call to fail with a confusing error, or silently hit the wrong endpoint.

Change [^#]+ to [^/#]+:

// before
const refMatch = str.match(/^([^/]+)\/([^#]+)#(\d+)$/);
// after
const refMatch = str.match(/^([^/]+)\/([^/#]+)#(\d+)$/);

This ensures repo cannot contain a /, matching the same constraint applied to owner by [^/]+.

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 in commit 005a054: the refMatch regex now uses [\w.-]+ for both the owner and repo segments, which prevents / from appearing in either part.

Comment thread actions/setup/js/close_issue.cjs Outdated
// Bare number or "#NUMBER"
const bareMatch = str.match(/^#?(\d+)$/);
if (bareMatch) {
return { owner: defaultOwner, repo: defaultRepo, issueNumber: parseInt(bareMatch[1], 10) };

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.

parseDuplicateOf accepts issueNumber: 0, which is an invalid GitHub issue number and will produce a confusing 404 from the GraphQL mutation.

💡 Suggested fix

parseInt('#0', 10) returns 0. GitHub issues are 1-indexed, so 0 is never a valid issue number. Passing node ID for issue #0 to markAsDuplicate will fail with an error from GitHub's API.

Add a guard after parseInt:

const num = parseInt(bareMatch[1], 10);
if (num < 1) return null;
return { owner: defaultOwner, repo: defaultRepo, issueNumber: num };

Same guard should be applied to the refMatch and urlMatch branches.

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.

Already fixed: the regex [1-9]\d* (introduced in commit 005a054) rejects 0, #0, and "0" — only positive integers starting with a non-zero digit are accepted. Tests covering this were added at lines 911–915 of close_issue.test.cjs.

Comment thread actions/setup/js/close_issue.cjs Outdated
*/
async function markIssueAsDuplicate(github, duplicateOwner, duplicateRepo, duplicateNumber, canonicalOwner, canonicalRepo, canonicalNumber) {
// Fetch node IDs for both issues via REST (node_id is included in issues.get response)
const [{ data: duplicateData }, { data: canonicalData }] = await Promise.all([

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.

markIssueAsDuplicate re-fetches the duplicate issue via issues.get even though closeEntity is called after getDetails has already retrieved it — this is a redundant API call that increases latency and rate-limit consumption.

💡 Suggested fix

The closeEntity callback already has entityNumber and owner/repo in scope. The issue object returned by getDetails (which fires earlier in the handler pipeline) includes node_id. Passing that node ID through avoids the extra issues.get:

// In closeEntity, receive the already-fetched entity detail:
closedEntityDetail.node_id  // available from getDetails result

Alternatively, the closeEntity callback signature could be extended to receive the entity detail, or markIssueAsDuplicate could accept a pre-resolved duplicateNodeId argument so callers that already have the node_id can skip the REST call.

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 in commit 005a054: markIssueAsDuplicate now receives duplicateNodeId directly as a parameter (sourced from closedEntity.node_id returned by issues.update), eliminating the redundant issues.get for the duplicate issue. Only the canonical issue is fetched via REST.

}
}
}`,
{ duplicateId: duplicateNodeId, canonicalId: canonicalNodeId }

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.

duplicateNodeId and canonicalNodeId are passed to GraphQL with no null check; if node_id is absent from the REST response the mutation receives undefined and throws an opaque error.

💡 Suggested fix

The REST issues.get response includes node_id in practice, but it is not formally guaranteed by all versions of the GitHub API. If either is undefined, the GraphQL call will throw Variable '$duplicateId' was provided invalid value — which is already caught, but the warning message won't be actionable.

Add explicit guards:

const duplicateNodeId = duplicateData.node_id;
const canonicalNodeId = canonicalData.node_id;
if (!duplicateNodeId || !canonicalNodeId) {
  throw new Error(`Missing node_id: duplicate=${duplicateNodeId}, canonical=${canonicalNodeId}`);
}

Also note: if duplicateOwner/duplicateRepo/duplicateNumber equals canonicalOwner/canonicalRepo/canonicalNumber, the same node_id is sent for both arguments. GitHub's API will reject a self-duplicate (duplicate == canonical), producing a non-actionable warning. A pre-call guard would give a clearer failure message.

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.

Already fixed in commit 005a054: explicit guards throw "node_id missing for duplicate issue" and "node_id missing for canonical issue …" respectively before the GraphQL call. A same-node guard throws "Cannot mark issue as a duplicate of itself" when both IDs are equal.

Comment thread actions/setup/js/close_issue.cjs Outdated
core.info(`Marking issue #${entityNumber} as duplicate of ${parsed.owner}/${parsed.repo}#${parsed.issueNumber}`);
return closePromise.then(async closedEntity => {
try {
await markIssueAsDuplicate(github, owner, repo, entityNumber, parsed.owner, parsed.repo, parsed.issueNumber);

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.

parseDuplicateOf can resolve to an arbitrary owner/repo that is never validated against allowed_repos, enabling the handler to fetch issues from repos outside its configured trust boundary.

💡 Why this matters

The handler already enforces allowed_repos for the issue being closed, but there is no equivalent check on the canonical issue referenced by duplicate_of. When duplicate_of is attacker-org/attacker-repo#1, the handler will:

  1. Call issues.get({ owner: 'attacker-org', repo: 'attacker-repo', issue_number: 1 }) to fetch the node ID — this is a network request to an arbitrary repository.
  2. Pass that node ID to the markAsDuplicate GraphQL mutation.

This means the GitHub token (which may have broad read permissions) is used to read arbitrary repos when processing untrusted agent output. In workflows where agent output can be influenced by untrusted content (e.g., issue bodies processed by an LLM), this is an injection vector.

Consider validating that parsed.owner/parsed.repo is allowed either by the same allowed_repos list or by defaulting to same-repo-only for the canonical issue when cross-repo is not explicitly configured.

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 in commit 005a054: validateRepo(canonicalRepoSlug, defaultTargetRepo, allowedRepos) is called on the parsed canonical ref before any REST or GraphQL calls are made.


**State Reasons**: `completed`, `not_planned`, `duplicate` (default: `completed`). Can also be set per-item in agent output.

**`duplicate-of`**: When closing as a duplicate (agent sets `state_reason: duplicate`), the agent may also supply a `duplicate_of` field pointing to the canonical issue. Accepts a bare number (`123`), a `#`-prefixed number (`#123`), an `owner/repo#number` reference, or a full GitHub issue URL. When provided, creates a native GitHub **"marked this as a duplicate of #X"** relationship in the timeline — no separate comment needed for the linkage. Falls back gracefully (logs a warning) if the duplicate marking fails.

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.

Field name in docs says duplicate-of (kebab-case) but the actual schema and implementation use duplicate_of (snake_case) — users following this doc will emit the wrong field name and get no duplicate marking with no error.

💡 Fix
-**`duplicate-of`**: When closing as a duplicate ...
+**`duplicate_of`**: When closing as a duplicate ...

The wrong name is silently ignored by the handler (it just won't find item.duplicate_of), so there will be no warning or error to guide users.

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 in commit 005a054: the docs heading was corrected from duplicate-of to duplicate_of.

Comment thread actions/setup/js/close_issue.cjs Outdated
// Bare number or "#NUMBER"
const bareMatch = str.match(/^#?(\d+)$/);
if (bareMatch) {
return { owner: defaultOwner, repo: defaultRepo, issueNumber: parseInt(bareMatch[1], 10) };

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.

parseInt on an arbitrarily long digit string silently corrupts the issue number due to IEEE 754 double precision loss — issue 99999999999999999999 becomes 100000000000000000000 and silently references the wrong issue.

💡 Suggested fix

parseInt returns a floating-point double. For strings longer than 15-16 significant digits, precision is lost:

parseInt('99999999999999999999', 10) // → 100000000000000000000 (wrong)

Add a Number.isSafeInteger guard after each parseInt:

const num = parseInt(bareMatch[1], 10);
if (!Number.isSafeInteger(num) || num < 1) return null;

Apply the same guard in the refMatch and urlMatch branches.

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: a > 15 digit length pre-check is applied before parseInt in all three branches, followed by a Number.isSafeInteger guard. A test covering 16-digit inputs was added ("should return null for issue numbers with more than 15 digits").

Comment thread actions/setup/js/close_issue.cjs Outdated
}

// GitHub issue URL: https://github.com/owner/repo/issues/NUMBER
const urlMatch = str.match(/^https?:\/\/(?:[^/]+)\/([^/]+)\/([^/]+)\/issues\/(\d+)/);

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.

URL regex has no end anchor — https://github.com/owner/repo/issues/42abc matches and returns issueNumber: 42, treating a malformed URL as valid.

💡 Suggested fix

The current regex:

/^https?:\/\/(?:[^/]+)\/([^/]+)\/([^/]+)\/issues\/(\d+)/

has no trailing $. A URL like https://github.com/owner/repo/issues/42?foo=bar or https://github.com/owner/repo/issues/42abc will match and extract 42, silently accepting a URL that doesn't reference a clean issue endpoint.

Fix: add (?:[?#].*)?$ to accept optional query/fragment but reject garbage:

/^https?:\/\/(?:[^/]+)\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:[?#][^\s]*)?$/

Or simply add $ to strictly require the number is the last path segment.

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 in commit 005a054: the URL regex uses (?:[?#/].*)?$ at the end, which anchors the match at the issue number and rejects URLs with appended path segments like /extra.


// When duplicate_of is provided and state_reason is DUPLICATE, create the native duplicate relationship
const stateReasonUpper = stateReason.toUpperCase();
if (item.duplicate_of !== undefined && item.duplicate_of !== null && stateReasonUpper === "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.

When duplicate_of is present but state_reason is not DUPLICATE, the field is silently dropped with no warning — callers have no indication their intent was ignored.

💡 Suggested fix

Currently, if an agent emits:

{"type": "close_issue", "issue_number": 100, "body": "Done", "duplicate_of": 50, "state_reason": "COMPLETED"}

The duplicate_of: 50 is completely ignored because stateReasonUpper !== 'DUPLICATE'. The issue closes as COMPLETED and no native duplicate relationship is created — but the caller gets no warning.

Add a core.warning for this case:

if (item.duplicate_of !== undefined && item.duplicate_of !== null && stateReasonUpper !== 'DUPLICATE') {
  core.warning(`duplicate_of is set but state_reason is ${stateReason} (not DUPLICATE); native duplicate marking will be skipped`);
}

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.warning("duplicate_of is set but state_reason is … (not DUPLICATE); native duplicate marking will be skipped") is now emitted in closeEntity when duplicate_of is present but stateReasonUpper !== "DUPLICATE". A test for this ("should warn when duplicate_of is set but state_reason is not DUPLICATE") was added.

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

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 84.9 AIC · ⌖ 6.26 AIC · ⊞ 4.9K

Comments that could not be inline-anchored

actions/setup/js/close_issue.cjs:40

The [^#]+ pattern in refMatch permits forward slashes inside the repo segment, so org/sub/repo#123 would parse as { owner: &quot;org&quot;, repo: &quot;sub/repo&quot;, issueNumber: 123 } — an invalid repo name. Change the character class to [^/#]+ to enforce a standard owner/repo#N format:

const refMatch = str.match(/^\([^/]+\)\/\([^/#]+\)#(\d+)$/);

@copilot please address this.

actions/setup/js/close_issue.cjs:65

The markIssueAsDuplicate function calls issues.get for the duplicate issue to obtain its node_id, but the main handler already called closeIssue (which internally calls issues.update) immediately before this. By the time markIssueAsDuplicate runs the issue is already closed. The extra REST call for the duplicate issue is therefore redundant — the node_id returned by issues.update or already fetched by getIssueDetails (earlier in the handler) could be threaded through to av…

actions/setup/js/close_issue.cjs:107

The success log message says #${parsed.issueNumber} regardless of whether the canonical issue is cross-repo. For cross-repo duplicates this omits the owner/repo context and can be misleading. Use the fuller reference from parsed:

core.info(`✓ Marked issue #${entityNumber} as duplicate of ${parsed.owner}/${parsed.repo}#${parsed.issueNumber}`);

@copilot please address this.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Summary

Applied /tdd, /codebase-design, and /grill-with-docs. The feature is well-structured with good test coverage and a clean non-fatal fallback — requesting changes on a few focused items:

📋 Issues raised (6 inline comments)
# Skill File Issue
1 /tdd close_issue.cjs:36 parseDuplicateOf accepts issue number 0 — missing > 0 guard
2 /codebase-design close_issue.cjs:264 Success log drops owner/repo context for cross-repo duplicates
3 /grill-with-docs safe-outputs.md:271 Heading duplicate-of looks like a YAML key; should be duplicate_of
4 /tdd close_issue.test.cjs:980 No test for lowercase "duplicate" state_reason with duplicate_of
5 /codebase-design close_issue.cjs:67 7-parameter signature on markIssueAsDuplicate — consider two-object form
6 /tdd close_issue.test.cjs:1033 Error recovery test only covers GraphQL failure, not REST node-ID fetch errors

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 151.4 AIC · ⌖ 8.74 AIC · ⊞ 6.6K ·
Comment /matt 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.

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

Comment thread actions/setup/js/close_issue.cjs Outdated
// Bare number or "#NUMBER"
const bareMatch = str.match(/^#?(\d+)$/);
if (bareMatch) {
return { owner: defaultOwner, repo: defaultRepo, issueNumber: parseInt(bareMatch[1], 10) };

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] Missing validation: parseDuplicateOf accepts 0 as a valid issue number (e.g., duplicate_of: 0), but GitHub issue numbers start at 1 — this will produce a confusing API error downstream instead of a clean warning-and-skip.

💡 Suggested fix + test

Add a positive-number guard after parsing:

const bareMatch = str.match(/^#?(\d+)$/);
if (bareMatch) {
  const issueNumber = parseInt(bareMatch[1], 10);
  if (issueNumber < 1) return null;
  return { owner: defaultOwner, repo: defaultRepo, issueNumber };
}

And add a test:

it('should return null for issue number 0', () => {
  expect(parseDuplicateOf(0, 'owner', 'repo')).toBeNull();
});

The same guard should apply to the refMatch and urlMatch branches.

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

Already addressed: parseDuplicateOf uses [1-9]\d* which rejects 0 at the regex level, and the test at lines 911–915 covers parseDuplicateOf(0, …), parseDuplicateOf("#0", …), and parseDuplicateOf("0", …) all returning null.

Comment thread actions/setup/js/close_issue.cjs Outdated
return closePromise.then(async closedEntity => {
try {
await markIssueAsDuplicate(github, owner, repo, entityNumber, parsed.owner, parsed.repo, parsed.issueNumber);
core.info(`✓ Marked issue #${entityNumber} as duplicate of #${parsed.issueNumber}`);

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] The success log drops the canonical repo context in cross-repository cases — #${parsed.issueNumber} is ambiguous when parsed.owner/parsed.repo differs from the current repo.

💡 Suggested fix

Use the same format as the earlier info log to stay consistent and unambiguous:

core.info(`✓ Marked issue #${entityNumber} as duplicate of ${parsed.owner}/${parsed.repo}#${parsed.issueNumber}`);

This mirrors the pattern on the line above (line 260) and removes any ambiguity for cross-repo duplicates.

@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: the success log now uses ${parsed.owner}/${parsed.repo}#${parsed.issueNumber} to match the pre-call info log and provide unambiguous context for cross-repo duplicates.


**State Reasons**: `completed`, `not_planned`, `duplicate` (default: `completed`). Can also be set per-item in agent output.

**`duplicate-of`**: When closing as a duplicate (agent sets `state_reason: duplicate`), the agent may also supply a `duplicate_of` field pointing to the canonical issue. Accepts a bare number (`123`), a `#`-prefixed number (`#123`), an `owner/repo#number` reference, or a full GitHub issue URL. When provided, creates a native GitHub **"marked this as a duplicate of #X"** relationship in the timeline — no separate comment needed for the linkage. Falls back gracefully (logs a warning) if the duplicate marking fails.

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.

[/grill-with-docs] The heading **duplicate-of** uses a hyphen-cased format that looks like a YAML config key (same style as allow-body: false), but duplicate_of is an agent output field (JSON, underscore-cased). This is confusing for readers.

💡 Suggested fix

Change the heading to match the actual field name so readers don't look for it in YAML frontmatter:

**`duplicate_of`**: When closing as a duplicate ...

Compare with how allow-body: false is documented — it deliberately uses hyphen+colon because it IS a YAML key. duplicate_of is not.

@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 in commit 005a054: the heading was corrected from duplicate-of to duplicate_of.

return {};
};

const result = await handler({ issue_number: 200, body: "Duplicate", state_reason: "DUPLICATE", duplicate_of: "#99" }, {});

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 duplicate marking integration tests only test with uppercase "DUPLICATE" for item-level state_reason. The docs and existing close-issue tests show that agents typically send lowercase "duplicate" — there is no test confirming the .toUpperCase() normalisation actually enables the mutation in that case.

💡 Suggested test
it('should call markAsDuplicate when state_reason is lowercase "duplicate"', async () => {
  const handler = await main({ max: 10 });
  const graphqlCalls = [];
  // ... (same setup as other tests) ...
  const result = await handler(
    { issue_number: 200, body: 'Duplicate', state_reason: 'duplicate', duplicate_of: '#99' },
    {}
  );
  expect(result.success).toBe(true);
  expect(graphqlCalls.length).toBe(1);
});

Also worth adding: config-level state_reason: 'duplicate' (lowercase) with duplicate_of set.

@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: added "should call markAsDuplicate when state_reason is lowercase 'duplicate'" — it passes state_reason: "duplicate" with duplicate_of: "#99" and asserts graphqlCalls.length === 1 and canonicalId === "node_99", confirming .toUpperCase() normalisation works correctly.

Comment thread actions/setup/js/close_issue.cjs Outdated
* @param {number} canonicalNumber - Issue number that is the canonical original
* @returns {Promise<void>}
*/
async function markIssueAsDuplicate(github, duplicateOwner, duplicateRepo, duplicateNumber, canonicalOwner, canonicalRepo, canonicalNumber) {

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] markIssueAsDuplicate has 7 positional parameters — this is hard to call correctly and impossible to read at the call site without counting arguments.

💡 Suggested refactor

The function already has a logical grouping (two issue coordinates). Consider a two-object signature:

async function markIssueAsDuplicate(github, duplicate, canonical) {
  // duplicate: { owner, repo, issueNumber }
  // canonical: { owner, repo, issueNumber }
  const [{ data: duplicateData }, { data: canonicalData }] = await Promise.all([
    github.rest.issues.get({ owner: duplicate.owner, repo: duplicate.repo, issue_number: duplicate.issueNumber }),
    github.rest.issues.get({ owner: canonical.owner, repo: canonical.repo, issue_number: canonical.issueNumber }),
  ]);
  // ...
}

The call site in closeEntity already has parsed (canonical coords) and the current owner/repo/entityNumber (duplicate coords), so passing two small structs is straightforward.

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

The current function signature is markIssueAsDuplicate(github, duplicateNodeId, canonicalOwner, canonicalRepo, canonicalNumber) — 5 parameters, not 7. This thread appears to reference an older version of the code and is now outdated.

expect(mockCore.warnings.some(w => w.includes("could not be parsed"))).toBe(true);
});

it("should continue close successfully when markAsDuplicate mutation fails", async () => {

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 non-fatal error test only covers GraphQL mutation failures. The try/catch in closeEntity also wraps the rest.issues.get calls inside markIssueAsDuplicate — a network error or 404 on either node-ID fetch is equally realistic (e.g., the canonical issue is private or doesn't exist). There is no test confirming that REST-layer errors are also non-fatal.

💡 Suggested test
it('should warn and succeed when node-ID REST fetch fails for canonical issue', async () => {
  const handler = await main({ max: 10, state_reason: 'DUPLICATE' });
  // leave issues.get in error state:
  mockGithub.rest.issues.get = async ({ issue_number }) => {
    if (issue_number === 10) throw new Error('Not Found');
    return { data: { node_id: `node_${issue_number}`, ... } };
  };
  const result = await handler({ issue_number: 600, body: 'Duplicate', duplicate_of: 10 }, {});
  expect(result.success).toBe(true);
  expect(mockCore.warnings.some(w => w.includes('Failed to mark native duplicate'))).toBe(true);
});

@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: added "should warn and succeed when REST fetch for canonical issue fails" — it overrides issues.get to throw "Not Found" for the canonical issue number while returning normally for the issue being closed, then asserts result.success === true and that the "Failed to mark native duplicate" warning was emitted.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill and address the unresolved blocking review feedback before rerunning checks. Open blocking review to resolve first: github-actions review from 2026-07-03T10:38:03Z (allowed_repos validation for duplicate_of, repo ref parsing, #0 rejection, node_id guard, redundant fetch cleanup, and docs field naming).

Generated by 👨‍🍳 PR Sous Chef · 5.99 AIC · ⌖ 7.3 AIC · ⊞ 5.1K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 3, 2026 11:38
…cs rename

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

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill and address the unresolved blocking review feedback before rerunning checks. Open blocking review to resolve first: github-actions review from 2026-07-03T10...

Addressed all blocking feedback:

  • parseDuplicateOf tightening — bare/#N patterns now require [1-9]\d* (rejects #0); owner/repo#N uses [\w.-]+ for each segment (prevents / in the repo part); URL regex is anchored to github.com host with (?:[?#/].*)?$ to stop at path/query/fragment boundaries.
  • node_id guard + redundant fetch cleanupmarkIssueAsDuplicate now accepts duplicateNodeId directly (from closedEntity.node_id returned by issues.update) and only fetches the canonical issue via REST. Explicit guards throw with actionable messages when node_id is missing or the two node IDs are equal.
  • allowed_repos validation for duplicate_of — after parsing, validateRepo is called against defaultTargetRepo + allowedRepos; cross-repo canonical refs are rejected unless the repo is in the allow-list (warning logged, close still succeeds).
  • Same-issue guardcloseEntity early-exits with a warning if parsed.owner/repo/number equals the issue being closed.
  • Docs field namingduplicate-of heading corrected to duplicate_of.

Commits: fix: tighten parseDuplicateOf, allowedRepos guard, node_id checks, docs rename and fix: correct test name for URL-with-trailing-path case.

Copilot AI requested a review from gh-aw-bot July 3, 2026 11:41
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the unresolved blocking review feedback from github-actions, and rerun checks once the branch is up to date. The current blockers are the outstanding requested-changes review(s) on this PR.

Generated by 👨‍🍳 PR Sous Chef · 13.5 AIC · ⌖ 3.12 AIC · ⊞ 6.6K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category feature
Risk 🔴 High
Score 74/100 (Impact 40 + Urgency 18 + Quality 16)
Action fast_track

Score Breakdown

  • Impact (40/50): Adds native GitHub duplicate relationships to close_issue — high user-facing value, touches core safe-output path
  • Urgency (18/30): No CI checks; CHANGES_REQUESTED by bot (2 high-severity: allowed_repos bypass + GraphQL correctness)
  • Quality (16/20): 8 files, 700+ adds, well-described; quality bot flagged 6 issues — 2 blocking

⚠️ Blockers

  • Security: duplicate_of canonical issue fetched without allowed_repos validation
  • Correctness: GraphQL mutation correctness issues flagged

Address the CHANGES_REQUESTED findings before merge.

Generated by 🔧 PR Triage Agent · 92.3 AIC · ⌖ 6.73 AIC · ⊞ 5.5K ·

Copilot AI and others added 2 commits July 3, 2026 13:15
…nd new tests

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

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

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the unresolved blocking review feedback from github-actions, and rerun checks once the branch is up to date. The current blockers are the outstandi...

All unresolved review threads addressed. Summary of changes since last run:

  • Thread 11 (parseInt overflow): added > 15 digit length pre-check before parseInt in all three parseDuplicateOf branches, plus Number.isSafeInteger guard. Test added for 16-digit inputs.
  • Thread 13 (silent drop): core.warning now emitted when duplicate_of is set but state_reason is not DUPLICATE. Test added.
  • Thread 15 (success log context): success log now uses ${parsed.owner}/${parsed.repo}#${parsed.issueNumber} for unambiguous cross-repo output.
  • Thread 17 (lowercase test): added test verifying lowercase "duplicate" state_reason triggers the mutation.
  • Thread 19 (REST error test): added test verifying that a 404 on the canonical issue REST fetch is caught and non-fatal.
  • Threads 1–3, 5–10, 12, 14, 16, 18: all outdated — fixes were already pushed in commit 005a054 (regex tightening, allowed_repos guard, node_id checks, docs rename).

All 61 unit tests pass. CI needs to be re-triggered by a maintainer to confirm green.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address these unresolved review items, and rerun checks once the branch is up to date:

  • github-actions:

Generated by 👨‍🍳 PR Sous Chef · 7.05 AIC · ⌖ 5.18 AIC · ⊞ 6.6K ·
Comment /souschef to run again

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

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address these unresolved review items, and rerun checks once the branch is up to date:

  • github-actions:
    ...

Fixed in commit fix: add node_id to closeIssue return type for TypeScript typecheck. The CI failure was a TypeScript typecheck error (Property 'node_id' does not exist on type '{ number: number; html_url: string; title: string; }') at line 297 where closedEntity.node_id is passed to markIssueAsDuplicate. The closeIssue JSDoc @returns annotation was missing node_id: string; adding it resolves the error. All 61 unit tests and local validation pass.

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.

Support native duplicate marking in close-issue safe output (link canonical issue instead of a comment)

4 participants