@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.
@@ -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
- if (process.env.JIRA_DEPLOY_CONFIG_PATH) {
161
- const configPath = expandHomePath(process.env.JIRA_DEPLOY_CONFIG_PATH);
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 (${process.env.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 process.env.JIRA_RELEASE_PROJECT_KEY ?? getDeployConfig().jira.releaseProjectKey;
207
+ return getRuntimeConfigValue('JIRA_RELEASE_PROJECT_KEY') ?? getDeployConfig().jira.releaseProjectKey;
179
208
  }
180
209
 
181
210
  export function resetDeployConfigForTests() {
@@ -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 = process.env.JIRA_PROJECT_KEY ?? config.jira.projectKey;
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 {getClusterList, ok, error, today} from './tools/helpers.js';
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
- const DRY_RUN = process.env.DRY_RUN === 'true';
7
+ function isDryRun() {
8
+ return getRuntimeConfigValue('DRY_RUN') === 'true';
9
+ }
7
10
 
8
11
  export class JiraClient {
9
12
  constructor() {
10
- const {JIRA_BASE_URL, JIRA_API_TOKEN} = process.env;
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 = DRY_RUN;
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
- // 取得可執行的 transitions
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 = process.env.BITBUCKET_URL ?? process.env.BITBUCKET_BASE_URL;
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 ${process.env.BITBUCKET_API_TOKEN}`,
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 = process.env.BITBUCKET_URL ?? process.env.BITBUCKET_BASE_URL;
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 ${process.env.BITBUCKET_API_TOKEN}`,
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 = process.env.BITBUCKET_URL ?? process.env.BITBUCKET_BASE_URL;
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 ${process.env.BITBUCKET_API_TOKEN}`,
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 = !!process.env.SLACK_BOT_TOKEN && !!process.env.SLACK_CHANNEL_ID;
13
- this.dryRun = process.env.DRY_RUN === 'true';
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.15",
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
  }
@@ -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: process.env.JIRA_PROJECT_KEY ?? 'OPS',
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 ?? parseInt(process.env.POLL_INTERVAL_MS ?? '30000');
23
- const timeoutMs = options.timeoutMs ?? parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000');
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();