# This reusable workflow sets the milestone based on the # current PR labels. name: PR Milestone on: workflow_call: inputs: pr_number: required: true type: number permissions: pull-requests: write contents: read jobs: set-milestone: name: Set PR milestone runs-on: ubuntu-latest steps: - name: Determine target milestone id: determine-milestone uses: actions/github-script@v8 with: script: | const prNumber = ${{ inputs.pr_number }}; /* * Get the base branch of the PR. */ const { data: pullRequest } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); const baseBranch = pullRequest.base.ref; /* * 1. Return early if base is main branch. Milestone is already * managed and probably done when merging to main. */ if (baseBranch === 'main') { core.info('PR targets main. Milestone is managed separately.'); core.setOutput('target_milestone', ''); return; } /* * Helper function to set the target milestone output. */ const setTargetMilestoneOutput = (targetMilestone) => { core.info(`Target milestone: ${targetMilestone}`); core.setOutput('target_milestone', targetMilestone); }; /* * 2. If PR targets a release-candidate branch (for example 9.1.0.rc), * set milestone to the release version prefix (for example 9.1.0) * and return early. */ if (baseBranch.endsWith('.rc')) { const targetMilestone = baseBranch.replace(/\.rc$/, ''); core.info( `PR targets release-candidate branch "${baseBranch}". ` + `Using milestone "${targetMilestone}".` ); setTargetMilestoneOutput(targetMilestone); return; } /** * 3. If PR targets a branch that is not developer, do nothing. */ if (baseBranch !== 'developer') { core.info('PR does not target developer branch. Skipping milestone management.'); core.setOutput('target_milestone', ''); return; } /* * 4. If PR targets developer, check "Equivalent PRs" in the PR body. * If an equivalent PR targets an RC branch (for example 9.1.0.rc), * use the corresponding release milestone (for example 9.1.0). */ if (baseBranch === 'developer') { const equivalentPrSectionMatch = pullRequest.body?.match( /(^|\n)#{1,6}\s*Equivalent PRs\s*\n([\s\S]*?)(?=\n#{1,6}\s+\S|\s*$)/i ); const equivalentPrNumbers = equivalentPrSectionMatch?.[2] ? Array.from( equivalentPrSectionMatch[2].matchAll(/#(\d+)/g), match => Number(match[1]) ) : []; for (const equivalentPrNumber of equivalentPrNumbers) { if (equivalentPrNumber === prNumber) { continue; } const { data: equivalentPr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: equivalentPrNumber }); const equivalentBaseBranch = equivalentPr.base.ref; if (!equivalentBaseBranch.endsWith('.rc')) { continue; } const targetMilestone = equivalentBaseBranch.replace(/\.rc$/, ''); core.info( `Developer PR references RC PR #${equivalentPrNumber} ` + `(${equivalentBaseBranch}). Using milestone "${targetMilestone}".` ); setTargetMilestoneOutput(targetMilestone); return; } } /* * 5. Read release targets through the GitHub API * from the base branch of PR. */ const { data: file } = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path: '.github/release-targets.yaml', ref: baseBranch }); const releaseTargetsFile = Buffer.from(file.content, 'base64').toString('utf8'); const getTarget = (name) => { const match = releaseTargetsFile.match( new RegExp(`^${name}:\\s*["']?([^"'\\s]+)["']?`, 'm') ); if (!match) { core.setFailed( `Release target "${name}" was not found.` ); return null; } return match[1]; }; const major = getTarget('major'); const minor = getTarget('minor'); const bugFix = getTarget('bug_fix'); if (!major || !minor || !bugFix) { core.setFailed( 'Required release targets (major, minor, bug_fix) could not be resolved from yaml file.' ); return; } /* * Get current PR labels. */ const { data: labels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); const labelNames = labels.map(label => label.name); /* * Determine the milestone. * * Breaking API has highest priority. */ let targetMilestone; if (labelNames.includes('Breaking API')) { targetMilestone = major; } else if (labelNames.includes('Feature')) { targetMilestone = minor; } else if ( labelNames.includes('Bug Fix') || labelNames.includes('Infrastructure') ) { targetMilestone = bugFix; } else { core.setFailed( 'No milestone-related labels found.' ); return; } setTargetMilestoneOutput(targetMilestone); - name: Set milestone if: steps.determine-milestone.outputs.target_milestone != '' uses: actions/github-script@v8 with: script: | const prNumber = ${{ inputs.pr_number }}; const targetMilestone = '${{ steps.determine-milestone.outputs.target_milestone }}'; const { data: milestones } = await github.rest.issues.listMilestones({ owner: context.repo.owner, repo: context.repo.repo, state: 'open' }); const milestone = milestones.find( milestone => milestone.title === targetMilestone ); if (!milestone) { core.setFailed( `Milestone "${targetMilestone}" was not found.` ); return; } await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, milestone: milestone.number }); core.info( `Milestone set to "${targetMilestone}".` );