Skip to content

Commit 3b7c0bf

Browse files
committed
Add workflows to manage milestones when releasing
1 parent ff8d72c commit 3b7c0bf

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Add milestone to pull requests
2+
on:
3+
pull_request:
4+
types: [closed]
5+
branches:
6+
- main
7+
- release/v*
8+
jobs:
9+
add_milestone_to_merged:
10+
name: Add milestone to merged pull requests
11+
permissions:
12+
issues: write # Required to update a pull request using the issues API
13+
pull-requests: write # Required to update the milestone of a pull request
14+
if: github.event.pull_request.merged && github.event.pull_request.milestone == null
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Add milestone to merged pull requests
18+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0
19+
with:
20+
retries: 3
21+
retry-exempt-status-codes: 400,401
22+
script: |
23+
// Get project milestones
24+
const response = await github.rest.issues.listMilestones({
25+
owner: context.repo.owner,
26+
repo: context.repo.repo,
27+
state: 'open'
28+
})
29+
if (!response.data || response.data.length == 0) {
30+
core.setFailed(`Failed to list milestones: ${response.status}`)
31+
return
32+
}
33+
// Get the base branch
34+
const base = '${{ github.event.pull_request.base.ref }}'
35+
// Look for the matching milestone
36+
let milestoneNumber = null
37+
if (base == 'main') {
38+
// Pick the milestone with the highest version as title using semver
39+
milestoneNumber = response.data
40+
.map(milestone => {
41+
// Parse version title as semver "<major>.<minor>.<patch>"
42+
const versionNumbers = milestone.title.match(/^(\d+)\.(\d+)\.(\d+)$/)
43+
if (versionNumbers == null) {
44+
return null
45+
}
46+
milestone.version = {
47+
major: parseInt(versionNumbers[1]),
48+
minor: parseInt(versionNumbers[2]),
49+
patch: parseInt(versionNumbers[3])
50+
}
51+
return milestone
52+
})
53+
.filter(milestone => milestone != null)
54+
.sort((a, b) => {
55+
if (a.version.major != b.version.major) {
56+
return a.version.major - b.version.major
57+
}
58+
if (a.version.minor != b.version.minor) {
59+
return a.version.minor - b.version.minor
60+
}
61+
return a.version.patch - b.version.patch
62+
})
63+
.pop()?.number
64+
} else if (base.startsWith('release/v') && base.endsWith('.x')) {
65+
// Extract the minor version related to the base branch (e.g. "release/v1.2.x" -> "1.2.")
66+
const version = base.substring(9, base.length - 1)
67+
// Pick the milestone with title matching the extracted version
68+
const versionMilestone = response.data
69+
.find(milestone => milestone.title.startsWith(version))
70+
if (!versionMilestone) {
71+
core.setFailed(`Milestone not found for minor version: ${version}`)
72+
} else {
73+
milestoneNumber = versionMilestone.number
74+
}
75+
} else {
76+
core.setFailed(`Unexpected pull request base: ${base}`)
77+
}
78+
// Update pull request milestone using the issues API (as pull requests are issues)
79+
if (milestoneNumber != null) {
80+
await github.rest.issues.update({
81+
owner: context.repo.owner,
82+
repo: context.repo.repo,
83+
issue_number: ${{ github.event.pull_request.number }},
84+
milestone: milestoneNumber
85+
});
86+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Increment milestones on tag
2+
on:
3+
create
4+
permissions:
5+
issues: write # Required to update milestones
6+
7+
jobs:
8+
increment_milestone:
9+
if: github.event.ref_type == 'tag' && contains(github.event.ref,'-RC') == false
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Close current milestone
13+
id: close-milestone
14+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0
15+
with:
16+
script: |
17+
// Get the milestone title ("X.Y.Z") from tag name ("vX.Y.Z")
18+
const match = '${{github.event.ref}}'.match(/v(\d+\.\d+\.\d+)/i)
19+
if (!match) {
20+
core.setFailed('Failed to parse tag name into milestone title: ${{github.event.ref}}')
21+
return
22+
}
23+
const milestoneTitle = match[1]
24+
// Look for the milestone from its title
25+
const response = await github.rest.issues.listMilestones({
26+
owner: context.repo.owner,
27+
repo: context.repo.repo,
28+
state: 'open'
29+
})
30+
if (!response.data || response.data.length == 0) {
31+
core.setFailed(`Failed to list milestones: ${response.status}`)
32+
return
33+
}
34+
const milestone = response.data.find(milestone => milestone.title == milestoneTitle)
35+
if (!milestone) {
36+
core.setFailed(`Failed to find milestone: ${milestoneTitle}`)
37+
return
38+
}
39+
// Close the milestone
40+
await github.rest.issues.updateMilestone({
41+
owner: context.repo.owner,
42+
repo: context.repo.repo,
43+
state: 'closed',
44+
milestone_number: milestone.number
45+
}).catch(error => {
46+
core.setFailed(`Failed to close milestone: ${error}`)
47+
})
48+
// Compute the next milestone version
49+
const versionNumbers = milestoneTitle.split('.').map(Number)
50+
if (versionNumbers[2] != 0) {
51+
core.info('Closing a patch version milestone. Not opening a new one.')
52+
return
53+
}
54+
versionNumbers[1]++
55+
const nextMilestoneTitle = versionNumbers.join('.')
56+
core.info(`Creating next version milestone: ${nextMilestoneTitle}`)
57+
// Create the next milestone
58+
await github.rest.issues.createMilestone({
59+
owner: context.repo.owner,
60+
repo: context.repo.repo,
61+
title: nextMilestoneTitle
62+
}).catch(error => {
63+
core.setFailed(`Failed to create milestone ${nextMilestoneTitle}: ${error}`)
64+
})

0 commit comments

Comments
 (0)