ci: add workflow to auto-label milestone issues on release (#9538)

## Summary
Adds a new GitHub Actions workflow that automatically applies release
version labels to milestone issues when a release is published.

## Changes
- New workflow: `.github/workflows/release-label-applier.yml`
- Triggers automatically after the Release workflow completes
successfully
- Can also be manually triggered via workflow_dispatch with a specific
release tag

## Behavior
When a release is created (e.g., `v0.30.0-rc1`):
1. Identifies the corresponding milestone (e.g., `v0.30.0`)
2. Finds all closed issues or issues with
`Needs-Testing`/`Testing-InProgress` labels in that milestone
3. Applies the release tag as a label to those issues
4. Includes deduplication logic to prevent applying duplicate labels

This helps track which specific release version an issue was shipped in,
making it easier to identify what needs testing in each release.

## Permissions
- `issues: write` - to add labels
- `contents: read` - to checkout repository

## Test Plan
- [ ] Workflow should trigger automatically on release completion
- [ ] Manual trigger should work with provided release tag
- [ ] Labels should be applied only to eligible issues
- [ ] Duplicate labels should be prevented
This commit is contained in:
Shrinath Rao
2025-12-09 19:34:07 +05:30
committed by GitHub
parent 5b988ab5c7
commit b2f9b4550a
+196
View File
@@ -0,0 +1,196 @@
name: Release Label Applier
on:
# Auto-trigger when Release workflow completes successfully
workflow_run:
workflows: ["Release"]
types:
- completed
# Manual trigger option
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag to apply (e.g., v0.30.0-rc1)'
required: true
type: string
jobs:
apply-labels:
name: Apply release labels to milestone issues
runs-on: ubuntu-latest
# Only run if the Release workflow succeeded
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
permissions:
issues: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Apply labels to milestone issues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Get the release tag
let releaseTag;
if (context.eventName === 'workflow_dispatch') {
releaseTag = '${{ inputs.release_tag }}';
} else {
// Get the tag from the completed workflow
const runId = context.payload.workflow_run.id;
const workflowRun = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId
});
// Extract tag from head_ref (more reliable for tag-triggered workflows)
// head_ref contains the tag name directly (e.g., "v0.30.0-rc1")
const headRef = workflowRun.data.head_ref;
if (!headRef) {
core.setFailed('Could not determine release tag from workflow run');
return;
}
releaseTag = headRef;
}
console.log(`📦 Processing release tag: ${releaseTag}`);
// Extract milestone version from tag
// Examples: v0.30.0-rc1 -> v0.30.0, v0.30.1 -> v0.30.0, v0.30.0 -> v0.30.0
// Patch releases map to their minor version milestone (e.g., v0.30.1 -> v0.30.0)
const milestoneMatch = releaseTag.match(/^v(\d+)\.(\d+)\.(\d+)/);
if (!milestoneMatch) {
core.setFailed(`Invalid tag format: ${releaseTag}. Expected format: vX.Y.Z[-suffix]`);
return;
}
// Construct milestone version with patch set to 0
const milestoneVersion = `v${milestoneMatch[1]}.${milestoneMatch[2]}.0`;
console.log(`🎯 Target milestone: ${milestoneVersion}`);
// Find the milestone
const milestones = await github.rest.issues.listMilestones({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all'
});
const milestone = milestones.data.find(m => m.title === milestoneVersion);
if (!milestone) {
core.setFailed(`Milestone "${milestoneVersion}" not found`);
return;
}
console.log(`✓ Found milestone: ${milestone.title} (ID: ${milestone.number})`);
// Get all issues in the milestone
const allIssues = [];
let page = 1;
while (true) {
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
milestone: milestone.number,
state: 'all',
per_page: 100,
page: page
});
if (issues.data.length === 0) break;
allIssues.push(...issues.data);
page++;
}
console.log(`📋 Found ${allIssues.length} total issues/PRs in milestone`);
// Filter issues that need labeling
const targetLabels = ['Needs-Testing', 'Testing-InProgress'];
const issuesToLabel = allIssues.filter(issue => {
// Include if: closed OR has target labels
const isClosed = issue.state === 'closed';
const hasTargetLabel = issue.labels.some(label =>
targetLabels.includes(label.name)
);
return isClosed || hasTargetLabel;
});
console.log(`🎯 ${issuesToLabel.length} issues match criteria (closed or have Needs-Testing/Testing-InProgress labels)`);
// Apply labels (with deduplication check)
let labeled = 0;
let skipped = 0;
const milestonePattern = new RegExp(`^${milestoneVersion.replace(/\./g, '\\.')}(-|$)`);
for (const issue of issuesToLabel) {
// Check if issue already has a label from this milestone
const hasExistingMilestoneLabel = issue.labels.some(label =>
milestonePattern.test(label.name)
);
if (hasExistingMilestoneLabel) {
const existingLabel = issue.labels.find(l => milestonePattern.test(l.name));
console.log(`⏭️ Skipping #${issue.number} - already has label: ${existingLabel.name}`);
skipped++;
continue;
}
// Check if the label already exists on the issue (shouldn't happen, but safety check)
if (issue.labels.some(label => label.name === releaseTag)) {
console.log(`⏭️ Skipping #${issue.number} - already has exact label: ${releaseTag}`);
skipped++;
continue;
}
// Create label if it doesn't exist in the repo
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: releaseTag,
color: '0E8A16', // Green color for releases
description: `Released in ${releaseTag}`
});
console.log(`✓ Created label: ${releaseTag}`);
} catch (error) {
if (error.status !== 422) { // 422 means label already exists
console.log(`⚠️ Error creating label: ${error.message}`);
}
}
// Apply label to issue
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [releaseTag]
});
console.log(`✓ Added label "${releaseTag}" to #${issue.number}: ${issue.title}`);
labeled++;
} catch (error) {
console.log(`❌ Failed to label #${issue.number}: ${error.message}`);
}
}
// Summary
console.log('\n📊 Summary:');
console.log(` Release Tag: ${releaseTag}`);
console.log(` Milestone: ${milestoneVersion}`);
console.log(` Total issues in milestone: ${allIssues.length}`);
console.log(` Issues matching criteria: ${issuesToLabel.length}`);
console.log(` ✓ Labeled: ${labeled}`);
console.log(` ⏭️ Skipped (already labeled): ${skipped}`);
core.summary
.addHeading('Release Label Application Summary')
.addRaw(`**Release Tag:** ${releaseTag}`, true)
.addRaw(`**Milestone:** ${milestoneVersion}`, true)
.addRaw(`**Issues Labeled:** ${labeled}`, true)
.addRaw(`**Issues Skipped:** ${skipped} (already had milestone release label)`, true)
.write();