Skip to content

Commit 62ff770

Browse files
Separated script from workflow
1 parent 47372f3 commit 62ff770

File tree

2 files changed

+189
-99
lines changed

2 files changed

+189
-99
lines changed

.github/workflows/assign_next_release_milestone.yml

Lines changed: 18 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -12,121 +12,40 @@ on:
1212
workflow_dispatch:
1313
inputs:
1414
dry_run:
15-
description: 'Run in test mode (no changes made)'
15+
description: 'To run in test mode (no changes made), set to true'
1616
required: false
17-
default: 'false'
17+
default: false
1818
type: boolean
1919

2020
jobs:
2121
assign-milestone:
22-
runs-on: ubuntu-latest
22+
runs-on: ubuntu-22.04
2323

2424
steps:
2525

2626
- name: Load secrets from 1Password
2727
id: onepw_secrets
2828
uses: 1password/[email protected]
2929
with:
30-
export-env: true # Export loaded secrets as environment variables
30+
export-env: true
3131
env:
3232
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
3333
GH_TOKEN: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/GitHub generic action token for all repos/credential"
3434

3535
- name: Checkout repository
36-
uses: actions/checkout@v4
36+
uses: actions/checkout@v4.2.2
3737

38-
- name: Assign milestone to merged issues
39-
uses: actions/github-script@v7
38+
- name: Setup Node.js
39+
uses: actions/[email protected]
4040
with:
41-
github-token: ${{ env.GH_TOKEN }}
42-
script: |
43-
const { owner, repo } = context.repo;
44-
45-
const isDryRun = '${{ github.event.inputs.dry_run }}' === 'true';
46-
47-
if (isDryRun) {
48-
console.log('🧪 RUNNING IN DRY RUN MODE - No changes will be made');
49-
}
50-
51-
// Get the "next release" milestone
52-
const milestones = await github.rest.issues.listMilestones({
53-
owner,
54-
repo,
55-
state: 'open'
56-
});
57-
58-
const nextReleaseMilestone = milestones.data.find(
59-
milestone => milestone.title.toLowerCase() === 'next release'
60-
);
61-
62-
if (!nextReleaseMilestone) {
63-
console.log('❌ "next release" milestone not found');
64-
return;
65-
}
66-
67-
console.log(`Found milestone: ${nextReleaseMilestone.title} (ID: ${nextReleaseMilestone.number})`);
68-
69-
// Get all closed issues
70-
const issues = await github.rest.issues.listForRepo({
71-
owner,
72-
repo,
73-
state: 'closed',
74-
per_page: 100
75-
});
76-
77-
console.log(`Found ${issues.data.length} closed issues`);
78-
79-
// Process each issue
80-
for (const issue of issues.data) {
81-
// Skip pull requests (they appear in issues API but have pull_request property)
82-
if (issue.pull_request) {
83-
continue;
84-
}
85-
86-
// Skip if already has the next release milestone
87-
if (issue.milestone && issue.milestone.number === nextReleaseMilestone.number) {
88-
console.log(`Issue #${issue.number} already has next release milestone`);
89-
continue;
90-
}
91-
92-
// Check if issue was closed by a merged PR
93-
const timelineEvents = await github.rest.issues.listEventsForTimeline({
94-
owner,
95-
repo,
96-
issue_number: issue.number
97-
});
98-
99-
const wasMergedToMain = timelineEvents.data.some(event => {
100-
return event.event === 'closed' &&
101-
event.commit_id &&
102-
event.source &&
103-
event.source.issue &&
104-
event.source.issue.pull_request &&
105-
event.source.issue.pull_request.merged_at;
106-
});
107-
108-
if (wasMergedToMain) {
109-
console.log(`${isDryRun ? '🧪 [DRY RUN] Would assign' : 'Assigning'} milestone to issue #${issue.number}: ${issue.title}`);
110-
111-
if (!isDryRun) {
112-
try {
113-
await github.rest.issues.update({
114-
owner,
115-
repo,
116-
issue_number: issue.number,
117-
milestone: nextReleaseMilestone.number
118-
});
119-
120-
console.log(`✅ Successfully assigned milestone to issue #${issue.number}`);
121-
} catch (error) {
122-
console.error(`❌ Failed to assign milestone to issue #${issue.number}:`, error.message);
123-
}
124-
} else {
125-
console.log(`🧪 [DRY RUN] Skipped actual assignment for issue #${issue.number}`);
126-
}
127-
} else {
128-
console.log(`Issue #${issue.number} was not merged to main, skipping`);
129-
}
130-
}
131-
132-
console.log(`${isDryRun ? '🧪 [DRY RUN] Test complete!' : 'Milestone assignment complete!'}`);
41+
node-version: 22
42+
43+
- name: Install dependencies
44+
run: npm install @octokit/rest
45+
46+
- name: Run milestone assignment
47+
env:
48+
GITHUB_TOKEN: ${{ env.GH_TOKEN }}
49+
GITHUB_REPOSITORY: ${{ github.repository }}
50+
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
51+
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)