@jira-deploy/core 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/defaults.js +1 -1
- package/constants/field-ids.js +4 -4
- package/package.json +1 -1
- package/poller.js +16 -3
- package/tools/cd.js +6 -4
- package/tools/grayrelease.js +546 -296
- package/tools/index.js +171 -106
- package/tools/release.js +48 -21
- package/tools/workflows.js +20 -0
- package/tools.test.js +444 -97
package/tools/release.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import https from 'https';
|
|
9
9
|
import http from 'http';
|
|
10
|
-
import {error, getModuleName, ok
|
|
11
|
-
import {SYSTEM_CODES, SYSTEM_MODULES} from '../constants/index.js';
|
|
10
|
+
import { error, getModuleName, ok } from './helpers.js';
|
|
11
|
+
import { SYSTEM_CODES, SYSTEM_MODULES } from '../constants/index.js';
|
|
12
12
|
|
|
13
13
|
// ── Schema definitions ───────────────────────────────────────────
|
|
14
14
|
export function getReleaseToolDefinitions() {
|
|
@@ -100,7 +100,7 @@ export function getReleaseToolDefinitions() {
|
|
|
100
100
|
|
|
101
101
|
// ── Handlers ─────────────────────────────────────────────────────
|
|
102
102
|
|
|
103
|
-
export async function handleGetUnreleasedVersions(args, {jira}) {
|
|
103
|
+
export async function handleGetUnreleasedVersions(args, { jira }) {
|
|
104
104
|
try {
|
|
105
105
|
const projectKey = args.projectKey ?? 'LBPRJ';
|
|
106
106
|
const versions = await jira.getUnreleasedVersionsList(projectKey);
|
|
@@ -109,7 +109,7 @@ export async function handleGetUnreleasedVersions(args, {jira}) {
|
|
|
109
109
|
return ok({
|
|
110
110
|
projectKey,
|
|
111
111
|
total: versions.length,
|
|
112
|
-
versions: versions.map((v) => ({id: v.id, name: v.name})),
|
|
112
|
+
versions: versions.map((v) => ({ id: v.id, name: v.name })),
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -135,13 +135,13 @@ export async function handleGetUnreleasedVersions(args, {jira}) {
|
|
|
135
135
|
};
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
return ok({projectKey, systemCode: args.systemCode, total: parsed.length, versions: parsed});
|
|
138
|
+
return ok({ projectKey, systemCode: args.systemCode, total: parsed.length, versions: parsed });
|
|
139
139
|
} catch (err) {
|
|
140
140
|
return error(`無法查詢 unreleased versions: ${err.message}`);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export async function handleTransitionToWaitApproval(args, {jira, notifier}) {
|
|
144
|
+
export async function handleTransitionToWaitApproval(args, { jira, notifier }) {
|
|
145
145
|
try {
|
|
146
146
|
await jira.transitionByName(args.cdIssueKey, 'Apply for approval');
|
|
147
147
|
await notifier.notify(args.cdIssueKey, '已切換到 Wait Approval,等待主管簽核');
|
|
@@ -157,7 +157,8 @@ export async function handleTransitionToWaitApproval(args, {jira, notifier}) {
|
|
|
157
157
|
|
|
158
158
|
export async function handleGetReleaseManager(args, _ctx) {
|
|
159
159
|
try {
|
|
160
|
-
|
|
160
|
+
|
|
161
|
+
const date = new Date().toISOString();
|
|
161
162
|
|
|
162
163
|
// dryRun 模式:回傳 mock 資料
|
|
163
164
|
if (args.dryRun) {
|
|
@@ -166,7 +167,7 @@ export async function handleGetReleaseManager(args, _ctx) {
|
|
|
166
167
|
found: true,
|
|
167
168
|
name: 'Alvin Wang',
|
|
168
169
|
dryRun: true,
|
|
169
|
-
event: {what: 'Sign off staff', who: 'Alvin Wang (BK00236)'},
|
|
170
|
+
event: { what: 'Sign off staff', who: 'Alvin Wang (BK00236)' },
|
|
170
171
|
});
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -177,42 +178,55 @@ export async function handleGetReleaseManager(args, _ctx) {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
// Release Manager 值班表的 sub-calendar ID(來自 wiki 頁 109861360)
|
|
180
|
-
const SUB_CALENDAR_ID = '
|
|
181
|
+
const SUB_CALENDAR_ID = '7e83502d-7994-4d90-87c2-9268e5d7d565';
|
|
181
182
|
|
|
182
183
|
const nextDay = new Date(date);
|
|
183
184
|
nextDay.setDate(nextDay.getDate() + 1);
|
|
184
|
-
const endDate = nextDay.toISOString()
|
|
185
|
+
const endDate = nextDay.toISOString();
|
|
185
186
|
|
|
186
187
|
const path =
|
|
187
188
|
`/rest/calendar-services/1.0/calendar/events.json` +
|
|
188
189
|
`?subCalendarId=${encodeURIComponent(SUB_CALENDAR_ID)}` +
|
|
189
|
-
`&start=${date}&end=${endDate}` +
|
|
190
|
+
`&start=${encodeURIComponent(date)}&end=${encodeURIComponent(endDate)}` +
|
|
190
191
|
`&userTimeZoneId=Asia%2FTaipei`;
|
|
191
192
|
|
|
192
193
|
const data = await calendarRequest(CONF_BASE_URL, path, CONF_TOKEN);
|
|
193
194
|
const events = data.events ?? [];
|
|
194
195
|
|
|
195
|
-
// 找
|
|
196
|
+
// 找 event title/what 欄位包含 "Sign off staff" 的事件。
|
|
196
197
|
const signOffEvent = events.find(
|
|
197
198
|
(e) =>
|
|
198
|
-
(e.
|
|
199
|
-
(e.
|
|
199
|
+
(e.title ?? '').toLowerCase().includes('sign off staff') ||
|
|
200
|
+
(e.what ?? '').toLowerCase().includes('sign off staff'),
|
|
200
201
|
);
|
|
201
202
|
|
|
202
203
|
if (!signOffEvent) {
|
|
203
204
|
return ok({
|
|
204
205
|
date,
|
|
205
206
|
found: false,
|
|
206
|
-
allEvents: events.map((e) => ({
|
|
207
|
+
allEvents: events.map((e) => ({
|
|
208
|
+
title: e.title,
|
|
209
|
+
what: e.what,
|
|
210
|
+
who: e.who,
|
|
211
|
+
})),
|
|
207
212
|
message: `${date} 找不到 Sign off staff 事件,請手動確認值班組長`,
|
|
208
213
|
});
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
// 回傳值班人員資訊
|
|
217
|
+
const invitee = signOffEvent.invitees?.[0];
|
|
218
|
+
const legacyWho = signOffEvent.who ?? '';
|
|
219
|
+
const legacyName = legacyWho.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
|
220
|
+
const legacyAccountId = legacyWho.match(/\(([^)]+)\)/)?.[1];
|
|
221
|
+
const managerName = (invitee?.displayName ?? legacyName)
|
|
222
|
+
|| signOffEvent.title
|
|
223
|
+
|| signOffEvent.what
|
|
224
|
+
|| 'Unknown';
|
|
212
225
|
return ok({
|
|
213
226
|
date,
|
|
214
227
|
found: true,
|
|
215
|
-
name:
|
|
228
|
+
name: managerName,
|
|
229
|
+
authorAccountId: invitee?.name ?? legacyAccountId ?? 'Unknown',
|
|
216
230
|
event: signOffEvent,
|
|
217
231
|
});
|
|
218
232
|
} catch (err) {
|
|
@@ -220,7 +234,7 @@ export async function handleGetReleaseManager(args, _ctx) {
|
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
236
|
|
|
223
|
-
export async function handleWaitForComment(args, {jira}) {
|
|
237
|
+
export async function handleWaitForComment(args, { jira, progress = () => {} }) {
|
|
224
238
|
// dryRun 模式:直接回傳 mock 結果
|
|
225
239
|
if (args.dryRun) {
|
|
226
240
|
return ok({
|
|
@@ -248,6 +262,7 @@ export async function handleWaitForComment(args, {jira}) {
|
|
|
248
262
|
while (true) {
|
|
249
263
|
attempts++;
|
|
250
264
|
const comments = await jira.getComments(args.issueKey);
|
|
265
|
+
const elapsedMs = Date.now() - startTime;
|
|
251
266
|
|
|
252
267
|
const match = comments.find((c) => {
|
|
253
268
|
const bodyMatch = (c.body ?? '').toLowerCase().includes(args.keyword.toLowerCase());
|
|
@@ -255,6 +270,18 @@ export async function handleWaitForComment(args, {jira}) {
|
|
|
255
270
|
return bodyMatch && authorMatch;
|
|
256
271
|
});
|
|
257
272
|
|
|
273
|
+
progress({
|
|
274
|
+
phase: 'polling',
|
|
275
|
+
title: '等待 Jira comment',
|
|
276
|
+
detail: `keyword="${args.keyword}"` +
|
|
277
|
+
(args.authorAccountId ? `, author=${args.authorAccountId}` : ''),
|
|
278
|
+
issueKey: args.issueKey,
|
|
279
|
+
attempts,
|
|
280
|
+
elapsedMs,
|
|
281
|
+
timeoutMs,
|
|
282
|
+
nextPollMs: intervalMs,
|
|
283
|
+
});
|
|
284
|
+
|
|
258
285
|
if (match) {
|
|
259
286
|
return ok({
|
|
260
287
|
found: true,
|
|
@@ -267,10 +294,9 @@ export async function handleWaitForComment(args, {jira}) {
|
|
|
267
294
|
});
|
|
268
295
|
}
|
|
269
296
|
|
|
270
|
-
|
|
271
|
-
if (elapsed >= timeoutMs) {
|
|
297
|
+
if (elapsedMs >= timeoutMs) {
|
|
272
298
|
return error(
|
|
273
|
-
`Timeout:等待 "${args.keyword}" comment in ${args.issueKey}(${attempts} 次輪詢,${Math.round(
|
|
299
|
+
`Timeout:等待 "${args.keyword}" comment in ${args.issueKey}(${attempts} 次輪詢,${Math.round(elapsedMs / 1000)}s)`,
|
|
274
300
|
);
|
|
275
301
|
}
|
|
276
302
|
|
|
@@ -292,7 +318,7 @@ function calendarRequest(baseUrl, path, token) {
|
|
|
292
318
|
path: url.pathname + url.search,
|
|
293
319
|
method: 'GET',
|
|
294
320
|
rejectUnauthorized: false,
|
|
295
|
-
headers: {Authorization: `Bearer ${token}`, Accept: 'application/json'},
|
|
321
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
296
322
|
},
|
|
297
323
|
(res) => {
|
|
298
324
|
let data = '';
|
|
@@ -300,6 +326,7 @@ function calendarRequest(baseUrl, path, token) {
|
|
|
300
326
|
res.on('end', () => {
|
|
301
327
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
302
328
|
try {
|
|
329
|
+
|
|
303
330
|
resolve(JSON.parse(data));
|
|
304
331
|
} catch (e) {
|
|
305
332
|
reject(new Error(`JSON parse error: ${e.message}`));
|
package/tools/workflows.js
CHANGED
|
@@ -80,6 +80,12 @@ function parseToolJson(result) {
|
|
|
80
80
|
|
|
81
81
|
async function runToolOrThrow(name, args, deps, workflowLog) {
|
|
82
82
|
workflowLog.push(`- ${name}: ${formatJson(args)}`);
|
|
83
|
+
deps.progress?.({
|
|
84
|
+
phase: 'action',
|
|
85
|
+
title: `執行 workflow step: ${name}`,
|
|
86
|
+
toolName: name,
|
|
87
|
+
issueKey: args.issueKey ?? args.cdIssueKey ?? args.linkedCiKey,
|
|
88
|
+
});
|
|
83
89
|
const result = await deps.executeToolImpl(name, args, deps);
|
|
84
90
|
return parseToolJson(result);
|
|
85
91
|
}
|
|
@@ -89,10 +95,24 @@ async function waitForIssueStatus(issueKey, targetStatuses, deps, options = {})
|
|
|
89
95
|
const intervalMs = options.intervalMs ?? 5000;
|
|
90
96
|
const startedAt = Date.now();
|
|
91
97
|
const wanted = targetStatuses.map((status) => status.toLowerCase());
|
|
98
|
+
let attempts = 0;
|
|
92
99
|
|
|
93
100
|
while (Date.now() - startedAt < timeoutMs) {
|
|
101
|
+
attempts++;
|
|
94
102
|
const issue = await deps.jira.getIssue(issueKey);
|
|
95
103
|
const current = issue.fields?.status?.name ?? '';
|
|
104
|
+
deps.progress?.({
|
|
105
|
+
phase: 'polling',
|
|
106
|
+
title: `等待 ${issueKey} workflow 狀態`,
|
|
107
|
+
detail: `target=${targetStatuses.join(' / ')}`,
|
|
108
|
+
issueKey,
|
|
109
|
+
currentStatus: current,
|
|
110
|
+
targetStatus: targetStatuses.join(' / '),
|
|
111
|
+
attempts,
|
|
112
|
+
elapsedMs: Date.now() - startedAt,
|
|
113
|
+
timeoutMs,
|
|
114
|
+
nextPollMs: intervalMs,
|
|
115
|
+
});
|
|
96
116
|
if (wanted.includes(current.toLowerCase())) {
|
|
97
117
|
return current;
|
|
98
118
|
}
|