From 904f9014f5c6e015f22042c7317fc6e8535ca345 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Wed, 14 Jan 2026 11:20:00 -0500 Subject: [PATCH 1/2] feat: Add milestone automation for merged PRs --- .github/workflows/milestone-automation.yml | 95 ++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/milestone-automation.yml diff --git a/.github/workflows/milestone-automation.yml b/.github/workflows/milestone-automation.yml new file mode 100644 index 00000000..ae0843d5 --- /dev/null +++ b/.github/workflows/milestone-automation.yml @@ -0,0 +1,95 @@ +name: Milestone Automation + +on: + pull_request_target: + types: + - closed + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Assigns merged PRs to the appropriate milestone + assign-milestone: + name: Assign milestone to merged PR + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + issues: write + # Only run on merged PRs + if: github.event.pull_request.merged == true + + steps: + - name: Assign or update milestone + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const pr = context.payload.pull_request; + const prNumber = pr.number; + const currentMilestone = pr.milestone; + + // Fetch all open milestones + const { data: milestones } = await github.rest.issues.listMilestones({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + sort: 'due_on', + direction: 'asc' + }); + + if (milestones.length === 0) { + console.log('No open milestones found. Skipping milestone assignment.'); + return; + } + + // Sort milestones: first by due_on (earliest first), then by title (version number) + const sortedMilestones = milestones.sort((a, b) => { + // If both have due dates, sort by due date + if (a.due_on && b.due_on) { + return new Date(a.due_on) - new Date(b.due_on); + } + // If only one has a due date, prioritize it + if (a.due_on) return -1; + if (b.due_on) return 1; + // Otherwise sort by title (version number) - lower version first + return a.title.localeCompare(b.title, undefined, { numeric: true }); + }); + + const targetMilestone = sortedMilestones[0]; + console.log(`Target milestone: ${targetMilestone.title} (#${targetMilestone.number})`); + + // Case 1: PR has no milestone - assign to target milestone + if (!currentMilestone) { + console.log(`PR #${prNumber} has no milestone. Assigning to ${targetMilestone.title}.`); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + milestone: targetMilestone.number + }); + return; + } + + // Case 2: PR has an open milestone - keep it + if (currentMilestone.state === 'open') { + console.log(`PR #${prNumber} already has open milestone: ${currentMilestone.title}. Keeping it.`); + return; + } + + // Case 3: PR has a closed milestone - update and comment + console.log(`PR #${prNumber} has closed milestone: ${currentMilestone.title}. Updating to ${targetMilestone.title}.`); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + milestone: targetMilestone.number + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `This PR was merged with a closed milestone (\`${currentMilestone.title}\`). It has been automatically moved to \`${targetMilestone.title}\`.\n\nIf this was intentional (e.g., a backport), please manually update the milestone.` + }); From cccc5045374230afe82695ae88319d73e43e01bb Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Thu, 22 Jan 2026 13:42:10 -0600 Subject: [PATCH 2/2] Remove closed milestone reassignment per review --- .github/workflows/milestone-automation.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/milestone-automation.yml b/.github/workflows/milestone-automation.yml index ae0843d5..683c1a78 100644 --- a/.github/workflows/milestone-automation.yml +++ b/.github/workflows/milestone-automation.yml @@ -77,19 +77,12 @@ jobs: return; } - // Case 3: PR has a closed milestone - update and comment - console.log(`PR #${prNumber} has closed milestone: ${currentMilestone.title}. Updating to ${targetMilestone.title}.`); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - milestone: targetMilestone.number - }); + // Case 3: PR has a closed milestone - notify but don't change + console.log(`PR #${prNumber} has closed milestone: ${currentMilestone.title}. Adding notification comment.`); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - body: `This PR was merged with a closed milestone (\`${currentMilestone.title}\`). It has been automatically moved to \`${targetMilestone.title}\`.\n\nIf this was intentional (e.g., a backport), please manually update the milestone.` + body: `This PR was merged with a closed milestone (\`${currentMilestone.title}\`). Please verify this is intentional.\n\nIf not, the next open milestone is \`${targetMilestone.title}\`.` });