@jira-deploy/core 1.0.1 → 1.0.3

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/tools/helpers.js CHANGED
@@ -5,7 +5,10 @@ export function ok(data) {
5
5
  }
6
6
 
7
7
  export function error(msg) {
8
- return {content: [{type: 'text', text: `❌ 錯誤: ${msg}`}]};
8
+ return {
9
+ content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
10
+ isError: true,
11
+ };
9
12
  }
10
13
 
11
14
  export function today() {
package/tools/index.js CHANGED
@@ -17,6 +17,9 @@ import {
17
17
  getGrayReleaseToolDefinitions,
18
18
  handleCreateGrayReleaseTicket,
19
19
  handleLinkStgGrayRelease,
20
+ handleAutoGrayRelease,
21
+ handleGetGrayReleaseStatus,
22
+ handleContinueGrayRelease,
20
23
  } from './grayrelease.js';
21
24
  import {
22
25
  getReleaseToolDefinitions,
@@ -26,6 +29,36 @@ import {
26
29
  handleWaitForComment,
27
30
  } from './release.js';
28
31
  import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
32
+ import {
33
+ getWorkflowToolDefinitions,
34
+ handleRunLibToStgReleaseWorkflow,
35
+ handleRunStgFullReleaseWorkflow,
36
+ } from './workflows.js';
37
+
38
+ const READ_ONLY_TOOL_NAMES = new Set([
39
+ 'get_issue_status',
40
+ 'list_transitions',
41
+ 'get_release_status',
42
+ 'get_unreleased_versions',
43
+ 'get_release_manager',
44
+ 'wait_for_comment',
45
+ 'get_grayrelease_status',
46
+ ]);
47
+
48
+ function withToolAnnotations(tools) {
49
+ return tools.map((tool) => {
50
+ if (!READ_ONLY_TOOL_NAMES.has(tool.name)) {
51
+ return tool;
52
+ }
53
+ return {
54
+ ...tool,
55
+ annotations: {
56
+ ...tool.annotations,
57
+ readOnlyHint: true,
58
+ },
59
+ };
60
+ });
61
+ }
29
62
 
30
63
  /**
31
64
  * 回傳所有 tool 定義(schema)給 MCP Server 註冊
@@ -40,13 +73,14 @@ import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
40
73
  * - defaults.js: 預設值及範本
41
74
  */
42
75
  export function getToolDefinitions() {
43
- return [
76
+ return withToolAnnotations([
44
77
  ...getLibraryToolDefinitions(),
45
78
  ...getCIToolDefinitions(),
46
79
  ...getCDToolDefinitions(),
47
80
  ...getGrayReleaseToolDefinitions(),
48
81
  ...getReleaseToolDefinitions(),
49
82
  ...getJabberToolDefinitions(),
83
+ ...getWorkflowToolDefinitions(),
50
84
  {
51
85
  name: 'transition_issue',
52
86
  description: '切換 Jira issue 狀態,用名稱指定(不需要知道 transition ID)',
@@ -155,6 +189,21 @@ export function getToolDefinitions() {
155
189
  },
156
190
  },
157
191
  },
192
+ {
193
+ name: 'wait_to_dev',
194
+ description:
195
+ 'CI 單 build 完成後,自動走完掃描流程切到 Wait To DEV 狀態。流程:Compliance Scan → Upload Scan Report → Accept → Wait To DEV(不執行 Dev Done)',
196
+ inputSchema: {
197
+ type: 'object',
198
+ required: ['issueKey'],
199
+ properties: {
200
+ issueKey: {
201
+ type: 'string',
202
+ description: 'CI issue key,例如 CID-1668',
203
+ },
204
+ },
205
+ },
206
+ },
158
207
  {
159
208
  name: 'wait_to_stg',
160
209
  description:
@@ -271,13 +320,14 @@ export function getToolDefinitions() {
271
320
  },
272
321
  },
273
322
  },
274
- ];
323
+ ]);
275
324
  }
276
325
 
277
326
  /**
278
327
  * 執行 tool,回傳 { content: [{ type: 'text', text }] }
279
328
  */
280
- export async function executeTool(name, args, {jira, notifier}) {
329
+ export async function executeTool(name, args, deps) {
330
+ const {jira, notifier} = deps;
281
331
  const poller = new Poller(jira);
282
332
 
283
333
  switch (name) {
@@ -346,6 +396,15 @@ export async function executeTool(name, args, {jira, notifier}) {
346
396
  case 'link_stg_grayrelease':
347
397
  return handleLinkStgGrayRelease(args, {jira, notifier});
348
398
 
399
+ case 'auto_grayrelease':
400
+ return handleAutoGrayRelease(args, {jira, notifier});
401
+
402
+ case 'get_grayrelease_status':
403
+ return handleGetGrayReleaseStatus(args, {jira});
404
+
405
+ case 'continue_grayrelease':
406
+ return handleContinueGrayRelease(args, {jira, notifier});
407
+
349
408
  case 'get_unreleased_versions':
350
409
  return handleGetUnreleasedVersions(args, {jira});
351
410
 
@@ -361,6 +420,22 @@ export async function executeTool(name, args, {jira, notifier}) {
361
420
  case 'send_jabber_message':
362
421
  return handleSendJabberMessage(args, {});
363
422
 
423
+ case 'run_stg_full_release':
424
+ return handleRunStgFullReleaseWorkflow(args, {
425
+ jira,
426
+ notifier,
427
+ executeToolImpl: deps.executeToolImpl ?? executeTool,
428
+ workflowWaitOptions: deps.workflowWaitOptions,
429
+ });
430
+
431
+ case 'run_lib_to_stg_release':
432
+ return handleRunLibToStgReleaseWorkflow(args, {
433
+ jira,
434
+ notifier,
435
+ executeToolImpl: deps.executeToolImpl ?? executeTool,
436
+ workflowWaitOptions: deps.workflowWaitOptions,
437
+ });
438
+
364
439
  case 'link_issues': {
365
440
  try {
366
441
  const linkType = args.linkType ?? 'Relates';
@@ -491,6 +566,48 @@ export async function executeTool(name, args, {jira, notifier}) {
491
566
  }
492
567
  }
493
568
 
569
+ case 'wait_to_dev': {
570
+ const {issueKey} = args;
571
+ const log = [];
572
+
573
+ const STEPS = [
574
+ {transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
575
+ {transition: 'Accept', targetStatus: 'Wait To DEV'},
576
+ ];
577
+ const finalTargetStatus = STEPS.at(-1).targetStatus;
578
+
579
+ try {
580
+ for (const step of STEPS) {
581
+ const transitions = await jira.getTransitions(issueKey);
582
+ const t = transitions.find((t) => t.name.toLowerCase() === step.transition.toLowerCase());
583
+ if (!t) {
584
+ const issue = await jira.getIssue(issueKey);
585
+ const current = issue.fields.status.name;
586
+ if (current.toLowerCase() === step.targetStatus.toLowerCase()) {
587
+ log.push(` 已是 ${current},跳過「${step.transition}」`);
588
+ continue;
589
+ }
590
+ if (current.toLowerCase() === finalTargetStatus.toLowerCase()) {
591
+ log.push(` 已是 ${current},流程已完成`);
592
+ break;
593
+ }
594
+ return error(`找不到 transition「${step.transition}」,目前狀態:${current}`);
595
+ }
596
+ log.push(`執行「${t.name}」→ ${step.targetStatus}`);
597
+ await jira.transitionById(issueKey, t.id);
598
+ }
599
+
600
+ const issue = await jira.getIssue(issueKey);
601
+ const finalStatus = issue.fields.status.name;
602
+ log.push(`✅ 完成,目前狀態:${finalStatus}`);
603
+ await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 DEV 部署`);
604
+
605
+ return ok({issueKey, status: finalStatus, steps: log});
606
+ } catch (err) {
607
+ return error(`wait_to_dev 失敗: ${err.message}`);
608
+ }
609
+ }
610
+
494
611
  case 'wait_to_stg': {
495
612
  const {issueKey} = args;
496
613
  const log = [];
@@ -669,7 +786,11 @@ export async function executeTool(name, args, {jira, notifier}) {
669
786
  if (!t) {
670
787
  const issue = await jira.getIssue(deploymentKey);
671
788
  const available = transitions.map((t) => t.name).join(', ');
672
- // transition 不存在但狀態已超前,跳過
789
+ if (['To AutoDeploy', 'Trigger AutoDeploy'].includes(step.name)) {
790
+ return error(
791
+ `找不到必要部署 transition「${step.name}」,目前狀態:${issue.fields.status.name},可用:${available || '無'}`,
792
+ );
793
+ }
673
794
  log.push(
674
795
  ` ⚠️ 找不到「${step.name}」(目前狀態:${issue.fields.status.name},可用:${available || '無'}),跳過`,
675
796
  );
@@ -739,19 +860,28 @@ export async function executeTool(name, args, {jira, notifier}) {
739
860
  const log = [];
740
861
 
741
862
  // CD 部署前置 transitions(依序嘗試,直到找到部署 transition 或完成申請流程)
742
- // STG 完整流程:
863
+ // DEV 流程會跳過通知主管簽核,建立 deployment 後直接嘗試自助 Approved / To Wait Deploy。
864
+ // STG/UAT/PRD 完整流程:
743
865
  // TO DO → (Accept) → Wait For Send Notice Email
744
866
  // → (Prepare to create deployment ticket) → Prepare For Deploy ← 建立 Deployment sub-task
745
867
  // → (Apply for approval) → Wait Approval ← 等 Reviewer 核准
746
868
  // → Wait Deploy ← 核准後自動切換,再呼叫 trigger_deployment
747
869
  // ⚠️ 不加 'Approved':Approved 需要其他主管才能執行
748
- const CD_PRE_TRANSITIONS = [
870
+ const CD_PRE_TRANSITIONS = envCode === 'dev' ? [
871
+ 'Accept',
872
+ ] : [
749
873
  'Accept',
750
874
  'Prepare to create deployment ticket',
751
875
  'Apply for approval',
752
876
  'To Wait Deploy',
753
877
  ];
754
878
 
879
+ const DEV_SELF_SERVICE_TRANSITIONS = [
880
+ 'Apply for approval',
881
+ 'Approved',
882
+ 'To Wait Deploy',
883
+ ];
884
+
755
885
  // 部署 transition 名稱(支援多種命名)
756
886
  const DEPLOY_TRANSITION_NAMES = [
757
887
  'Prepare to create deployment ticket', // 實際 Jira transition 名稱
@@ -788,6 +918,45 @@ export async function executeTool(name, args, {jira, notifier}) {
788
918
 
789
919
  let deployTrans = await findDeployTrans();
790
920
 
921
+ const getCurrentStatus = async () => {
922
+ const issue = await jira.getIssue(issueKey);
923
+ return issue.fields.status.name;
924
+ };
925
+
926
+ const runDevSelfServiceTransitions = async () => {
927
+ if (envCode !== 'dev') return;
928
+ for (const transitionName of DEV_SELF_SERVICE_TRANSITIONS) {
929
+ const currentStatus = await getCurrentStatus();
930
+ if (currentStatus.toLowerCase() === 'wait deploy') {
931
+ log.push(' DEV 自助流程已到 Wait Deploy,停止續跑');
932
+ break;
933
+ }
934
+
935
+ const transitions = await jira.getTransitions(issueKey);
936
+ const next = transitions.find(
937
+ (t) => t.name.toLowerCase() === transitionName.toLowerCase(),
938
+ );
939
+ if (!next) continue;
940
+
941
+ log.push(` DEV 自助流程觸發「${next.name}」...`);
942
+ await jira.transitionById(issueKey, next.id);
943
+ await new Promise((r) => setTimeout(r, 2000));
944
+ }
945
+ };
946
+
947
+ if (envCode === 'dev') {
948
+ const currentStatus = await getCurrentStatus();
949
+ if (currentStatus.toLowerCase() === 'wait deploy') {
950
+ log.push('✅ CD 單已在 Wait Deploy,無需重複 prepare');
951
+ return ok({
952
+ issueKey,
953
+ environment: envCode,
954
+ status: currentStatus,
955
+ steps: log,
956
+ });
957
+ }
958
+ }
959
+
791
960
  if (!deployTrans) {
792
961
  log.push('未找到部署 transition,逐步觸發前置狀態...');
793
962
  for (const preName of CD_PRE_TRANSITIONS) {
@@ -807,6 +976,25 @@ export async function executeTool(name, args, {jira, notifier}) {
807
976
  }
808
977
  }
809
978
 
979
+ if (!deployTrans && envCode === 'dev') {
980
+ await runDevSelfServiceTransitions();
981
+ const currentStatus = await getCurrentStatus();
982
+ if (currentStatus.toLowerCase() === 'wait deploy') {
983
+ log.push(`✅ DEV 自助流程完成,目前狀態:${currentStatus}`);
984
+ await notifier.notify(
985
+ issueKey,
986
+ `CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${currentStatus})`,
987
+ );
988
+ return ok({
989
+ issueKey,
990
+ environment: envCode,
991
+ status: currentStatus,
992
+ steps: log,
993
+ });
994
+ }
995
+ deployTrans = await findDeployTrans();
996
+ }
997
+
810
998
  if (!deployTrans) {
811
999
  const issue = await jira.getIssue(issueKey);
812
1000
  const transitions = await jira.getTransitions(issueKey);
@@ -820,21 +1008,37 @@ export async function executeTool(name, args, {jira, notifier}) {
820
1008
 
821
1009
  // Step 4: 觸发部署 transition
822
1010
  log.push(`執行「${deployTrans.name}」transition(id: ${deployTrans.id})...`);
823
- await jira.transitionById(issueKey, deployTrans.id);
1011
+ try {
1012
+ await jira.transitionById(issueKey, deployTrans.id);
1013
+ } catch (err) {
1014
+ if (envCode === 'dev' && err.message.includes('Already create deployment ticket')) {
1015
+ log.push(' Deployment ticket 已存在,繼續 DEV approval 續跑流程');
1016
+ } else {
1017
+ throw err;
1018
+ }
1019
+ }
824
1020
 
825
1021
  const issue = await jira.getIssue(issueKey);
826
1022
  const newStatus = issue.fields.status.name;
827
1023
  log.push(`✅ 部署已觸发,目前狀態:${newStatus}`);
828
1024
 
1025
+ await runDevSelfServiceTransitions();
1026
+
1027
+ const finalIssue = await jira.getIssue(issueKey);
1028
+ const finalStatus = finalIssue.fields.status.name;
1029
+ if (finalStatus !== newStatus) {
1030
+ log.push(`✅ DEV 自助流程完成,目前狀態:${finalStatus}`);
1031
+ }
1032
+
829
1033
  await notifier.notify(
830
1034
  issueKey,
831
- `CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${newStatus})`,
1035
+ `CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${finalStatus})`,
832
1036
  );
833
1037
 
834
1038
  return ok({
835
1039
  issueKey,
836
1040
  environment: envCode,
837
- status: newStatus,
1041
+ status: finalStatus,
838
1042
  steps: log,
839
1043
  });
840
1044
  } catch (err) {
@@ -1115,5 +1319,6 @@ function ok(data) {
1115
1319
  function error(msg) {
1116
1320
  return {
1117
1321
  content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
1322
+ isError: true,
1118
1323
  };
1119
1324
  }
package/tools/library.js CHANGED
@@ -31,7 +31,7 @@ export function getLibraryToolDefinitions() {
31
31
  '建立 Library Release 上版單。專用工具,提前驗證必填欄位。當使用者說「幫我開 Lib」、「開 Library」時優先使用這個 tool',
32
32
  inputSchema: {
33
33
  type: 'object',
34
- required: ['systemCode', 'module', 'gitBranch'],
34
+ required: ['systemCode', 'gitBranch'],
35
35
  properties: {
36
36
  systemCode: {
37
37
  type: 'string',
@@ -210,7 +210,7 @@ export async function handleCreateLibraryTicket(args, {jira, notifier}) {
210
210
  const issue = await jira.createIssue(fields);
211
211
  await notifier.notify(
212
212
  issue.key,
213
- `Library Release 單已建立。系統: ${args.systemCode}, 模組: ${args.module}, 環境: ${envCode}`,
213
+ `Library Release 單已建立。系統: ${normalizedArgs.systemCode}, 模組: ${normalizedArgs.module}, 環境: ${envCode}`,
214
214
  );
215
215
  return ok({
216
216
  issueKey: issue.key,
@@ -218,6 +218,7 @@ export async function handleCreateLibraryTicket(args, {jira, notifier}) {
218
218
  url: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
219
219
  type: 'Library Release',
220
220
  system: normalizedArgs.systemCode,
221
+ module: normalizedArgs.module,
221
222
  });
222
223
  } catch (err) {
223
224
  return error(`無法建立 Library 單: ${err.message}`);
@@ -0,0 +1,239 @@
1
+ import {getClusterList, ok, error} from './helpers.js';
2
+
3
+ export function getWorkflowToolDefinitions() {
4
+ return [
5
+ {
6
+ name: 'run_stg_full_release',
7
+ description:
8
+ '執行標準 STG 全流程:建立 CI 單、Build、Wait To STG、建立 CD 單、Prepare CD Deployment,最後 trigger deployment 並 Apply for close。',
9
+ inputSchema: {
10
+ type: 'object',
11
+ required: ['systemCode'],
12
+ properties: {
13
+ systemCode: {
14
+ type: 'string',
15
+ description: '系統代碼,例如 IBK、CWA、EIB、EVT、NPM、BOF',
16
+ },
17
+ environment: {
18
+ type: 'string',
19
+ enum: ['stg'],
20
+ default: 'stg',
21
+ description: '固定為 stg;保留此參數讓自然語言 planner 可以映射使用者提到的 STG。',
22
+ },
23
+ },
24
+ },
25
+ },
26
+ {
27
+ name: 'run_lib_to_stg_release',
28
+ description:
29
+ '執行 Library 到 STG 的完整流程:建立 Library 單、Build library、等待 library build 狀態、建立關聯 CI、Build CI、Wait To STG、建立 CD、Prepare CD Deployment,最後 trigger deployment 並 Apply for close。',
30
+ inputSchema: {
31
+ type: 'object',
32
+ required: ['systemCode', 'gitBranch'],
33
+ properties: {
34
+ systemCode: {
35
+ type: 'string',
36
+ description: '系統代碼,例如 IBK、CWA、EIB、EVT、NPM、BOF',
37
+ },
38
+ module: {
39
+ type: 'string',
40
+ description: 'Library 模組;未提供時預設使用 systemCode 小寫,例如 IBK -> ibk。',
41
+ },
42
+ gitBranch: {
43
+ type: 'string',
44
+ description: 'Library release/hotfix/feature branch,例如 release/v1.5.2.0。',
45
+ },
46
+ environment: {
47
+ type: 'string',
48
+ enum: ['stg'],
49
+ default: 'stg',
50
+ description: '固定為 stg;保留此參數讓自然語言 planner 可以映射使用者提到的 STG。',
51
+ },
52
+ },
53
+ },
54
+ },
55
+ ];
56
+ }
57
+
58
+ function extractText(result) {
59
+ if (!result?.content?.length) {
60
+ return '';
61
+ }
62
+ return result.content
63
+ .filter((item) => item.type === 'text')
64
+ .map((item) => item.text)
65
+ .join('\n')
66
+ .trim();
67
+ }
68
+
69
+ function formatJson(value) {
70
+ return JSON.stringify(value, null, 2);
71
+ }
72
+
73
+ function parseToolJson(result) {
74
+ const text = extractText(result);
75
+ if (result?.isError || text.startsWith('❌')) {
76
+ throw new Error(text);
77
+ }
78
+ return JSON.parse(text);
79
+ }
80
+
81
+ async function runToolOrThrow(name, args, deps, workflowLog) {
82
+ workflowLog.push(`- ${name}: ${formatJson(args)}`);
83
+ const result = await deps.executeToolImpl(name, args, deps);
84
+ return parseToolJson(result);
85
+ }
86
+
87
+ async function waitForIssueStatus(issueKey, targetStatuses, deps, options = {}) {
88
+ const timeoutMs = options.timeoutMs ?? 10 * 60 * 1000;
89
+ const intervalMs = options.intervalMs ?? 5000;
90
+ const startedAt = Date.now();
91
+ const wanted = targetStatuses.map((status) => status.toLowerCase());
92
+
93
+ while (Date.now() - startedAt < timeoutMs) {
94
+ const issue = await deps.jira.getIssue(issueKey);
95
+ const current = issue.fields?.status?.name ?? '';
96
+ if (wanted.includes(current.toLowerCase())) {
97
+ return current;
98
+ }
99
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
100
+ }
101
+
102
+ throw new Error(`等待 ${issueKey} 狀態 ${targetStatuses.join(' / ')} 超時`);
103
+ }
104
+
105
+ export async function handleRunStgFullReleaseWorkflow(args, deps) {
106
+ try {
107
+ const workflowLog = [];
108
+ const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
109
+ const ciArgs = {systemCode, environment: 'stg'};
110
+ const ci = await runToolOrThrow('create_ci_ticket', ciArgs, deps, workflowLog);
111
+
112
+ const build = await runToolOrThrow('build_ticket', {issueKey: ci.issueKey}, deps, workflowLog);
113
+ const toStg = await runToolOrThrow('wait_to_stg', {issueKey: ci.issueKey}, deps, workflowLog);
114
+
115
+ const clusters = getClusterList(systemCode, 'stg');
116
+ if (!clusters.length) {
117
+ throw new Error(`找不到 ${systemCode} STG 的 cluster 設定`);
118
+ }
119
+
120
+ const cdArgs = {
121
+ systemCode,
122
+ environment: 'stg',
123
+ linkedCiKey: ci.issueKey,
124
+ clusterDeploy: clusters.join(','),
125
+ moduleChild: systemCode.toLowerCase(),
126
+ };
127
+ const cd = await runToolOrThrow('create_cd_ticket', cdArgs, deps, workflowLog);
128
+ const prepared = await runToolOrThrow(
129
+ 'prepare_cd_deployment',
130
+ {issueKey: cd.issueKey, environment: 'stg'},
131
+ deps,
132
+ workflowLog,
133
+ );
134
+ const deployed = await runToolOrThrow(
135
+ 'trigger_deployment',
136
+ {cdIssueKey: cd.issueKey, environment: 'stg', applyForClose: true},
137
+ deps,
138
+ workflowLog,
139
+ );
140
+
141
+ return ok({
142
+ type: 'STG Full Release',
143
+ systemCode,
144
+ ciIssueKey: ci.issueKey,
145
+ ciStatus: toStg.status ?? build.status,
146
+ cdIssueKey: cd.issueKey,
147
+ cdStatus: prepared.status,
148
+ deploymentStatus: deployed.status,
149
+ assumptions: [`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`],
150
+ workflowLog,
151
+ });
152
+ } catch (err) {
153
+ return error(`run_stg_full_release 失敗: ${err.message}`);
154
+ }
155
+ }
156
+
157
+ export async function handleRunLibToStgReleaseWorkflow(args, deps) {
158
+ try {
159
+ const workflowLog = [];
160
+ const assumptions = [];
161
+ const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
162
+ const module = args.module || systemCode.toLowerCase();
163
+
164
+ if (!args.module) {
165
+ assumptions.push(`未指定 module,已依 systemCode 帶入 ${module}`);
166
+ }
167
+
168
+ const libArgs = {
169
+ systemCode,
170
+ module,
171
+ gitBranch: args.gitBranch,
172
+ environment: 'stg',
173
+ jenkinsBranch: 'master',
174
+ };
175
+ const lib = await runToolOrThrow('create_library_ticket', libArgs, deps, workflowLog);
176
+ await runToolOrThrow('build_ticket', {issueKey: lib.issueKey}, deps, workflowLog);
177
+ const libFinalStatus = await waitForIssueStatus(
178
+ lib.issueKey,
179
+ ['Released'],
180
+ deps,
181
+ {timeoutMs: 10 * 60 * 1000, ...deps.workflowWaitOptions},
182
+ );
183
+ workflowLog.push(`- wait_library_status: ${lib.issueKey} -> ${libFinalStatus}`);
184
+
185
+ const ciArgs = {
186
+ systemCode,
187
+ environment: 'stg',
188
+ relatesTo: lib.issueKey,
189
+ };
190
+ const ci = await runToolOrThrow('create_ci_ticket', ciArgs, deps, workflowLog);
191
+ const build = await runToolOrThrow('build_ticket', {issueKey: ci.issueKey}, deps, workflowLog);
192
+ const toStg = await runToolOrThrow('wait_to_stg', {issueKey: ci.issueKey}, deps, workflowLog);
193
+
194
+ const clusters = getClusterList(systemCode, 'stg');
195
+ if (!clusters.length) {
196
+ throw new Error(`找不到 ${systemCode} STG 的 cluster 設定`);
197
+ }
198
+ assumptions.push(`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`);
199
+
200
+ const cdArgs = {
201
+ systemCode,
202
+ environment: 'stg',
203
+ linkedCiKey: ci.issueKey,
204
+ clusterDeploy: clusters.join(','),
205
+ moduleChild: systemCode.toLowerCase(),
206
+ };
207
+ const cd = await runToolOrThrow('create_cd_ticket', cdArgs, deps, workflowLog);
208
+ const prepared = await runToolOrThrow(
209
+ 'prepare_cd_deployment',
210
+ {issueKey: cd.issueKey, environment: 'stg'},
211
+ deps,
212
+ workflowLog,
213
+ );
214
+ const deployed = await runToolOrThrow(
215
+ 'trigger_deployment',
216
+ {cdIssueKey: cd.issueKey, environment: 'stg', applyForClose: true},
217
+ deps,
218
+ workflowLog,
219
+ );
220
+
221
+ return ok({
222
+ type: 'Library To STG Full Release',
223
+ systemCode,
224
+ module,
225
+ gitBranch: args.gitBranch,
226
+ libraryIssueKey: lib.issueKey,
227
+ libraryStatus: libFinalStatus,
228
+ ciIssueKey: ci.issueKey,
229
+ ciStatus: toStg.status ?? build.status,
230
+ cdIssueKey: cd.issueKey,
231
+ cdStatus: prepared.status,
232
+ deploymentStatus: deployed.status,
233
+ assumptions,
234
+ workflowLog,
235
+ });
236
+ } catch (err) {
237
+ return error(`run_lib_to_stg_release 失敗: ${err.message}`);
238
+ }
239
+ }