forked from qgis/QGIS
-
Notifications
You must be signed in to change notification settings - Fork 0
170 lines (150 loc) · 7.06 KB
/
pr-auto-milestone.yml
File metadata and controls
170 lines (150 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
name: 📅 Auto set milestone on PR
# This workflow automatically assigns milestones to newly opened PRs based on:
# - The target branch (master vs release branch)
# - The latest released version found in git tags
# - Master PRs get milestone +2 minor versions from latest (e.g., 4.0 latest -> 4.2 milestone)
# - Release branch PRs get milestone +0.1 patch version (e.g., 4.0.0 latest -> 4.0.1 milestone)
# - queued_ltr_backports PRs target the current LTR + 0.2 patch (e.g., 3.44.6 → 3.44.8, 4.2.1 → 4.2.3)
# LTR series: 3.44, then 4.2, 4.8, 4.14, 4.20, 4.26... (every 6 minor versions starting from 4.2)
# - Works for both 3.x and 4.x release branches (major version auto-detected from branch name)
on:
workflow_call:
workflow_dispatch:
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
permissions:
contents: read
jobs:
pr-without-milestones:
runs-on: ubuntu-latest
if: github.repository == 'qgis/QGIS'
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const MAJOR_VERSION_MASTER = 4; // Update when bumping to 5.x
const MAJOR_VERSION_LTR = 3; // 3 for 3.44, will be 4 when 4.2 becomes LTR
// LTR minor versions per major version
const LTR_MINORS = {
3: [44],
4: [2, 8, 14, 20, 26, 32, 38, 44, 50], // 2 + 6*n
};
// --- Fetch data from GitHub API ---
const { repository } = await github.graphql(`query {
repository(owner: "qgis", name: "QGIS") {
pullRequests(states: OPEN, last: 100) {
edges { node { number milestone { number } baseRef { name } } }
}
milestones(orderBy: {field: CREATED_AT, direction: DESC}, first: 50) {
edges { node { title number } }
}
refs(refPrefix: "refs/tags/", orderBy: {field: TAG_COMMIT_DATE, direction: DESC}, first: 30) {
edges { node { name } }
}
}
}`);
const prs = repository.pullRequests.edges.map(e => e.node);
const milestones = repository.milestones.edges.map(e => e.node);
const tags = repository.refs.edges.map(e => e.node.name);
// --- Helper: parse "final-M_m_p" tag into {major, minor, patch} ---
function parseTag(tag) {
const m = tag.match(/^final-(\d+)_(\d+)_(\d+)$/);
return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
}
// --- Helper: find max patch version among tags matching major_minor ---
function maxRelease(major, minor) {
let max = -1;
for (const tag of tags) {
const parsed = parseTag(tag);
if (parsed && parsed.major === major && parsed.minor === minor) {
max = Math.max(max, parsed.patch);
}
}
return max >= 0 ? { major, minor, patch: max } : null;
}
// --- Helper: find or create milestone, return its number ---
async function getOrCreateMilestone(title) {
const existing = milestones.find(m => m.title === title);
if (existing) {
core.info(`Found existing milestone "${title}" (#${existing.number})`);
return existing.number;
}
core.info(`Creating milestone "${title}"`);
const { data } = await github.rest.issues.createMilestone({
...context.repo,
title,
});
// Cache it for subsequent PRs
milestones.push({ title, number: data.number });
return data.number;
}
// --- Compute milestone title for a given branch ---
function computeMilestoneTitle(branch) {
const releaseMatch = branch.match(/^release-(\d+)_(\d+)$/);
if (branch === 'queued_ltr_backports') {
// LTR backports: find latest LTR release, increment patch by 2
const ltrMinors = LTR_MINORS[MAJOR_VERSION_LTR] || [];
let best = null;
for (const minor of ltrMinors) {
const rel = maxRelease(MAJOR_VERSION_LTR, minor);
if (rel && (!best || rel.minor > best.minor || (rel.minor === best.minor && rel.patch > best.patch))) {
best = rel;
}
}
if (!best) {
core.warning(`No LTR release found for major version ${MAJOR_VERSION_LTR}`);
return null;
}
return `${best.major}.${best.minor}.${best.patch + 2}`;
} else if (releaseMatch) {
// Release branch (e.g. release-4_0): increment patch by 1
const major = +releaseMatch[1];
const minor = +releaseMatch[2];
const rel = maxRelease(major, minor);
if (!rel) {
throw new Error(`No release found for ${major}.${minor}`);
}
return `${rel.major}.${rel.minor}.${rel.patch + 1}`;
} else {
// Master (or any other branch): increment minor by 2
// Find the highest minor version tagged for this major
let maxMinor = -1;
for (const tag of tags) {
const parsed = parseTag(tag);
if (parsed && parsed.major === MAJOR_VERSION_MASTER) {
maxMinor = Math.max(maxMinor, parsed.minor);
}
}
if (maxMinor < 0) {
throw new Error(`No release found for major version ${MAJOR_VERSION_MASTER}`);
}
return `${MAJOR_VERSION_MASTER}.${maxMinor + 2}.0`;
}
}
// --- Process all PRs without milestones ---
const prsToProcess = prs.filter(pr => !pr.milestone);
core.info(`PRs without milestones: ${prsToProcess.length}`);
for (const pr of prsToProcess) {
const branch = pr.baseRef?.name;
if (!branch) {
core.warning(`PR #${pr.number}: no base branch, skipping`);
continue;
}
core.info(`\nProcessing PR #${pr.number} (branch: ${branch})`);
const milestoneTitle = computeMilestoneTitle(branch);
if (!milestoneTitle) {
core.warning(`PR #${pr.number}: could not determine milestone, skipping`);
continue;
}
const milestoneNumber = await getOrCreateMilestone(milestoneTitle);
await github.rest.issues.update({
...context.repo,
issue_number: pr.number,
milestone: milestoneNumber,
});
core.info(`✓ PR #${pr.number} → milestone "${milestoneTitle}"`);
}