Skip to content

feat: add close-older-pull-requests to create-pull-request safe output#36702

Merged
pelikhan merged 5 commits into
mainfrom
copilot/reduce-placeholder-waste
Jun 3, 2026
Merged

feat: add close-older-pull-requests to create-pull-request safe output#36702
pelikhan merged 5 commits into
mainfrom
copilot/reduce-placeholder-waste

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jun 3, 2026

WIP/placeholder PRs were accumulating unboundedly — chaos-pr-bundle-fuzzer alone had 10+ open PRs at 0% merge rate, generating reviewer noise with no signal value.

Adds a close-older-pull-requests option to create-pull-request (analogous to the existing close-older-issues on create-issue) that auto-closes older open PRs from the same workflow when a new one is created.

New runtime handler

  • close_older_pull_requests.cjs — mirrors close_older_issues.cjs; uses is:pr search qualifier and pulls.update(state: "closed") to close; comments via issues.createComment (GitHub uses issues endpoint for PR comments); capped at 10 closures per run

Go config wiring

  • Added CloseOlderPullRequests *string and CloseOlderKey string to CreatePullRequestsConfig
  • "close-older-pull-requests" registered in BoolFields so YAML true/false is parsed correctly
  • safe_outputs_config.go: emits close_older_pull_requests + close_older_key into handler config
  • safe_output_handlers.go: adds issues: write permission when close-older-pull-requests is enabled with fallback-as-issue: false (already included when true)

Integration

  • create_pull_request.cjs reads config flags, reads GH_AW_CALLER_WORKFLOW_ID for reusable-workflow isolation, and calls closeOlderPullRequests after PR creation (best-effort; errors logged, not fatal)

Usage

- type: create-pull-request
  close-older-pull-requests: true   # auto-close previous open PRs from this workflow
  close-older-key: my-custom-key    # optional: explicit deduplication key

Applied immediately

  • chaos-pr-bundle-fuzzer.md: enabled close-older-pull-requests: true to stop backlog accumulation
  • frontmatter-full.md: documented new fields

Copilot AI and others added 2 commits June 3, 2026 19:45
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 changed the title [WIP] Add policies to reduce placeholder PR waste feat: add close-older-pull-requests to create-pull-request safe output Jun 3, 2026
Copilot AI requested a review from gh-aw-bot June 3, 2026 19:59
@pelikhan pelikhan marked this pull request as ready for review June 3, 2026 20:06
Copilot AI review requested due to automatic review settings June 3, 2026 20:06
Copy link
Copy Markdown
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

Adds a new close-older-pull-requests option to the create-pull-request safe-output handler to automatically close older open PRs when a new PR is created, reducing accumulation of low-signal placeholder/WIP PRs. The PR also wires the new config through the Go compiler and documents the new frontmatter fields.

Changes:

  • Add close-older-pull-requests + close-older-key fields to create-pull-request config, compile them into handler JSON, and adjust permissions.
  • Implement close_older_pull_requests.cjs (+ tests) and invoke it from create_pull_request.cjs after successful PR creation.
  • Document the new settings and enable them for chaos-pr-bundle-fuzzer.
Show a summary per file
File Description
pkg/workflow/safe_outputs_config.go Emits close_older_pull_requests + close_older_key into handler config JSON.
pkg/workflow/safe_output_handlers.go Adds issues: write permission when close-older PR closure is enabled.
pkg/workflow/create_pull_request.go Adds config fields and parsing for close-older-pull-requests and close-older-key.
docs/src/content/docs/reference/frontmatter-full.md Documents close-older-pull-requests and close-older-key under create-pull-request.
actions/setup/js/create_pull_request.cjs Calls closeOlderPullRequests after creating a PR; reads caller workflow ID + close-key config.
actions/setup/js/close_older_pull_requests.cjs New runtime helper to search/comment/close older PRs (shares core logic with close-older entities).
actions/setup/js/close_older_pull_requests.test.cjs Unit tests for search/filter/close behavior and caps.
.github/workflows/chaos-pr-bundle-fuzzer.md Enables close-older-pull-requests: true to prevent backlog growth.

Copilot's findings

Tip

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

  • Files reviewed: 8/8 changed files
  • Comments generated: 3

Comment on lines +2195 to +2202
// Close older pull requests if enabled (best-effort: errors are logged but do not fail the workflow)
if (closeOlderPullRequestsEnabled) {
if (workflowId || rawCloseOlderKey) {
const searchKey = rawCloseOlderKey ? `gh-aw-close-key: ${rawCloseOlderKey}` : `workflow-id: ${workflowId}`;
core.info(`Attempting to close older pull requests for ${repoParts.owner}/${repoParts.repo}#${pullRequest.number} using ${searchKey}`);
try {
const closedPRs = await closeOlderPullRequests(github, repoParts.owner, repoParts.repo, workflowId, { number: pullRequest.number, html_url: pullRequest.html_url }, workflowName, runUrl, callerWorkflowId, rawCloseOlderKey);
if (closedPRs.length > 0) {
Comment on lines +20 to +22
// isCloseOlderPullRequestsEnabled returns true when close-older-pull-requests is
// configured and not explicitly set to false or a GitHub Actions expression.
// Used for compile-time permission calculation.
Comment on lines +4741 to +4746
# Optional explicit deduplication key for close-older matching. When set, a `<!--
# gh-aw-close-key: <value> -->` marker is embedded in the PR body and used as the
# primary key for searching and filtering older pull requests instead of the
# workflow-id markers. This gives deterministic isolation across caller workflows
# and is stable across workflow renames. The value is normalized to identifier
# style (lowercase alphanumeric, dashes, underscores).
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions github-actions Bot mentioned this pull request Jun 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

🧪 Test Quality Sentinel Report

Test Quality Score: 81/100 — Excellent

Analyzed 16 JavaScript tests (vitest): 16 design tests, 0 implementation tests, 0 guideline violations. Mild test inflation detected (459 test lines vs 224 production lines, ratio ≈ 2.05:1).

📊 Metrics & Test Classification (16 tests analyzed)
Metric Value
New/modified tests analyzed 16
✅ Design tests (behavioral contracts) 16 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 11 (69%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes — 459 test lines / 224 production lines ≈ 2.05:1
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
should search for pull requests with workflow-id marker close_older_pull_requests.test.cjs ✅ Design Verifies result shape + query params sent to GitHub API
should exclude the newly created pull request close_older_pull_requests.test.cjs ✅ Design Edge case: current PR excluded from results
should return empty array if no workflow-id provided close_older_pull_requests.test.cjs ✅ Design Edge case: guard on empty workflowId
should exclude issues (only return pull requests) close_older_pull_requests.test.cjs ✅ Design Edge case: issue vs PR discrimination
should return empty array if no results close_older_pull_requests.test.cjs ✅ Design Edge case: empty search results
should exclude PRs whose body does not contain the exact marker close_older_pull_requests.test.cjs ✅ Design Edge case: exact-match filtering (substring false positives)
should filter by gh-aw-workflow-call-id when callerWorkflowId is provided close_older_pull_requests.test.cjs ✅ Design Verifies caller-scoped filtering
should use close-key marker as primary search term when closeOlderKey is provided close_older_pull_requests.test.cjs ✅ Design Verifies alternative key search + query string
should return empty array when neither workflowId nor closeOlderKey is provided close_older_pull_requests.test.cjs ✅ Design Edge case: both identifiers absent
should add comment to pull request close_older_pull_requests.test.cjs ✅ Design Verifies return value and API payload
should close pull request close_older_pull_requests.test.cjs ✅ Design Verifies state: "closed" sent to API
should generate closing message close_older_pull_requests.test.cjs ✅ Design Verifies message content includes PR URL, number, workflow name
should close older pull requests successfully close_older_pull_requests.test.cjs ✅ Design Happy path integration: comment posted + PR closed
should limit to MAX_CLOSE_COUNT pull requests close_older_pull_requests.test.cjs ✅ Design Boundary: enforces MAX_CLOSE_COUNT cap + warning emitted
should continue on error for individual pull requests close_older_pull_requests.test.cjs ✅ Design Error path: partial failure continues, error logged
should return empty array if no older pull requests found close_older_pull_requests.test.cjs ✅ Design Edge case: no older PRs, info message emitted

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests
  • 🟨 JavaScript (*.test.cjs): 16 tests (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The mocking pattern is appropriate — all mocked objects are external GitHub REST API calls (github.rest.*), which is the correct boundary to stub in unit tests for this codebase.

i️ Minor note on test inflation: The test file is 2.05× the size of the production file (459 vs 224 lines). This is largely because the test scaffolding (mock setup, realistic fixture objects) is verbose relative to the implementation. The tests themselves cover meaningful scenarios — this is not copy-paste inflation.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §26909970993

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1.1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 81/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 16 tests verify behavioral contracts against the GitHub REST API.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnose, /tdd, and /zoom-out — clean implementation that closely mirrors close_older_issues; flagging one misleading log string and a small test coverage gap.

📋 Key Themes & Highlights

Key Themes

  • Log message inconsistency: workflow-id: in the info log doesn't match the actual gh-aw-workflow-id: marker format — misleading during debugging
  • Test coverage gap: closeOlderPullRequests integration tests don't exercise the closeOlderKey forwarding path end-to-end
  • Permission over-grant note: isCloseOlderPullRequestsEnabled conservatively grants issues: write for any non-false expression value, including unresolved GA expressions — intentional but worth documenting

Positive Highlights

  • ✅ Strong unit test coverage for searchOlderPullRequests across all meaningful filter variants
  • ✅ Clean delegation to closeOlderEntities via closure — avoids duplicating the rate-limit / error-continue loop
  • ✅ Best-effort error handling in create_pull_request.cjs — failures log a warning but don't abort the workflow
  • ✅ Permission grant correctly scoped to issues: write (comment posting via issues API) rather than pull-requests: write
  • ✅ Documentation in frontmatter-full.md is thorough and accurate

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.5M

// Close older pull requests if enabled (best-effort: errors are logged but do not fail the workflow)
if (closeOlderPullRequestsEnabled) {
if (workflowId || rawCloseOlderKey) {
const searchKey = rawCloseOlderKey ? `gh-aw-close-key: ${rawCloseOlderKey}` : `workflow-id: ${workflowId}`;
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.

[/diagnose] Log message uses wrong marker name — workflow-id: should be gh-aw-workflow-id: to match the actual HTML comment marker embedded in PR bodies.

💡 Suggested fix
// Before
const searchKey = rawCloseOlderKey ? `gh-aw-close-key: ${rawCloseOlderKey}` : `workflow-id: ${workflowId}`;

// After
const searchKey = rawCloseOlderKey ? `gh-aw-close-key: ${rawCloseOlderKey}` : `gh-aw-workflow-id: ${workflowId}`;

This is a log-only string so it has no functional impact, but it is misleading when debugging why close-older didn't match — the log message would show a different key format than what's actually searched for.

expect(global.core.info).toHaveBeenCalledWith("✓ No older pull requests found to close - operation complete");
});
});
});
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 closeOlderPullRequests integration suite doesn't include a test for the closeOlderKey forwarding path. The unit-level coverage in searchOlderPullRequests is good, but there's no test verifying that when closeOlderKey is passed to closeOlderPullRequests it reaches the search call with the correct query.

💡 Suggested test case to add
it('should forward closeOlderKey to search when provided', async () => {
  mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
    data: { items: [] },
  });

  const newPR = { number: 200, html_url: 'https://github.com/owner/repo/pull/200' };
  await closeOlderPullRequests(
    mockGithub, 'owner', 'repo', 'some-workflow', newPR,
    'My Workflow', 'https://github.com/owner/repo/actions/runs/1',
    undefined, 'my-close-key'
  );

  expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledWith({
    q: 'repo:owner/repo is:pr is:open "gh-aw-close-key: my-close-key" in:body',
    per_page: 50,
  });
});

This guards against a regression where the closure over closeOlderKey in closeOlderPullRequests stops forwarding the value.

}

// CreatePullRequestsConfig holds configuration for creating GitHub pull requests from agent output
type CreatePullRequestsConfig struct {
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.

[/zoom-out] isCloseOlderPullRequestsEnabled treats any non-"false"/non-"0" string value (including GitHub Actions expressions like ${{ inputs.enable_close }}) as "enabled" at compile time. This means workflows using a templatable value always compile with issues: write granted, even when the runtime expression evaluates to false.

This is consistent with how other templatable fields (e.g. draft, auto-merge) are handled elsewhere in the codebase — so no change needed if over-granting is intentional/acceptable. Worth calling out explicitly in the comment so future maintainers understand the tradeoff.

@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Jun 3, 2026

@copilot merge main and recompile
Review all comments
apply copilot-review skill

…ody; add schema entry for close-older-pull-requests

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jun 3, 2026

@copilot merge main and recompile
Review all comments
apply copilot-review skill

Done. Here's what was addressed:

Marker embedding fixed (create_pull_request.cjs):

  • Added generateWorkflowCallIdMarker, generateCloseKeyMarker, and normalizeCloseOlderKey imports (matching create_issue.cjs)
  • Now embeds <!-- gh-aw-workflow-call-id: ... --> marker in the PR body when GH_AW_CALLER_WORKFLOW_ID is set — fixes the issue where reusable-workflow callers would filter each other out
  • Now normalizes close-older-key and embeds <!-- gh-aw-close-key: ... --> marker in the PR body — fixes the issue where close-older-key matching never found older PRs
  • Uses the normalized key (not raw) in the closeOlderPullRequests call

Doc comment corrected (create_pull_request.go): Updated isCloseOlderPullRequestsEnabled comment to accurately state that any non-empty value other than "false" or "0" — including expression strings — is treated as enabled.

Docs now accurate (frontmatter-full.md): No change needed; the docs correctly describe the behavior now that the marker embedding is implemented.

Schema fix (main_workflow_schema.json): Added close-older-pull-requests and close-older-key to the JSON schema for create-pull-request — this was missing and caused the compiler to reject the field. All 239 workflows now compile cleanly.

Main merged at fe76b12. Recompiled: 239/239 workflows pass. All JS and Go tests pass.

Copilot AI requested a review from pelikhan June 3, 2026 20:49
@pelikhan pelikhan merged commit c1db7d9 into main Jun 3, 2026
31 checks passed
@pelikhan pelikhan deleted the copilot/reduce-placeholder-waste branch June 3, 2026 20:57
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.

[deep-report] [quick-win] Reduce Copilot [WIP] placeholder-PR waste (cap concurrent + auto-close stale)

4 participants