@jira-deploy/core 1.0.15 → 1.0.17
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/config.js +34 -5
- package/constants/issue-types.js +2 -2
- package/index.js +1 -1
- package/jira-client.js +79 -44
- package/notifier.js +3 -2
- package/package.json +9 -2
- package/platform-config.js +2 -1
- package/poller.js +4 -2
- package/tools/branch-prs.js +164 -0
- package/tools/build.js +117 -0
- package/tools/cd.js +506 -119
- package/tools/ci.js +497 -29
- package/tools/deployment-helpers.js +103 -0
- package/tools/deployment.js +269 -0
- package/tools/grayrelease.js +244 -231
- package/tools/helpers.js +29 -7
- package/tools/index.js +131 -566
- package/tools/jabber.js +13 -5
- package/tools/library.js +221 -25
- package/tools/release.js +137 -38
- package/tools/transition-helpers.js +22 -0
- package/tools/workflows.js +388 -110
package/constants/config.js
CHANGED
|
@@ -28,6 +28,7 @@ const DEFAULT_DEPLOY_CONFIG = {
|
|
|
28
28
|
deptManagerSign: 'customfield_dept_manager_sign',
|
|
29
29
|
authManagerSign: 'customfield_auth_manager_sign',
|
|
30
30
|
releaseInfo: 'customfield_release_info',
|
|
31
|
+
buildResult: 'customfield_build_result',
|
|
31
32
|
},
|
|
32
33
|
cd: {
|
|
33
34
|
systemSubmodule: 'customfield_system_submodule',
|
|
@@ -49,12 +50,12 @@ const DEFAULT_DEPLOY_CONFIG = {
|
|
|
49
50
|
antiScanRequired: 'customfield_anti_scan_required',
|
|
50
51
|
ciNotes: 'customfield_ci_notes',
|
|
51
52
|
releaseVersion: 'customfield_release_version',
|
|
53
|
+
uploadResult: 'customfield_13452',
|
|
52
54
|
},
|
|
53
55
|
grayRelease: {
|
|
54
56
|
grayReleaseVersion: 'customfield_grayrelease_version',
|
|
55
57
|
clusterList: 'customfield_cluster_list',
|
|
56
58
|
grayReleaseNotes: 'customfield_grayrelease_notes',
|
|
57
|
-
buildResult: 'customfield_build_result',
|
|
58
59
|
deployResult: 'customfield_deploy_result',
|
|
59
60
|
},
|
|
60
61
|
},
|
|
@@ -112,11 +113,22 @@ const DEFAULT_DEPLOY_CONFIG = {
|
|
|
112
113
|
},
|
|
113
114
|
release: {
|
|
114
115
|
managerSubCalendarId: '',
|
|
116
|
+
birthdayCalendarId: '',
|
|
117
|
+
leaveCalendarId: '',
|
|
115
118
|
dryRunManager: { name: 'Demo User', accountId: 'demo-user' },
|
|
116
119
|
grayReleaseUatApprovers: {
|
|
117
120
|
commentReviewerAlias: 'uat-comment-reviewer',
|
|
118
121
|
finalApproverAlias: 'uat-final-approver',
|
|
119
122
|
},
|
|
123
|
+
managerSubstitutes: {},
|
|
124
|
+
cdApprovers: {
|
|
125
|
+
prd: {
|
|
126
|
+
leadReviewerAliases: [],
|
|
127
|
+
secondReviewerAlias: '',
|
|
128
|
+
thirdReviewerAlias: '',
|
|
129
|
+
finalApproverAlias: '',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
120
132
|
},
|
|
121
133
|
jabber: {
|
|
122
134
|
domain: 'example.internal',
|
|
@@ -124,6 +136,7 @@ const DEFAULT_DEPLOY_CONFIG = {
|
|
|
124
136
|
};
|
|
125
137
|
|
|
126
138
|
let cachedConfig;
|
|
139
|
+
let runtimeConfigEnv = EMPTY_OBJECT;
|
|
127
140
|
|
|
128
141
|
function parseJsonConfig(value, source) {
|
|
129
142
|
try {
|
|
@@ -157,16 +170,32 @@ function expandHomePath(filePath) {
|
|
|
157
170
|
}
|
|
158
171
|
|
|
159
172
|
function loadExternalConfig() {
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
const deployConfigPath = getRuntimeConfigValue('JIRA_DEPLOY_CONFIG_PATH');
|
|
174
|
+
if (deployConfigPath) {
|
|
175
|
+
const configPath = expandHomePath(deployConfigPath);
|
|
162
176
|
return parseJsonConfig(
|
|
163
177
|
readFileSync(configPath, 'utf8'),
|
|
164
|
-
`JIRA_DEPLOY_CONFIG_PATH (${
|
|
178
|
+
`JIRA_DEPLOY_CONFIG_PATH (${deployConfigPath})`,
|
|
165
179
|
);
|
|
166
180
|
}
|
|
167
181
|
return EMPTY_OBJECT;
|
|
168
182
|
}
|
|
169
183
|
|
|
184
|
+
export function configureRuntimeConfig({env = EMPTY_OBJECT} = {}) {
|
|
185
|
+
runtimeConfigEnv = Object.freeze({...env});
|
|
186
|
+
cachedConfig = undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getRuntimeConfigValue(key) {
|
|
190
|
+
return runtimeConfigEnv[key] ?? process.env[key];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getRuntimeConfigNumber(key, fallback) {
|
|
194
|
+
const value = getRuntimeConfigValue(key);
|
|
195
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
196
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
197
|
+
}
|
|
198
|
+
|
|
170
199
|
export function getDeployConfig() {
|
|
171
200
|
if (!cachedConfig) {
|
|
172
201
|
cachedConfig = deepMerge(DEFAULT_DEPLOY_CONFIG, loadExternalConfig());
|
|
@@ -175,7 +204,7 @@ export function getDeployConfig() {
|
|
|
175
204
|
}
|
|
176
205
|
|
|
177
206
|
export function getReleaseProjectKey() {
|
|
178
|
-
return
|
|
207
|
+
return getRuntimeConfigValue('JIRA_RELEASE_PROJECT_KEY') ?? getDeployConfig().jira.releaseProjectKey;
|
|
179
208
|
}
|
|
180
209
|
|
|
181
210
|
export function resetDeployConfigForTests() {
|
package/constants/issue-types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {getDeployConfig} from './config.js';
|
|
1
|
+
import {getDeployConfig, getRuntimeConfigValue} from './config.js';
|
|
2
2
|
|
|
3
3
|
const config = getDeployConfig();
|
|
4
4
|
|
|
@@ -8,4 +8,4 @@ export const ISSUE_TYPE_IDS = config.issueTypes.ids;
|
|
|
8
8
|
|
|
9
9
|
export const JIRA_PROJECT_ID_NUMERIC = config.jira.projectIdNumeric;
|
|
10
10
|
|
|
11
|
-
export const JIRA_PROJECT_ID =
|
|
11
|
+
export const JIRA_PROJECT_ID = getRuntimeConfigValue('JIRA_PROJECT_KEY') ?? config.jira.projectKey;
|
package/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export {Notifier} from './notifier.js';
|
|
|
3
3
|
export {Poller} from './poller.js';
|
|
4
4
|
export {getPlatformConfig, PLATFORM_TO_SYSTEM_CODE, PLATFORMS} from './platform-config.js';
|
|
5
5
|
export {getToolDefinitions, executeTool} from './tools/index.js';
|
|
6
|
-
export {
|
|
6
|
+
export {getServerList, ok, error, today} from './tools/helpers.js';
|
package/jira-client.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import https from 'https';
|
|
3
|
+
import {getRuntimeConfigValue} from './constants/config.js';
|
|
3
4
|
|
|
4
|
-
const httpsAgent = new https.Agent({rejectUnauthorized: false});
|
|
5
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
function isDryRun() {
|
|
8
|
+
return getRuntimeConfigValue('DRY_RUN') === 'true';
|
|
9
|
+
}
|
|
7
10
|
|
|
8
11
|
export class JiraClient {
|
|
9
12
|
constructor() {
|
|
10
|
-
const
|
|
13
|
+
const JIRA_BASE_URL = getRuntimeConfigValue('JIRA_BASE_URL');
|
|
14
|
+
const JIRA_API_TOKEN = getRuntimeConfigValue('JIRA_API_TOKEN');
|
|
11
15
|
if (!JIRA_BASE_URL || !JIRA_API_TOKEN) {
|
|
12
16
|
throw new Error('Missing required Jira env vars: JIRA_BASE_URL, JIRA_API_TOKEN');
|
|
13
17
|
}
|
|
@@ -20,7 +24,7 @@ export class JiraClient {
|
|
|
20
24
|
Authorization: `Bearer ${JIRA_API_TOKEN}`,
|
|
21
25
|
},
|
|
22
26
|
});
|
|
23
|
-
this.dryRun =
|
|
27
|
+
this.dryRun = isDryRun();
|
|
24
28
|
this.fieldIdByName = new Map();
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -28,9 +32,9 @@ export class JiraClient {
|
|
|
28
32
|
async createIssue(fields) {
|
|
29
33
|
if (this.dryRun) {
|
|
30
34
|
console.log('[DRY RUN] JiraClient.createIssue', JSON.stringify(fields).slice(0, 1000));
|
|
31
|
-
return {id: 'DRY-RUN', key: 'DRY-RUN', self: ''};
|
|
35
|
+
return { id: 'DRY-RUN', key: 'DRY-RUN', self: '' };
|
|
32
36
|
}
|
|
33
|
-
const res = await this.http.post('/issue', {fields}).catch((err) => {
|
|
37
|
+
const res = await this.http.post('/issue', { fields }).catch((err) => {
|
|
34
38
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
35
39
|
throw new Error(`Create issue failed: ${detail}`);
|
|
36
40
|
});
|
|
@@ -45,22 +49,22 @@ export class JiraClient {
|
|
|
45
49
|
issueKey,
|
|
46
50
|
JSON.stringify(fields).slice(0, 1000),
|
|
47
51
|
);
|
|
48
|
-
return {issueKey, updated: Object.keys(fields)};
|
|
52
|
+
return { issueKey, updated: Object.keys(fields) };
|
|
49
53
|
}
|
|
50
|
-
await this.http.put(`/issue/${issueKey}`, {fields});
|
|
51
|
-
return {issueKey, updated: Object.keys(fields)};
|
|
54
|
+
await this.http.put(`/issue/${issueKey}`, { fields });
|
|
55
|
+
return { issueKey, updated: Object.keys(fields) };
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
// 更新 Assignee(使用專用 endpoint,Jira Server 相容)
|
|
55
59
|
async updateAssignee(issueKey, accountId) {
|
|
56
60
|
if (this.dryRun) {
|
|
57
61
|
console.log('[DRY RUN] JiraClient.updateAssignee', issueKey, accountId);
|
|
58
|
-
return {issueKey, accountId};
|
|
62
|
+
return { issueKey, accountId };
|
|
59
63
|
}
|
|
60
|
-
await this.http.put(`/issue/${issueKey}/assignee`, {name: accountId}).catch(async (err) => {
|
|
64
|
+
await this.http.put(`/issue/${issueKey}/assignee`, { name: accountId }).catch(async (err) => {
|
|
61
65
|
// 若 name 格式失敗,改試 accountId 格式(Jira Cloud)
|
|
62
66
|
if (err.response?.status === 400) {
|
|
63
|
-
await this.http.put(`/issue/${issueKey}/assignee`, {accountId}).catch((e) => {
|
|
67
|
+
await this.http.put(`/issue/${issueKey}/assignee`, { accountId }).catch((e) => {
|
|
64
68
|
const detail = e.response?.data ? JSON.stringify(e.response.data) : e.message;
|
|
65
69
|
throw new Error(`updateAssignee failed: ${detail}`);
|
|
66
70
|
});
|
|
@@ -69,18 +73,18 @@ export class JiraClient {
|
|
|
69
73
|
throw new Error(`updateAssignee failed: ${detail}`);
|
|
70
74
|
}
|
|
71
75
|
});
|
|
72
|
-
return {issueKey, accountId};
|
|
76
|
+
return { issueKey, accountId };
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
// 查詢 issue 目前狀態
|
|
76
80
|
async getIssue(issueKey) {
|
|
77
81
|
const res = await this.http.get(`/issue/${issueKey}`, {
|
|
78
|
-
params: {fields: 'status,summary,assignee,comment'},
|
|
82
|
+
params: { fields: 'status,summary,assignee,comment' },
|
|
79
83
|
});
|
|
80
84
|
return res.data;
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
//
|
|
87
|
+
// 取得當前狀態可執行的 transitions (可以執行的狀態切換列表)
|
|
84
88
|
async getTransitions(issueKey) {
|
|
85
89
|
const res = await this.http.get(`/issue/${issueKey}/transitions`);
|
|
86
90
|
return res.data.transitions; // [{ id, name, to: { name } }]
|
|
@@ -89,7 +93,7 @@ export class JiraClient {
|
|
|
89
93
|
// 查詢 issue 的指定欄位(例如取得 CI 單的 CID_release_version)
|
|
90
94
|
async getIssueFields(issueKey, fields = []) {
|
|
91
95
|
const res = await this.http.get(`/issue/${issueKey}`, {
|
|
92
|
-
params: {fields: fields.join(',')},
|
|
96
|
+
params: { fields: fields.join(',') },
|
|
93
97
|
});
|
|
94
98
|
return res.data.fields;
|
|
95
99
|
}
|
|
@@ -110,24 +114,24 @@ export class JiraClient {
|
|
|
110
114
|
async transitionById(issueKey, transitionId) {
|
|
111
115
|
if (this.dryRun) {
|
|
112
116
|
console.log('[DRY RUN] JiraClient.transitionById', issueKey, transitionId);
|
|
113
|
-
return {issueKey, transitionId};
|
|
117
|
+
return { issueKey, transitionId };
|
|
114
118
|
}
|
|
115
119
|
await this.http
|
|
116
120
|
.post(`/issue/${issueKey}/transitions`, {
|
|
117
|
-
transition: {id: transitionId},
|
|
121
|
+
transition: { id: transitionId },
|
|
118
122
|
})
|
|
119
123
|
.catch((err) => {
|
|
120
124
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
121
125
|
throw new Error(`Transition id=${transitionId} failed: ${detail}`);
|
|
122
126
|
});
|
|
123
|
-
return {issueKey, transitionId};
|
|
127
|
+
return { issueKey, transitionId };
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// 依名稱切換狀態(不用記 id)
|
|
127
131
|
async transitionByName(issueKey, transitionName) {
|
|
128
132
|
if (this.dryRun) {
|
|
129
133
|
console.log('[DRY RUN] JiraClient.transitionByName', issueKey, transitionName);
|
|
130
|
-
return {issueKey, transitioned: transitionName, toStatus: 'DRY'};
|
|
134
|
+
return { issueKey, transitioned: transitionName, toStatus: 'DRY' };
|
|
131
135
|
}
|
|
132
136
|
const transitions = await this.getTransitions(issueKey);
|
|
133
137
|
const match = transitions.find((t) => t.name.toLowerCase() === transitionName.toLowerCase());
|
|
@@ -137,34 +141,34 @@ export class JiraClient {
|
|
|
137
141
|
}
|
|
138
142
|
await this.http
|
|
139
143
|
.post(`/issue/${issueKey}/transitions`, {
|
|
140
|
-
transition: {id: match.id},
|
|
144
|
+
transition: { id: match.id },
|
|
141
145
|
})
|
|
142
146
|
.catch((err) => {
|
|
143
147
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
144
148
|
throw new Error(`Transition "${match.name}" failed: ${detail}`);
|
|
145
149
|
});
|
|
146
|
-
return {issueKey, transitioned: match.name, toStatus: match.to.name};
|
|
150
|
+
return { issueKey, transitioned: match.name, toStatus: match.to.name };
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// 新增 issue link(relates to / blocks / is blocked by 等)
|
|
150
154
|
async linkIssue(inwardKey, outwardKey, linkTypeName = 'Relates') {
|
|
151
155
|
if (this.dryRun) {
|
|
152
156
|
console.log('[DRY RUN] JiraClient.linkIssue', inwardKey, outwardKey, linkTypeName);
|
|
153
|
-
return {inwardKey, outwardKey, linkType: linkTypeName};
|
|
157
|
+
return { inwardKey, outwardKey, linkType: linkTypeName };
|
|
154
158
|
}
|
|
155
159
|
await this.http.post('/issueLink', {
|
|
156
|
-
type: {name: linkTypeName},
|
|
157
|
-
inwardIssue: {key: inwardKey},
|
|
158
|
-
outwardIssue: {key: outwardKey},
|
|
160
|
+
type: { name: linkTypeName },
|
|
161
|
+
inwardIssue: { key: inwardKey },
|
|
162
|
+
outwardIssue: { key: outwardKey },
|
|
159
163
|
});
|
|
160
|
-
return {inwardKey, outwardKey, linkType: linkTypeName};
|
|
164
|
+
return { inwardKey, outwardKey, linkType: linkTypeName };
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
// 新增 comment(/rest/api/2 用純文字 body)
|
|
164
168
|
async addComment(issueKey, text) {
|
|
165
169
|
if (this.dryRun) {
|
|
166
170
|
console.log('[DRY RUN] JiraClient.addComment', issueKey, text);
|
|
167
|
-
return {issueKey, body: text};
|
|
171
|
+
return { issueKey, body: text };
|
|
168
172
|
}
|
|
169
173
|
const res = await this.http.post(`/issue/${issueKey}/comment`, {
|
|
170
174
|
body: text,
|
|
@@ -176,11 +180,11 @@ export class JiraClient {
|
|
|
176
180
|
async addRemoteLink(issueKey, url, title) {
|
|
177
181
|
if (this.dryRun) {
|
|
178
182
|
console.log('[DRY RUN] JiraClient.addRemoteLink', issueKey, url, title);
|
|
179
|
-
return {issueKey, url, title};
|
|
183
|
+
return { issueKey, url, title };
|
|
180
184
|
}
|
|
181
185
|
const res = await this.http
|
|
182
186
|
.post(`/issue/${issueKey}/remotelink`, {
|
|
183
|
-
object: {url, title},
|
|
187
|
+
object: { url, title },
|
|
184
188
|
})
|
|
185
189
|
.catch((err) => {
|
|
186
190
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
@@ -197,7 +201,7 @@ export class JiraClient {
|
|
|
197
201
|
//
|
|
198
202
|
// status: 'unreleased'
|
|
199
203
|
// query: 相當於 UI 的 contains 參數(版本名稱包含此字串,不分大小寫)
|
|
200
|
-
async getProjectVersions(projectKey, {name = {}}) {
|
|
204
|
+
async getProjectVersions(projectKey, { name = {} }) {
|
|
201
205
|
const res = await this.http.get(`/project/${projectKey}/versions`);
|
|
202
206
|
let versions = res.data ?? [];
|
|
203
207
|
|
|
@@ -231,7 +235,7 @@ export class JiraClient {
|
|
|
231
235
|
async getSubTasks(issueKey) {
|
|
232
236
|
const res = await this.http
|
|
233
237
|
.get(`/issue/${issueKey}`, {
|
|
234
|
-
params: {fields: 'subtasks,issuetype'},
|
|
238
|
+
params: { fields: 'subtasks,issuetype' },
|
|
235
239
|
})
|
|
236
240
|
.catch((err) => {
|
|
237
241
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
@@ -244,7 +248,7 @@ export class JiraClient {
|
|
|
244
248
|
async searchIssues(jql, fields = [], maxResults = 10) {
|
|
245
249
|
const res = await this.http
|
|
246
250
|
.get('/search', {
|
|
247
|
-
params: {jql, fields: fields.join(','), maxResults},
|
|
251
|
+
params: { jql, fields: fields.join(','), maxResults },
|
|
248
252
|
})
|
|
249
253
|
.catch((err) => {
|
|
250
254
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
@@ -255,7 +259,7 @@ export class JiraClient {
|
|
|
255
259
|
|
|
256
260
|
// 取得 Bitbucket repo 的原始檔案內容(Bitbucket REST API 1.0)
|
|
257
261
|
async getBitbucketFileContent(project, repo, filePath, branch) {
|
|
258
|
-
const BB_BASE =
|
|
262
|
+
const BB_BASE = getRuntimeConfigValue('BITBUCKET_URL') ?? getRuntimeConfigValue('BITBUCKET_BASE_URL');
|
|
259
263
|
if (!BB_BASE) throw new Error('Missing required Bitbucket env var: BITBUCKET_URL');
|
|
260
264
|
const url = `${BB_BASE}/rest/api/1.0/projects/${project}/repos/${repo}/raw/${filePath}`;
|
|
261
265
|
const res = await axios
|
|
@@ -263,9 +267,9 @@ export class JiraClient {
|
|
|
263
267
|
httpsAgent,
|
|
264
268
|
headers: {
|
|
265
269
|
Accept: 'text/plain',
|
|
266
|
-
Authorization: `Bearer ${
|
|
270
|
+
Authorization: `Bearer ${getRuntimeConfigValue('BITBUCKET_API_TOKEN')}`,
|
|
267
271
|
},
|
|
268
|
-
params: {at: branch},
|
|
272
|
+
params: { at: branch },
|
|
269
273
|
responseType: 'text',
|
|
270
274
|
})
|
|
271
275
|
.catch((err) => {
|
|
@@ -279,9 +283,9 @@ export class JiraClient {
|
|
|
279
283
|
async getBitbucketTags(
|
|
280
284
|
project,
|
|
281
285
|
repo,
|
|
282
|
-
{filterValue = '', orderBy = 'MODIFICATION', limit = 1} = {},
|
|
286
|
+
{ filterValue = '', orderBy = 'MODIFICATION', limit = 1 } = {},
|
|
283
287
|
) {
|
|
284
|
-
const BB_BASE =
|
|
288
|
+
const BB_BASE = getRuntimeConfigValue('BITBUCKET_URL') ?? getRuntimeConfigValue('BITBUCKET_BASE_URL');
|
|
285
289
|
if (!BB_BASE) throw new Error('Missing required Bitbucket env var: BITBUCKET_URL');
|
|
286
290
|
const url = `${BB_BASE}/rest/api/1.0/projects/${project}/repos/${repo}/tags`;
|
|
287
291
|
const res = await axios
|
|
@@ -290,9 +294,9 @@ export class JiraClient {
|
|
|
290
294
|
headers: {
|
|
291
295
|
'Content-Type': 'application/json',
|
|
292
296
|
Accept: 'application/json',
|
|
293
|
-
Authorization: `Bearer ${
|
|
297
|
+
Authorization: `Bearer ${getRuntimeConfigValue('BITBUCKET_API_TOKEN')}`,
|
|
294
298
|
},
|
|
295
|
-
params: {filterValue, orderBy, limit},
|
|
299
|
+
params: { filterValue, orderBy, limit },
|
|
296
300
|
})
|
|
297
301
|
.catch((err) => {
|
|
298
302
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
@@ -305,9 +309,9 @@ export class JiraClient {
|
|
|
305
309
|
async getBitbucketBranches(
|
|
306
310
|
project,
|
|
307
311
|
repo,
|
|
308
|
-
{filterValue = '', orderBy = 'MODIFICATION', limit = 1} = {},
|
|
312
|
+
{ filterValue = '', orderBy = 'MODIFICATION', limit = 1 } = {},
|
|
309
313
|
) {
|
|
310
|
-
const BB_BASE =
|
|
314
|
+
const BB_BASE = getRuntimeConfigValue('BITBUCKET_URL') ?? getRuntimeConfigValue('BITBUCKET_BASE_URL');
|
|
311
315
|
if (!BB_BASE) throw new Error('Missing required Bitbucket env var: BITBUCKET_URL');
|
|
312
316
|
const url = `${BB_BASE}/rest/api/1.0/projects/${project}/repos/${repo}/branches`;
|
|
313
317
|
const res = await axios
|
|
@@ -316,9 +320,9 @@ export class JiraClient {
|
|
|
316
320
|
headers: {
|
|
317
321
|
'Content-Type': 'application/json',
|
|
318
322
|
Accept: 'application/json',
|
|
319
|
-
Authorization: `Bearer ${
|
|
323
|
+
Authorization: `Bearer ${getRuntimeConfigValue('BITBUCKET_API_TOKEN')}`,
|
|
320
324
|
},
|
|
321
|
-
params: {filterValue, orderBy, limit},
|
|
325
|
+
params: { filterValue, orderBy, limit },
|
|
322
326
|
})
|
|
323
327
|
.catch((err) => {
|
|
324
328
|
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
@@ -326,4 +330,35 @@ export class JiraClient {
|
|
|
326
330
|
});
|
|
327
331
|
return res.data?.values ?? [];
|
|
328
332
|
}
|
|
333
|
+
|
|
334
|
+
// 取得 Bitbucket repo 的 pull requests(Bitbucket REST API 1.0)
|
|
335
|
+
async getBitbucketPullRequests(
|
|
336
|
+
project,
|
|
337
|
+
repo,
|
|
338
|
+
{ branch = '', state = 'OPEN', limit = 100 } = {},
|
|
339
|
+
) {
|
|
340
|
+
const BB_BASE = getRuntimeConfigValue('BITBUCKET_URL') ?? getRuntimeConfigValue('BITBUCKET_BASE_URL');
|
|
341
|
+
if (!BB_BASE) throw new Error('Missing required Bitbucket env var: BITBUCKET_URL');
|
|
342
|
+
const url = `${BB_BASE}/rest/api/1.0/projects/${project}/repos/${repo}/pull-requests`;
|
|
343
|
+
const res = await axios
|
|
344
|
+
.get(url, {
|
|
345
|
+
httpsAgent,
|
|
346
|
+
headers: {
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
Accept: 'application/json',
|
|
349
|
+
Authorization: `Bearer ${getRuntimeConfigValue('BITBUCKET_API_TOKEN')}`,
|
|
350
|
+
},
|
|
351
|
+
params: {
|
|
352
|
+
state,
|
|
353
|
+
limit,
|
|
354
|
+
direction: 'OUTGOING',
|
|
355
|
+
...(branch ? { at: `refs/heads/${branch}` } : {}),
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
.catch((err) => {
|
|
359
|
+
const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
360
|
+
throw new Error(`getBitbucketPullRequests failed: ${detail}`);
|
|
361
|
+
});
|
|
362
|
+
return res.data?.values ?? [];
|
|
363
|
+
}
|
|
329
364
|
}
|
package/notifier.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* 擴充 Slack:填入 .env 的 SLACK_BOT_TOKEN + SLACK_CHANNEL_ID 後,
|
|
6
6
|
* 把下方 notifySlack 的 TODO 實作即可,其他地方不用動。
|
|
7
7
|
*/
|
|
8
|
+
import {getRuntimeConfigValue} from './constants/config.js';
|
|
8
9
|
|
|
9
10
|
export class Notifier {
|
|
10
11
|
constructor(jiraClient) {
|
|
11
12
|
this.jira = jiraClient;
|
|
12
|
-
this.slackEnabled = !!
|
|
13
|
-
this.dryRun =
|
|
13
|
+
this.slackEnabled = !!getRuntimeConfigValue('SLACK_BOT_TOKEN') && !!getRuntimeConfigValue('SLACK_CHANNEL_ID');
|
|
14
|
+
this.dryRun = getRuntimeConfigValue('DRY_RUN') === 'true';
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
async notify(issueKey, message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jira-deploy/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"repository": {
|
|
@@ -11,24 +11,31 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./index.js",
|
|
13
13
|
"./constants": "./constants/index.js",
|
|
14
|
+
"./runtime-config": "./constants/config.js",
|
|
14
15
|
"./jira-client": "./jira-client.js",
|
|
15
16
|
"./notifier": "./notifier.js",
|
|
16
17
|
"./poller": "./poller.js",
|
|
17
18
|
"./platform-config": "./platform-config.js",
|
|
18
19
|
"./tools": "./tools/index.js",
|
|
20
|
+
"./tools/build": "./tools/build.js",
|
|
19
21
|
"./tools/helpers": "./tools/helpers.js",
|
|
20
22
|
"./tools/library": "./tools/library.js"
|
|
21
23
|
},
|
|
22
24
|
"files": [
|
|
23
25
|
"constants/**/*.js",
|
|
24
26
|
"scripts/jabber_notify.py",
|
|
27
|
+
"tools/branch-prs.js",
|
|
28
|
+
"tools/build.js",
|
|
25
29
|
"tools/cd.js",
|
|
26
30
|
"tools/ci.js",
|
|
31
|
+
"tools/deployment.js",
|
|
32
|
+
"tools/deployment-helpers.js",
|
|
27
33
|
"tools/grayrelease.js",
|
|
28
34
|
"tools/helpers.js",
|
|
29
35
|
"tools/index.js",
|
|
30
36
|
"tools/jabber.js",
|
|
31
37
|
"tools/library.js",
|
|
38
|
+
"tools/transition-helpers.js",
|
|
32
39
|
"tools/release.js",
|
|
33
40
|
"tools/workflows.js",
|
|
34
41
|
"index.js",
|
|
@@ -42,6 +49,6 @@
|
|
|
42
49
|
"dotenv": "^16.3.0"
|
|
43
50
|
},
|
|
44
51
|
"scripts": {
|
|
45
|
-
"test": "node --import ./test-env.js --test tools.test.js tools/jabber.test.js config.test.js"
|
|
52
|
+
"test": "node --import ./test-env.js --test tools.test.js tools/jabber.test.js tools/transition-helpers.test.js config.test.js"
|
|
46
53
|
}
|
|
47
54
|
}
|
package/platform-config.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {SYSTEM_CODES, GRAY_RELEASE_MODULE_IDS} from './constants/index.js';
|
|
9
|
+
import {getRuntimeConfigValue} from './constants/config.js';
|
|
9
10
|
import {getServerList} from './tools/helpers.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -27,7 +28,7 @@ export function getPlatformConfig(platformName, environment = 'stg') {
|
|
|
27
28
|
if (!systemCode) {
|
|
28
29
|
console.warn(`Unknown platform: ${platformName}, using defaults`);
|
|
29
30
|
return {
|
|
30
|
-
projectKey:
|
|
31
|
+
projectKey: getRuntimeConfigValue('JIRA_PROJECT_KEY') ?? 'OPS',
|
|
31
32
|
systemCode: undefined,
|
|
32
33
|
clusters: [],
|
|
33
34
|
environment,
|
package/poller.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {getRuntimeConfigNumber} from './constants/config.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Poller — 輪詢等待 Jira issue 達到目標狀態
|
|
3
5
|
*
|
|
@@ -19,8 +21,8 @@ export class Poller {
|
|
|
19
21
|
* @returns {{ issueKey, status, elapsedMs, attempts }}
|
|
20
22
|
*/
|
|
21
23
|
async waitForStatus(issueKey, targetStatus, options = {}) {
|
|
22
|
-
const intervalMs = options.intervalMs ??
|
|
23
|
-
const timeoutMs = options.timeoutMs ??
|
|
24
|
+
const intervalMs = options.intervalMs ?? getRuntimeConfigNumber('POLL_INTERVAL_MS', 30000);
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? getRuntimeConfigNumber('POLL_TIMEOUT_MS', 3600000);
|
|
24
26
|
const progress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
25
27
|
|
|
26
28
|
const startTime = Date.now();
|