Skip to content

Commit f59411b

Browse files
Merge pull request #1243 from MobilityData/feat-next-release-milestone-2025-06-05
2 parents 372476e + 9f0931c commit f59411b

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Assign `Next Release` Milestone
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths-ignore:
8+
- 'README.md'
9+
- 'LICENSE'
10+
- '.gitignore'
11+
workflow_dispatch:
12+
inputs:
13+
dry_run:
14+
description: 'To run in test mode (no changes made), set to true'
15+
required: false
16+
default: false
17+
type: boolean
18+
19+
jobs:
20+
assign-milestone:
21+
runs-on: ubuntu-22.04
22+
23+
steps:
24+
25+
- name: Load secrets from 1Password
26+
id: onepw_secrets
27+
uses: 1password/[email protected]
28+
with:
29+
export-env: true
30+
env:
31+
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
32+
GH_TOKEN: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GitHub generic action token for all repos/credential"
33+
34+
- name: Checkout repository
35+
uses: actions/[email protected]
36+
37+
- name: Setup Node.js
38+
uses: actions/[email protected]
39+
with:
40+
node-version: 22
41+
42+
- name: Install dependencies
43+
run: npm install @octokit/rest
44+
45+
- name: Run milestone assignment
46+
env:
47+
GITHUB_TOKEN: ${{ env.GH_TOKEN }}
48+
GITHUB_REPOSITORY: ${{ github.repository }}
49+
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
50+
run: node .github/scripts/assign-next-release-milestone.js
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env node
2+
/**
3+
* GitHub Milestone Assignment Script
4+
* Assigns "next release" milestone to closed issues when merged to main
5+
*
6+
* Usage:
7+
*
8+
* 1. Setup Environment variables to run locally:
9+
* export GITHUB_TOKEN="ghp_your_token_here"
10+
* export GITHUB_REPOSITORY="MobilityData/mobility-feed-api"
11+
*
12+
* 2. Install dependencies
13+
* npm install @octokit/rest
14+
*
15+
* 3. Run in dry mode
16+
* node scripts/assign-next-release-milestone.js --dry-run
17+
* - Optional. If provided then the script will do a dry run without affecting any issues, logging will explain what will be done when running in production mode.
18+
*
19+
* 4. Run in production mode
20+
* node scripts/assign-next-release-milestone.js
21+
*/
22+
23+
const { Octokit } = require('@octokit/rest');
24+
25+
// Parse command line arguments
26+
const args = process.argv.slice(2);
27+
const hasArgDryRun = args.includes('--dry-run');
28+
29+
// Determine dry run mode based on priority:
30+
// 1. Command line argument --dry-run takes precedence
31+
// 2. Environment variable DRY_RUN
32+
// 3. Default to false
33+
const isDryRun = hasArgDryRun || process.env.DRY_RUN === 'true';
34+
35+
// Validate required environment variables
36+
const token = process.env.GITHUB_TOKEN;
37+
const repository = process.env.GITHUB_REPOSITORY;
38+
39+
if (!token) {
40+
console.error('❌ GITHUB_TOKEN environment variable is required');
41+
process.exit(1);
42+
}
43+
44+
if (!repository) {
45+
console.error('❌ GITHUB_REPOSITORY environment variable is required (format: owner/repo)');
46+
process.exit(1);
47+
}
48+
49+
const [owner, repo] = repository.split('/');
50+
if (!owner || !repo) {
51+
console.error('❌ GITHUB_REPOSITORY must be in format "owner/repo"');
52+
process.exit(1);
53+
}
54+
55+
// Initialize Octokit
56+
const octokit = new Octokit({
57+
auth: token,
58+
});
59+
60+
async function main() {
61+
try {
62+
if (isDryRun) {
63+
console.log('RUNNING IN DRY RUN MODE - No changes will be made');
64+
}
65+
66+
console.log(`Processing repository: ${owner}/${repo}`);
67+
68+
// Get the "next release" milestone
69+
console.log('Fetching milestones...');
70+
const milestones = await octokit.rest.issues.listMilestones({
71+
owner,
72+
repo,
73+
state: 'open'
74+
});
75+
76+
const nextReleaseMilestone = milestones.data.find(
77+
milestone => milestone.title.toLowerCase() === 'next release'
78+
);
79+
80+
if (!nextReleaseMilestone) {
81+
console.log('❌ "Next Release" milestone not found');
82+
return;
83+
}
84+
85+
console.log(`Found milestone: ${nextReleaseMilestone.title} (ID: ${nextReleaseMilestone.number})`);
86+
87+
// Get all closed issues
88+
console.log('Fetching closed issues...');
89+
const issues = await octokit.rest.issues.listForRepo({
90+
owner,
91+
repo,
92+
state: 'closed',
93+
per_page: 100
94+
});
95+
96+
console.log(`Found ${issues.data.length} closed issues`);
97+
98+
let processedCount = 0;
99+
let assignedCount = 0;
100+
101+
// Process each issue
102+
for (const issue of issues.data) {
103+
// Skip pull requests (they appear in issues API but have pull_request property)
104+
if (issue.pull_request) {
105+
continue;
106+
}
107+
108+
processedCount++;
109+
110+
// Skip if already has the next release milestone
111+
if (issue.milestone && issue.milestone.number === nextReleaseMilestone.number) {
112+
console.log(`Issue #${issue.number} already has Next Release milestone, skipping...`);
113+
continue;
114+
}
115+
116+
// Check if issue was closed by a merged PR
117+
console.log(`Checking timeline for issue #${issue.number}...`);
118+
const timelineEvents = await octokit.rest.issues.listEventsForTimeline({
119+
owner,
120+
repo,
121+
issue_number: issue.number
122+
});
123+
124+
const wasMergedToMain = timelineEvents.data.some(event => {
125+
return event.event === 'closed' &&
126+
event.commit_id &&
127+
event.source &&
128+
event.source.issue &&
129+
event.source.issue.pull_request &&
130+
event.source.issue.pull_request.merged_at;
131+
});
132+
133+
if (wasMergedToMain) {
134+
console.log(`${isDryRun ? '[DRY RUN] Would assign' : 'Assigning'} milestone to issue #${issue.number}: ${issue.title}`);
135+
136+
if (!isDryRun) {
137+
try {
138+
await octokit.rest.issues.update({
139+
owner,
140+
repo,
141+
issue_number: issue.number,
142+
milestone: nextReleaseMilestone.number
143+
});
144+
145+
console.log(`✅ Successfully assigned milestone to issue #${issue.number}`);
146+
assignedCount++;
147+
} catch (error) {
148+
console.error(`❌ Failed to assign milestone to issue #${issue.number}:`, error.message);
149+
}
150+
} else {
151+
console.log(`[DRY RUN] Skipped actual assignment for issue #${issue.number}`);
152+
assignedCount++;
153+
}
154+
} else {
155+
console.log(`Issue #${issue.number} was not merged to main, skipping`);
156+
}
157+
}
158+
159+
console.log(`\n\nSummary:`);
160+
console.log(`- Processed ${processedCount} issues`);
161+
console.log(`- ${isDryRun ? 'Would assign' : 'Assigned'} milestone to ${assignedCount} issues`);
162+
console.log(`${isDryRun ? '[DRY RUN] Test complete!' : '✅ Milestone assignment complete!'}`);
163+
164+
} catch (error) {
165+
console.error('❌ Script failed:', error.message);
166+
process.exit(1);
167+
}
168+
}
169+
170+
// Run the script
171+
main();

0 commit comments

Comments
 (0)