11name : milestone-release
22
33on :
4+ push :
5+ tags :
6+ - ' *'
47 milestone :
5- types : [created, edited, deleted, closed, opened]
8+ types : [created, edited, closed, opened]
69 issues :
710 types : [opened, edited, closed, reopened, deleted, milestoned, demilestoned]
811 pull_request :
912 types : [opened, edited, closed, reopened, milestoned, demilestoned]
10- push :
11- tags :
12- - ' *'
1313 workflow_dispatch :
1414 inputs :
1515 milestone :
@@ -30,96 +30,96 @@ jobs:
3030 script : |
3131 const { owner, repo } = context.repo;
3232
33- async function syncMilestone(milestone) {
34- const milestoneTitle = milestone.title;
35- const milestoneNumber = milestone.number;
36-
37- // Find existing release by tag_name (includes drafts)
38- let release = null;
33+ // Helper: Find release by tag name
34+ async function findRelease(tagName) {
3935 for await (const response of github.paginate.iterator(
4036 github.rest.repos.listReleases,
4137 { owner, repo, per_page: 100 }
4238 )) {
43- release = response.data.find(r => r.tag_name === milestoneTitle );
44- if (release) break ;
39+ const release = response.data.find(r => r.tag_name === tagName );
40+ if (release) return release ;
4541 }
42+ return null;
43+ }
4644
47- // Fetch all issues and PRs in milestone
45+ // Helper: Find milestone by title
46+ async function findMilestone(title) {
47+ for await (const response of github.paginate.iterator(
48+ github.rest.issues.listMilestones,
49+ { owner, repo, state: 'all', per_page: 100 }
50+ )) {
51+ const milestone = response.data.find(m => m.title === title);
52+ if (milestone) return milestone;
53+ }
54+ return null;
55+ }
56+
57+ // Helper: Generate release body from milestone
58+ async function generateBody(milestoneNumber) {
4859 const items = [];
4960 for await (const response of github.paginate.iterator(
5061 github.rest.issues.listForRepo,
5162 { owner, repo, milestone: milestoneNumber, state: 'all', per_page: 100 }
5263 )) {
5364 items.push(...response.data);
5465 }
55-
56- // Sort by number and generate body
5766 items.sort((a, b) => a.number - b.number);
58- const body = items.map(item => {
67+ return items.map(item => {
5968 const checkbox = item.state === 'closed' ? '[x]' : '[ ]';
6069 return `- ${checkbox} [#${item.number}](${item.html_url}) ${item.title}`;
61- }).join('\n');
62-
63- // Determine if release should be draft or published
64- const isDraft = milestone.state === 'open';
65-
66- if (release) {
67- await github.rest.repos.updateRelease({
68- owner, repo,
69- release_id: release.id,
70- name: milestoneTitle,
71- body: body || 'No issues in this milestone yet.',
72- draft: isDraft
73- });
74- console.log(`Updated release: ${milestoneTitle}`);
75- } else {
76- // Check if tag exists before creating release
77- let tagExists = false;
78- try {
79- await github.rest.git.getRef({
80- owner, repo,
81- ref: `tags/${milestoneTitle}`
82- });
83- tagExists = true;
84- } catch (error) {
85- if (error.status !== 404) throw error;
86- }
70+ }).join('\n') || 'No issues in this milestone yet.';
71+ }
8772
88- if (tagExists) {
89- await github.rest.repos.createRelease({
90- owner, repo,
91- tag_name: milestoneTitle,
92- name: milestoneTitle,
93- body: body || 'No issues in this milestone yet.',
94- draft: isDraft
95- });
96- console.log(`Created release: ${milestoneTitle}`);
97- } else {
98- console.log(`Skipping release creation: tag '${milestoneTitle}' does not exist`);
99- }
73+ // Helper: Update existing release only
74+ async function updateReleaseIfExists(milestone) {
75+ const release = await findRelease(milestone.title);
76+ if (!release) {
77+ console.log(`No release found for ${milestone.title}, skipping`);
78+ return;
10079 }
80+ const body = await generateBody(milestone.number);
81+ await github.rest.repos.updateRelease({
82+ owner, repo,
83+ release_id: release.id,
84+ body: body
85+ });
86+ console.log(`Updated release: ${milestone.title}`);
10187 }
10288
103- // Handle tag push - find matching milestone and sync
89+ // Handle tag push events
10490 if (context.eventName === 'push' && context.ref.startsWith('refs/tags/')) {
10591 const tagName = context.ref.replace('refs/tags/', '');
106- const milestones = [];
107- for await (const response of github.paginate.iterator(
108- github.rest.issues.listMilestones,
109- { owner, repo, state: 'all', per_page: 100 }
110- )) {
111- milestones.push(...response.data);
112- }
113- const milestone = milestones.find(ms => ms.title === tagName);
114- if (milestone) {
115- await syncMilestone(milestone);
116- } else {
117- console.log(`No milestone found matching tag '${tagName}'`);
92+
93+ // Tag deleted
94+ if (context.payload.deleted) {
95+ const release = await findRelease(tagName);
96+ if (release) {
97+ await github.rest.repos.deleteRelease({
98+ owner, repo, release_id: release.id
99+ });
100+ console.log(`Deleted release for tag: ${tagName}`);
101+ }
102+ return;
118103 }
104+
105+ // Tag created - create release
106+ const milestone = await findMilestone(tagName);
107+ const body = milestone
108+ ? await generateBody(milestone.number)
109+ : '';
110+
111+ await github.rest.repos.createRelease({
112+ owner, repo,
113+ tag_name: tagName,
114+ name: tagName,
115+ body: body,
116+ draft: false
117+ });
118+ console.log(`Created release for tag: ${tagName}`);
119119 return;
120120 }
121121
122- // Handle manual trigger - rebuild all or specific milestone
122+ // Handle workflow_dispatch - update only
123123 if (context.eventName === 'workflow_dispatch') {
124124 const inputMilestone = context.payload.inputs?.milestone;
125125 const milestones = [];
@@ -129,16 +129,15 @@ jobs:
129129 )) {
130130 milestones.push(...response.data);
131131 }
132-
133132 for (const ms of milestones) {
134133 if (!inputMilestone || ms.title === inputMilestone) {
135- await syncMilestone (ms);
134+ await updateReleaseIfExists (ms);
136135 }
137136 }
138137 return;
139138 }
140139
141- // Get milestone from event
140+ // Handle milestone/issue/PR events - update only
142141 let milestone = context.payload.milestone;
143142 if (!milestone && context.payload.issue?.milestone) {
144143 milestone = context.payload.issue.milestone;
@@ -151,26 +150,4 @@ jobs:
151150 return;
152151 }
153152
154- const eventAction = context.payload.action;
155-
156- // Handle milestone deleted
157- if (context.eventName === 'milestone' && eventAction === 'deleted') {
158- const milestoneTitle = milestone.title;
159- let release = null;
160- for await (const response of github.paginate.iterator(
161- github.rest.repos.listReleases,
162- { owner, repo, per_page: 100 }
163- )) {
164- release = response.data.find(r => r.tag_name === milestoneTitle);
165- if (release) break;
166- }
167- if (release) {
168- await github.rest.repos.deleteRelease({
169- owner, repo, release_id: release.id
170- });
171- console.log(`Deleted release for milestone: ${milestoneTitle}`);
172- }
173- return;
174- }
175-
176- await syncMilestone(milestone);
153+ await updateReleaseIfExists(milestone);
0 commit comments