@jira-deploy/core 1.0.16 → 1.0.18
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 +22 -4
- package/constants/issue-types.js +2 -2
- package/jira-client.js +15 -11
- package/notifier.js +3 -2
- package/package.json +2 -1
- package/platform-config.js +2 -1
- package/poller.js +4 -2
- package/tools/cd.js +6 -5
- package/tools/ci.js +2 -1
- package/tools/grayrelease.js +21 -5
- package/tools/helpers.js +4 -3
- package/tools/jabber.js +13 -5
- package/tools/library.js +2 -1
- package/tools/release.js +6 -5
package/constants/config.js
CHANGED
|
@@ -57,6 +57,7 @@ const DEFAULT_DEPLOY_CONFIG = {
|
|
|
57
57
|
clusterList: 'customfield_cluster_list',
|
|
58
58
|
grayReleaseNotes: 'customfield_grayrelease_notes',
|
|
59
59
|
deployResult: 'customfield_deploy_result',
|
|
60
|
+
extraVars: 'customfield_extra_vars',
|
|
60
61
|
},
|
|
61
62
|
},
|
|
62
63
|
issueTypes: {
|
|
@@ -136,6 +137,7 @@ const DEFAULT_DEPLOY_CONFIG = {
|
|
|
136
137
|
};
|
|
137
138
|
|
|
138
139
|
let cachedConfig;
|
|
140
|
+
let runtimeConfigEnv = EMPTY_OBJECT;
|
|
139
141
|
|
|
140
142
|
function parseJsonConfig(value, source) {
|
|
141
143
|
try {
|
|
@@ -169,16 +171,32 @@ function expandHomePath(filePath) {
|
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
function loadExternalConfig() {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
const deployConfigPath = getRuntimeConfigValue('JIRA_DEPLOY_CONFIG_PATH');
|
|
175
|
+
if (deployConfigPath) {
|
|
176
|
+
const configPath = expandHomePath(deployConfigPath);
|
|
174
177
|
return parseJsonConfig(
|
|
175
178
|
readFileSync(configPath, 'utf8'),
|
|
176
|
-
`JIRA_DEPLOY_CONFIG_PATH (${
|
|
179
|
+
`JIRA_DEPLOY_CONFIG_PATH (${deployConfigPath})`,
|
|
177
180
|
);
|
|
178
181
|
}
|
|
179
182
|
return EMPTY_OBJECT;
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
export function configureRuntimeConfig({env = EMPTY_OBJECT} = {}) {
|
|
186
|
+
runtimeConfigEnv = Object.freeze({...env});
|
|
187
|
+
cachedConfig = undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getRuntimeConfigValue(key) {
|
|
191
|
+
return runtimeConfigEnv[key] ?? process.env[key];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function getRuntimeConfigNumber(key, fallback) {
|
|
195
|
+
const value = getRuntimeConfigValue(key);
|
|
196
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
197
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
export function getDeployConfig() {
|
|
183
201
|
if (!cachedConfig) {
|
|
184
202
|
cachedConfig = deepMerge(DEFAULT_DEPLOY_CONFIG, loadExternalConfig());
|
|
@@ -187,7 +205,7 @@ export function getDeployConfig() {
|
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
export function getReleaseProjectKey() {
|
|
190
|
-
return
|
|
208
|
+
return getRuntimeConfigValue('JIRA_RELEASE_PROJECT_KEY') ?? getDeployConfig().jira.releaseProjectKey;
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
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/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
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
|
|
|
@@ -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,7 +267,7 @@ 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
272
|
params: { at: branch },
|
|
269
273
|
responseType: 'text',
|
|
@@ -281,7 +285,7 @@ export class JiraClient {
|
|
|
281
285
|
repo,
|
|
282
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,7 +294,7 @@ 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
299
|
params: { filterValue, orderBy, limit },
|
|
296
300
|
})
|
|
@@ -307,7 +311,7 @@ export class JiraClient {
|
|
|
307
311
|
repo,
|
|
308
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,7 +320,7 @@ 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
325
|
params: { filterValue, orderBy, limit },
|
|
322
326
|
})
|
|
@@ -333,7 +337,7 @@ export class JiraClient {
|
|
|
333
337
|
repo,
|
|
334
338
|
{ branch = '', state = 'OPEN', limit = 100 } = {},
|
|
335
339
|
) {
|
|
336
|
-
const BB_BASE =
|
|
340
|
+
const BB_BASE = getRuntimeConfigValue('BITBUCKET_URL') ?? getRuntimeConfigValue('BITBUCKET_BASE_URL');
|
|
337
341
|
if (!BB_BASE) throw new Error('Missing required Bitbucket env var: BITBUCKET_URL');
|
|
338
342
|
const url = `${BB_BASE}/rest/api/1.0/projects/${project}/repos/${repo}/pull-requests`;
|
|
339
343
|
const res = await axios
|
|
@@ -342,7 +346,7 @@ export class JiraClient {
|
|
|
342
346
|
headers: {
|
|
343
347
|
'Content-Type': 'application/json',
|
|
344
348
|
Accept: 'application/json',
|
|
345
|
-
Authorization: `Bearer ${
|
|
349
|
+
Authorization: `Bearer ${getRuntimeConfigValue('BITBUCKET_API_TOKEN')}`,
|
|
346
350
|
},
|
|
347
351
|
params: {
|
|
348
352
|
state,
|
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.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"repository": {
|
|
@@ -11,6 +11,7 @@
|
|
|
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",
|
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();
|
package/tools/cd.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
SYSTEM_MODULES,
|
|
26
26
|
SYSTEM_TO_DEPT_MAP,
|
|
27
27
|
} from '../constants/index.js';
|
|
28
|
+
import {getRuntimeConfigValue} from '../constants/config.js';
|
|
28
29
|
|
|
29
30
|
// ── Flow Definition ──────────────────────────────────────────────
|
|
30
31
|
|
|
@@ -291,7 +292,7 @@ export async function handleCreateCDTicket(args, {jira, notifier, progress: repo
|
|
|
291
292
|
return ok({
|
|
292
293
|
issueKey: issue.key,
|
|
293
294
|
issueId: issue.id,
|
|
294
|
-
url:
|
|
295
|
+
url: getIssueUrl(issue.key),
|
|
295
296
|
type: 'CD Deploy',
|
|
296
297
|
system: normalizedArgs.systemCode,
|
|
297
298
|
environment: normalizedArgs.environment,
|
|
@@ -615,12 +616,12 @@ function resolveRequiredAccountId(alias, label) {
|
|
|
615
616
|
}
|
|
616
617
|
|
|
617
618
|
function getJabberJid(accountId) {
|
|
618
|
-
const domain =
|
|
619
|
+
const domain = getRuntimeConfigValue('JABBER_DOMAIN') ?? getDeployConfig().jabber?.domain;
|
|
619
620
|
return domain ? `${accountId}@${domain}` : accountId;
|
|
620
621
|
}
|
|
621
622
|
|
|
622
623
|
function getIssueUrl(issueKey) {
|
|
623
|
-
return `${
|
|
624
|
+
return `${getRuntimeConfigValue('JIRA_BASE_URL')}/browse/${issueKey}`;
|
|
624
625
|
}
|
|
625
626
|
|
|
626
627
|
function isPrdLikeEnv(env) {
|
|
@@ -971,7 +972,7 @@ async function generateReleaseNotes(jira, ciKey, env, systemCode) {
|
|
|
971
972
|
targetBranch = await findLatestReleaseBranchWithTag(jira, repoMeta, repoName);
|
|
972
973
|
if (!targetBranch) continue;
|
|
973
974
|
}
|
|
974
|
-
const baseUrl =
|
|
975
|
+
const baseUrl = getRuntimeConfigValue('BITBUCKET_URL') || 'https://bitbucket.example.com';
|
|
975
976
|
const compareUrl = `${baseUrl}/projects/${repoMeta.project}/repos/${repoName}/compare/diff?sourceBranch=${sourceBranch.replace(/\//g, '%2F')}&targetBranch=${targetBranch.replace(/\//g, '%2F')}&targetRepoId=${repoMeta.repoId}`;
|
|
976
977
|
notes.push(`${label}: ${compareUrl}`);
|
|
977
978
|
} catch (_) {
|
|
@@ -1056,7 +1057,7 @@ async function handleWeblink(jira, issue, notifier, args) {
|
|
|
1056
1057
|
});
|
|
1057
1058
|
|
|
1058
1059
|
if (versionId) {
|
|
1059
|
-
const versionUrl = `${
|
|
1060
|
+
const versionUrl = `${getRuntimeConfigValue('JIRA_BASE_URL')}/projects/LBPRJ/versions/${versionId}`;
|
|
1060
1061
|
await jira.addRemoteLink(issue.key, versionUrl, moduleName);
|
|
1061
1062
|
await notifier.notify(issue.key, `已附上版本 Web Link:${moduleName}(${libKey})`);
|
|
1062
1063
|
} else {
|
package/tools/ci.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
SYSTEM_TO_CI_REPO_MAP,
|
|
23
23
|
SYSTEM_TO_DEPT_MAP,
|
|
24
24
|
} from '../constants/index.js';
|
|
25
|
+
import {getRuntimeConfigValue} from '../constants/config.js';
|
|
25
26
|
import { assertNoOpenPRBeforeCreate } from './branch-prs.js';
|
|
26
27
|
|
|
27
28
|
// ── Flow Definition ──────────────────────────────────────────────
|
|
@@ -481,7 +482,7 @@ export async function handleCreateCITicket(args, { jira, notifier }) {
|
|
|
481
482
|
return ok({
|
|
482
483
|
issueKey: issue.key,
|
|
483
484
|
issueId: issue.id,
|
|
484
|
-
url: `${
|
|
485
|
+
url: `${getRuntimeConfigValue('JIRA_BASE_URL')}/browse/${issue.key}`,
|
|
485
486
|
type: 'CI Release',
|
|
486
487
|
system: systemCode,
|
|
487
488
|
goldenImageVersion,
|
package/tools/grayrelease.js
CHANGED
|
@@ -40,6 +40,7 @@ import { assertNoOpenPRBeforeCreate } from './branch-prs.js';
|
|
|
40
40
|
import { Poller } from '../poller.js';
|
|
41
41
|
import { handleGetReleaseManager, handleWaitForComment } from './release.js';
|
|
42
42
|
import { handleSendJabberMessage } from './jabber.js';
|
|
43
|
+
import {getRuntimeConfigValue} from '../constants/config.js';
|
|
43
44
|
import {
|
|
44
45
|
needSwitchExecutionNode as shouldSwitchExecutionNode,
|
|
45
46
|
waitForSwitchExecutionNode as waitForSharedSwitchExecutionNode,
|
|
@@ -122,10 +123,14 @@ function progress(ctx, event) {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function getJabberJid(accountId) {
|
|
125
|
-
const domain =
|
|
126
|
+
const domain = getRuntimeConfigValue('JABBER_DOMAIN') ?? getDeployConfig().jabber?.domain;
|
|
126
127
|
return domain ? `${accountId}@${domain}` : accountId;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
function getJiraIssueUrl(issueKey) {
|
|
131
|
+
return `${getRuntimeConfigValue('JIRA_BASE_URL')}/browse/${issueKey}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
function getGrayReleaseUatApprovers() {
|
|
130
135
|
const config = getDeployConfig().release.grayReleaseUatApprovers ?? {};
|
|
131
136
|
return {
|
|
@@ -251,6 +256,12 @@ export function getGrayReleaseToolDefinitions() {
|
|
|
251
256
|
type: 'string',
|
|
252
257
|
description: 'Jenkins branch(選填,預設 master)',
|
|
253
258
|
},
|
|
259
|
+
fortifyScan: {
|
|
260
|
+
type: 'boolean',
|
|
261
|
+
default: false,
|
|
262
|
+
description:
|
|
263
|
+
'(選填) 是否掃描 Fortify。預設 false;若為 true,會在 GrayRelease 專屬 extraVars 加入 {"fortifyScan":"True"}',
|
|
264
|
+
},
|
|
254
265
|
dryRun: {
|
|
255
266
|
type: 'boolean',
|
|
256
267
|
description: '(選填) 預覽模式,不實際建立 Jira 單,回傳會送出的 payload',
|
|
@@ -507,6 +518,11 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
507
518
|
// gray release notes
|
|
508
519
|
fields[GRAY_RELEASE_FIELD_IDS.grayReleaseNotes] = NOTES_TEMPLATES.grayRelease;
|
|
509
520
|
|
|
521
|
+
// fortify scan (GrayRelease 專屬 extraVars)
|
|
522
|
+
if (normalizedArgs.fortifyScan === true && GRAY_RELEASE_FIELD_IDS.extraVars) {
|
|
523
|
+
fields[GRAY_RELEASE_FIELD_IDS.extraVars] = JSON.stringify({ fortifyScan: 'True' });
|
|
524
|
+
}
|
|
525
|
+
|
|
510
526
|
await assertNoOpenPRBeforeCreate({
|
|
511
527
|
ticketType: 'grayrelease',
|
|
512
528
|
systemCode: normalizedArgs.systemCode,
|
|
@@ -527,7 +543,7 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
527
543
|
const result = {
|
|
528
544
|
issueKey: issue.key,
|
|
529
545
|
issueId: issue.id,
|
|
530
|
-
url:
|
|
546
|
+
url: getJiraIssueUrl(issue.key),
|
|
531
547
|
type: 'GrayRelease',
|
|
532
548
|
system: normalizedArgs.systemCode,
|
|
533
549
|
module: normalizedArgs.module,
|
|
@@ -1311,7 +1327,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1311
1327
|
await handleSendJabberMessage(
|
|
1312
1328
|
{
|
|
1313
1329
|
to: jabberTo,
|
|
1314
|
-
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核。環境: STG,系統: ${systemCode}\n${
|
|
1330
|
+
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核。環境: STG,系統: ${systemCode}\n${getJiraIssueUrl(issueKey)}`,
|
|
1315
1331
|
},
|
|
1316
1332
|
{},
|
|
1317
1333
|
);
|
|
@@ -1354,7 +1370,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1354
1370
|
await handleSendJabberMessage(
|
|
1355
1371
|
{
|
|
1356
1372
|
to: commentReviewerJabber,
|
|
1357
|
-
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核並留言確認。環境: UAT,系統: ${systemCode}\n${
|
|
1373
|
+
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核並留言確認。環境: UAT,系統: ${systemCode}\n${getJiraIssueUrl(issueKey)}`,
|
|
1358
1374
|
},
|
|
1359
1375
|
{},
|
|
1360
1376
|
);
|
|
@@ -1401,7 +1417,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1401
1417
|
await handleSendJabberMessage(
|
|
1402
1418
|
{
|
|
1403
1419
|
to: finalApproverJabber,
|
|
1404
|
-
message: `[GrayRelease 簽核通知] ${issueKey} 已由 ${commentReviewerAlias} 確認,需要您的最終簽核。環境: UAT,系統: ${systemCode}\n${
|
|
1420
|
+
message: `[GrayRelease 簽核通知] ${issueKey} 已由 ${commentReviewerAlias} 確認,需要您的最終簽核。環境: UAT,系統: ${systemCode}\n${getJiraIssueUrl(issueKey)}`,
|
|
1405
1421
|
},
|
|
1406
1422
|
{},
|
|
1407
1423
|
);
|
package/tools/helpers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {getDeployConfig, META_TEST_NODES, SERVER_MODULE_MAP, SERVERS} from '../constants/index.js';
|
|
2
|
+
import {getRuntimeConfigNumber} from '../constants/config.js';
|
|
2
3
|
|
|
3
4
|
export function ok(data) {
|
|
4
5
|
return {content: [{type: 'text', text: JSON.stringify(data, null, 2)}]};
|
|
@@ -66,11 +67,11 @@ export function getModuleName(systemCode, module, versionName) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export function getPollIntervalMs() {
|
|
69
|
-
return
|
|
70
|
+
return getRuntimeConfigNumber('POLL_INTERVAL_MS', 30000);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export function getPollTimeoutMs() {
|
|
73
|
-
return
|
|
74
|
+
return getRuntimeConfigNumber('POLL_TIMEOUT_MS', 3600000);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export function isPassingResult(value) {
|
|
@@ -83,4 +84,4 @@ export function isFailingResult(value) {
|
|
|
83
84
|
return ['fail', 'failed', 'failure', 'error'].includes(
|
|
84
85
|
String(value ?? '').trim().toLowerCase(),
|
|
85
86
|
);
|
|
86
|
-
}
|
|
87
|
+
}
|
package/tools/jabber.js
CHANGED
|
@@ -7,6 +7,7 @@ import fs from 'fs';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import {fileURLToPath} from 'url';
|
|
9
9
|
import {error, ok} from './helpers.js';
|
|
10
|
+
import {getRuntimeConfigValue} from '../constants/config.js';
|
|
10
11
|
|
|
11
12
|
const packageScriptPath = path.resolve(
|
|
12
13
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -32,8 +33,9 @@ export function resolveJabberNotifyScriptPath({
|
|
|
32
33
|
execPath = process.execPath,
|
|
33
34
|
cwd = process.cwd(),
|
|
34
35
|
} = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const configuredScript = getRuntimeConfigValue('JABBER_NOTIFY_SCRIPT') ?? env.JABBER_NOTIFY_SCRIPT;
|
|
37
|
+
if (configuredScript) {
|
|
38
|
+
return configuredScript;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const resolvedExecPath = realpathOrNull(execPath);
|
|
@@ -91,14 +93,20 @@ export async function handleSendJabberMessage(args, _ctx) {
|
|
|
91
93
|
sent: false,
|
|
92
94
|
dryRun: true,
|
|
93
95
|
message: args.message,
|
|
94
|
-
to: args.to ??
|
|
95
|
-
room: args.room ??
|
|
96
|
-
preview: `[DRY RUN] 將發送 Jabber 給 ${args.to ??
|
|
96
|
+
to: args.to ?? getRuntimeConfigValue('JABBER_TO') ?? '(JABBER_TO not set)',
|
|
97
|
+
room: args.room ?? getRuntimeConfigValue('JABBER_ROOM'),
|
|
98
|
+
preview: `[DRY RUN] 將發送 Jabber 給 ${args.to ?? getRuntimeConfigValue('JABBER_TO') ?? '?'}: ${args.message}`,
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
// 允許透過 tool 參數覆蓋 JABBER_TO / JABBER_ROOM
|
|
101
103
|
const env = {...process.env};
|
|
104
|
+
for (const key of ['JABBER_SERVER', 'JABBER_USER', 'JABBER_DOMAIN', 'JABBER_KEYCHAIN_SERVICE', 'JABBER_KEYCHAIN_ACCOUNT', 'JABBER_TO', 'JABBER_ROOM', 'JABBER_PORT', 'JABBER_RESOURCE', 'JABBER_NICK']) {
|
|
105
|
+
const value = getRuntimeConfigValue(key);
|
|
106
|
+
if (value !== undefined) {
|
|
107
|
+
env[key] = value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
102
110
|
if (args.to) env.JABBER_TO = args.to;
|
|
103
111
|
if (args.room) env.JABBER_ROOM = args.room;
|
|
104
112
|
|
package/tools/library.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SYSTEM_CODES,
|
|
20
20
|
SYSTEM_TO_DEPT_MAP,
|
|
21
21
|
} from '../constants/index.js';
|
|
22
|
+
import {getRuntimeConfigValue} from '../constants/config.js';
|
|
22
23
|
import { error, getPollIntervalMs, getPollTimeoutMs, ok, today } from './helpers.js';
|
|
23
24
|
import { assertNoOpenPRBeforeCreate } from './branch-prs.js';
|
|
24
25
|
import {
|
|
@@ -267,7 +268,7 @@ export async function handleCreateLibraryTicket(args, { jira, notifier }) {
|
|
|
267
268
|
return ok({
|
|
268
269
|
issueKey: issue.key,
|
|
269
270
|
issueId: issue.id,
|
|
270
|
-
url: `${
|
|
271
|
+
url: `${getRuntimeConfigValue('JIRA_BASE_URL')}/browse/${issue.key}`,
|
|
271
272
|
type: 'Library Release',
|
|
272
273
|
system: normalizedArgs.systemCode,
|
|
273
274
|
module: normalizedArgs.module,
|
package/tools/release.js
CHANGED
|
@@ -9,6 +9,7 @@ import https from 'https';
|
|
|
9
9
|
import http from 'http';
|
|
10
10
|
import {error, getModuleName, ok} from './helpers.js';
|
|
11
11
|
import {getDeployConfig, getReleaseProjectKey, SYSTEM_CODES, SYSTEM_MODULES} from '../constants/index.js';
|
|
12
|
+
import {getRuntimeConfigNumber, getRuntimeConfigValue} from '../constants/config.js';
|
|
12
13
|
|
|
13
14
|
// ── Schema definitions ───────────────────────────────────────────
|
|
14
15
|
export function getReleaseToolDefinitions() {
|
|
@@ -177,13 +178,13 @@ export async function handleGetReleaseManager(args = {}) {
|
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
const CONF_BASE_URL =
|
|
181
|
-
const CONF_TOKEN =
|
|
181
|
+
const CONF_BASE_URL = getRuntimeConfigValue('CONF_BASE_URL');
|
|
182
|
+
const CONF_TOKEN = getRuntimeConfigValue('CONF_TOKEN');
|
|
182
183
|
if (!CONF_BASE_URL || !CONF_TOKEN) {
|
|
183
184
|
return error('缺少環境變數 CONF_BASE_URL 或 CONF_TOKEN');
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
const SUB_CALENDAR_ID =
|
|
187
|
+
const SUB_CALENDAR_ID = getRuntimeConfigValue('CONF_RELEASE_MANAGER_SUB_CALENDAR_ID')
|
|
187
188
|
?? releaseConfig.managerSubCalendarId;
|
|
188
189
|
if (!SUB_CALENDAR_ID) {
|
|
189
190
|
return error('缺少環境變數 CONF_RELEASE_MANAGER_SUB_CALENDAR_ID 或 release.managerSubCalendarId config');
|
|
@@ -265,8 +266,8 @@ export async function handleWaitForComment(args, {
|
|
|
265
266
|
});
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
const intervalMs = args.intervalMs ??
|
|
269
|
-
const timeoutMs = args.timeoutMs ??
|
|
269
|
+
const intervalMs = args.intervalMs ?? getRuntimeConfigNumber('POLL_INTERVAL_MS', 30000);
|
|
270
|
+
const timeoutMs = args.timeoutMs ?? getRuntimeConfigNumber('POLL_TIMEOUT_MS', 3600000);
|
|
270
271
|
const startTime = Date.now();
|
|
271
272
|
let attempts = 0;
|
|
272
273
|
|