Skip to content

Commit 13e27ef

Browse files
authored
Create auto-milestone.yml
1 parent 73675c9 commit 13e27ef

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
name: Auto Milestone & Label Management
2+
3+
on:
4+
push:
5+
branches:
6+
- 'release/v*'
7+
pull_request:
8+
types: [opened, synchronize, reopened, edited]
9+
release:
10+
types: [published]
11+
12+
permissions:
13+
contents: read
14+
issues: write
15+
pull-requests: write
16+
17+
jobs:
18+
manage-milestones-and-labels:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout repo
23+
uses: actions/checkout@v4
24+
25+
- name: Auto milestone and label management
26+
uses: actions/github-script@v7
27+
with:
28+
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
29+
script: |
30+
const org = context.repo.owner;
31+
const repo = context.repo.repo;
32+
33+
function extractVersion(str) {
34+
const match = str.match(/^v?(\d+\.\d+(\.\d+)?)$/);
35+
return match ? match[1] : null;
36+
}
37+
38+
function compareVersions(a, b) {
39+
const pa = a.split('.').map(Number);
40+
const pb = b.split('.').map(Number);
41+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
42+
const x = pa[i] || 0;
43+
const y = pb[i] || 0;
44+
if (x !== y) return x - y;
45+
}
46+
return 0;
47+
}
48+
49+
// 1) Создание label при push в release/v*
50+
if (context.eventName === 'push') {
51+
const ref = context.ref;
52+
if (!ref.startsWith('refs/heads/release/v')) return;
53+
54+
const version = extractVersion(ref.replace('refs/heads/release/', ''));
55+
if (!version) return;
56+
57+
const labelName = `release:v${version}`;
58+
const labels = await github.rest.issues.listLabelsForRepo({ owner: org, repo });
59+
60+
if (!labels.data.some(l => l.name === labelName)) {
61+
await github.rest.issues.createLabel({
62+
owner: org,
63+
repo,
64+
name: labelName,
65+
color: '0e8a16',
66+
description: `Release version v${version}`
67+
});
68+
console.log(`✅ Label "${labelName}" создан.`);
69+
}
70+
71+
return;
72+
}
73+
74+
// 2) Присвоение label + назначение milestone для PR
75+
if (context.eventName === 'pull_request') {
76+
const pr = context.payload.pull_request;
77+
const releaseMatch = pr.head.ref.match(/^release\/v(\d+\.\d+(\.\d+)?)$/)
78+
|| pr.base.ref.match(/^release\/v(\d+\.\d+(\.\d+)?)$/);
79+
80+
if (!releaseMatch) return;
81+
const version = releaseMatch[1];
82+
const labelName = `release:v${version}`;
83+
84+
await github.rest.issues.addLabels({
85+
owner: org,
86+
repo,
87+
issue_number: pr.number,
88+
labels: [labelName]
89+
});
90+
console.log(`✅ Label "${labelName}" добавлен в PR #${pr.number}`);
91+
92+
// Назначение milestone
93+
const milestoneTitle = `v${version}`;
94+
const milestones = await github.rest.issues.listMilestones({ owner: org, repo, state: 'open' });
95+
const milestone = milestones.data.find(m => m.title === milestoneTitle);
96+
97+
if (milestone) {
98+
await github.rest.issues.update({
99+
owner: org,
100+
repo,
101+
issue_number: pr.number,
102+
milestone: milestone.number
103+
});
104+
console.log(`📌 Milestone "${milestoneTitle}" назначен PR #${pr.number}`);
105+
} else {
106+
console.log(`⚠️ Milestone "${milestoneTitle}" не найден — назначение пропущено.`);
107+
}
108+
109+
return;
110+
}
111+
112+
// 3) После релиза: закрыть milestone и перенести задачи
113+
if (context.eventName === 'release' && context.payload.action === 'published') {
114+
const tag = context.payload.release.tag_name;
115+
116+
function parseVersion(name) {
117+
const m = name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
118+
return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
119+
}
120+
121+
function isGreater(a, b) {
122+
if (a.major !== b.major) return a.major > b.major;
123+
if (a.minor !== b.minor) return a.minor > b.minor;
124+
return a.patch > b.patch;
125+
}
126+
127+
const currentVersion = parseVersion(tag);
128+
if (!currentVersion) {
129+
console.log(`⚠️ Tag ${tag} не похож на vX.Y.Z — перенос пропущен.`);
130+
return;
131+
}
132+
133+
const milestoneTitle = `v${currentVersion.major}.${currentVersion.minor}.${currentVersion.patch}`;
134+
const { data: allMilestones } = await github.rest.issues.listMilestones({
135+
owner: org,
136+
repo,
137+
state: 'all'
138+
});
139+
140+
const current = allMilestones.find(m => m.title === milestoneTitle);
141+
if (!current) {
142+
console.log(`⚠️ Milestone "${milestoneTitle}" не найден.`);
143+
return;
144+
}
145+
146+
// Находим следующий milestone по SemVer
147+
const versions = allMilestones
148+
.map(m => ({ m, v: parseVersion(m.title) }))
149+
.filter(x => x.v && isGreater(x.v, currentVersion))
150+
.sort((a, b) =>
151+
isGreater(a.v, b.v) ? 1 : -1
152+
);
153+
154+
const next = versions.length ? versions[0].m : null;
155+
156+
const openIssues = await github.rest.issues.listForRepo({
157+
owner: org,
158+
repo,
159+
milestone: current.number,
160+
state: 'open',
161+
per_page: 100
162+
});
163+
164+
if (next) {
165+
for (const issue of openIssues.data) {
166+
await github.rest.issues.update({
167+
owner: org,
168+
repo,
169+
issue_number: issue.number,
170+
milestone: next.number
171+
});
172+
console.log(`➡️ #${issue.number} перенесён → ${next.title}`);
173+
}
174+
} else {
175+
console.log(`⚠️ Следующий milestone не найден — задачи остаются в закрытом.`);
176+
}
177+
178+
await github.rest.issues.updateMilestone({
179+
owner: org,
180+
repo,
181+
milestone_number: current.number,
182+
state: 'closed'
183+
});
184+
185+
console.log(`🎉 Milestone "${milestoneTitle}" закрыт.`);
186+
}

0 commit comments

Comments
 (0)