@jira-deploy/core 1.0.4 → 1.0.6

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.
@@ -5,12 +5,12 @@
5
5
  */
6
6
  export const USER_MAP = {
7
7
  // 格式:'名稱/暱稱(不分大小寫)': 'accountId'
8
- 'Solar Chen': 'BK00619',
9
- 'Rex Li': 'BK00619',
10
- 'James Yu': 'BK00619',
11
- 'Alvin Wang': 'BK00619',
12
- 'Chester Kuo': 'BK00619',
13
- 'Riemann Tseng': 'BK00619',
8
+ 'Solar Chen': 'BK00325',
9
+ 'Rex Li': 'BK00325',
10
+ 'James Yu': 'BK00325',
11
+ 'Alvin Wang': 'BK00325',
12
+ 'Chester Kuo': 'BK00325',
13
+ 'Riemann Tseng': 'BK00325',
14
14
  // 'Solar Chen': 'BK00129',
15
15
  // 'Rex Li': 'BK00136',
16
16
  // 'James Yu': 'BK00178',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jira-deploy/core",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "repository": {
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "files": [
23
23
  "constants/**/*.js",
24
+ "scripts/**/*.py",
24
25
  "tools/**/*.js",
25
26
  "*.js"
26
27
  ],
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ jabber_notify.py — Send a Jabber message to a MUC room or a specific user.
4
+
5
+ Usage:
6
+ python3 src/scripts/jabber_notify.py "<message>"
7
+
8
+ Required environment variables:
9
+ JABBER_SERVER
10
+ JABBER_USER
11
+ JABBER_DOMAIN
12
+ JABBER_KEYCHAIN_SERVICE
13
+ JABBER_KEYCHAIN_ACCOUNT
14
+
15
+ One of the following must be set:
16
+ JABBER_ROOM — MUC room JID (e.g. webqa@conference.linebank.com.tw)
17
+ JABBER_TO — Direct message recipient JID (e.g. BK00178@linebank.com.tw)
18
+
19
+ Optional environment variables:
20
+ JABBER_PORT (defaults to 5222)
21
+ JABBER_RESOURCE (defaults to copilot)
22
+ JABBER_NICK (defaults to Copilot) — only used for MUC
23
+
24
+ To add or update Keychain entry:
25
+ security add-generic-password -a "$JABBER_KEYCHAIN_ACCOUNT" -s "$JABBER_KEYCHAIN_SERVICE" -w
26
+ """
27
+
28
+ import base64
29
+ import os
30
+ import socket
31
+ import ssl
32
+ import subprocess
33
+ import sys
34
+ import time
35
+
36
+
37
+ def required_env(key: str) -> str:
38
+ value = os.getenv(key, "").strip()
39
+ if not value:
40
+ raise RuntimeError(f"Missing required environment variable: {key}")
41
+ return value
42
+
43
+
44
+ def load_config() -> dict:
45
+ room = os.getenv("JABBER_ROOM", "").strip()
46
+ to = os.getenv("JABBER_TO", "").strip()
47
+ if not room and not to:
48
+ raise RuntimeError("Either JABBER_ROOM or JABBER_TO must be set")
49
+ return {
50
+ "server": required_env("JABBER_SERVER"),
51
+ "port": int(os.getenv("JABBER_PORT", "5222")),
52
+ "user": required_env("JABBER_USER"),
53
+ "domain": required_env("JABBER_DOMAIN"),
54
+ "resource": os.getenv("JABBER_RESOURCE", "copilot").strip() or "copilot",
55
+ "room": room,
56
+ "to": to,
57
+ "nick": os.getenv("JABBER_NICK", "Copilot").strip() or "Copilot",
58
+ "keychain_service": required_env("JABBER_KEYCHAIN_SERVICE"),
59
+ "keychain_account": required_env("JABBER_KEYCHAIN_ACCOUNT"),
60
+ }
61
+
62
+
63
+ def recv_all(sock, timeout=2):
64
+ sock.settimeout(timeout)
65
+ data = b""
66
+ try:
67
+ while True:
68
+ chunk = sock.recv(8192)
69
+ if not chunk:
70
+ break
71
+ data += chunk
72
+ except socket.timeout:
73
+ pass
74
+ return data.decode(errors="replace")
75
+
76
+
77
+ def get_jabber_password(keychain_service: str, keychain_account: str) -> str:
78
+ # Keychain credential must be created before using this script.
79
+ # Example setup:
80
+ # security add-generic-password -a "$JABBER_KEYCHAIN_ACCOUNT" -s "$JABBER_KEYCHAIN_SERVICE" -w
81
+ result = subprocess.run(
82
+ [
83
+ "security",
84
+ "find-generic-password",
85
+ "-s",
86
+ keychain_service,
87
+ "-a",
88
+ keychain_account,
89
+ "-w",
90
+ ],
91
+ capture_output=True,
92
+ text=True,
93
+ )
94
+ if result.returncode != 0:
95
+ raise RuntimeError(
96
+ "Cannot read Jabber password from Keychain. "
97
+ "Check JABBER_KEYCHAIN_SERVICE and JABBER_KEYCHAIN_ACCOUNT."
98
+ )
99
+ return result.stdout.strip()
100
+
101
+
102
+ def _extract_error_reason(xml_text: str) -> str:
103
+ reasons = [
104
+ "not-authorized",
105
+ "forbidden",
106
+ "conflict",
107
+ "service-unavailable",
108
+ "item-not-found",
109
+ "not-allowed",
110
+ "registration-required",
111
+ "remote-server-not-found",
112
+ "internal-server-error",
113
+ ]
114
+ for reason in reasons:
115
+ if f"<{reason}" in xml_text:
116
+ return reason
117
+ if "<error" in xml_text:
118
+ return "unknown"
119
+ return ""
120
+
121
+
122
+ def _wait_for_join_confirmation(tls, room: str, nick: str, timeout_seconds: int = 10) -> None:
123
+ deadline = time.time() + timeout_seconds
124
+ self_presence_markers = [
125
+ '<status code="110"',
126
+ "<status code='110'",
127
+ ]
128
+ explicit_nick_markers = [
129
+ f'from="{room}/{nick}"',
130
+ f"from='{room}/{nick}'",
131
+ ]
132
+ room_presence_markers = [
133
+ f'from="{room}/',
134
+ f"from='{room}/",
135
+ ]
136
+ saw_room_presence = False
137
+
138
+ while time.time() < deadline:
139
+ chunk = recv_all(tls, 1)
140
+ if not chunk:
141
+ continue
142
+
143
+ error_reason = _extract_error_reason(chunk)
144
+ if error_reason:
145
+ raise RuntimeError(f"Jabber room join failed: {error_reason}")
146
+
147
+ if "<presence" in chunk and any(marker in chunk for marker in room_presence_markers):
148
+ saw_room_presence = True
149
+ if "type=\"error\"" in chunk or "type='error'" in chunk:
150
+ raise RuntimeError("Jabber room join failed: error")
151
+ if "type=\"unavailable\"" in chunk or "type='unavailable'" in chunk:
152
+ continue
153
+
154
+ if any(marker in chunk for marker in self_presence_markers):
155
+ return
156
+
157
+ if any(marker in chunk for marker in explicit_nick_markers):
158
+ return
159
+
160
+ if saw_room_presence:
161
+ return
162
+ raise RuntimeError("Jabber room join not confirmed")
163
+
164
+
165
+ def _wait_for_message_confirmation(
166
+ tls,
167
+ room: str,
168
+ msg_id: str,
169
+ timeout_seconds: int = 5,
170
+ ) -> None:
171
+ deadline = time.time() + timeout_seconds
172
+ id_markers = [f'id="{msg_id}"', f"id='{msg_id}'"]
173
+
174
+ while time.time() < deadline:
175
+ chunk = recv_all(tls, 1)
176
+ if not chunk:
177
+ continue
178
+
179
+ if "<message" in chunk and any(marker in chunk for marker in id_markers):
180
+ if "type=\"error\"" in chunk or "type='error'" in chunk:
181
+ error_reason = _extract_error_reason(chunk) or "error"
182
+ raise RuntimeError(f"Jabber message rejected: {error_reason}")
183
+
184
+ if "<message" in chunk and (
185
+ "type=\"error\"" in chunk or "type='error'" in chunk
186
+ ):
187
+ error_reason = _extract_error_reason(chunk) or "error"
188
+ raise RuntimeError(f"Jabber message rejected: {error_reason}")
189
+
190
+ return
191
+
192
+
193
+ def send_to_room(message: str) -> None:
194
+ config = load_config()
195
+ password = get_jabber_password(
196
+ config["keychain_service"],
197
+ config["keychain_account"],
198
+ )
199
+
200
+ sasl_plain = base64.b64encode(f"\x00{config['user']}\x00{password}".encode()).decode()
201
+
202
+ ctx = ssl.create_default_context()
203
+ ctx.check_hostname = False
204
+ ctx.verify_mode = ssl.CERT_NONE
205
+
206
+ raw = None
207
+ tls = None
208
+ try:
209
+ # ── TCP connect ──────────────────────────────────────────────────────
210
+ raw = socket.create_connection((config["server"], config["port"]), timeout=10)
211
+ raw.sendall(
212
+ f"<?xml version='1.0'?><stream:stream xmlns='jabber:client' version='1.0' "
213
+ f"xmlns:stream='http://etherx.jabber.org/streams' to='{config['domain']}'>".encode()
214
+ )
215
+ time.sleep(0.3)
216
+ recv_all(raw, 1)
217
+
218
+ # ── STARTTLS ─────────────────────────────────────────────────────────
219
+ raw.sendall(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
220
+ time.sleep(0.3)
221
+ recv_all(raw, 1)
222
+ tls = ctx.wrap_socket(raw, server_hostname=config["server"])
223
+
224
+ # ── Re-open stream over TLS ──────────────────────────────────────────
225
+ tls.sendall(
226
+ f"<?xml version='1.0'?><stream:stream xmlns='jabber:client' version='1.0' "
227
+ f"xmlns:stream='http://etherx.jabber.org/streams' to='{config['domain']}'>".encode()
228
+ )
229
+ time.sleep(0.3)
230
+ recv_all(tls, 1)
231
+
232
+ # ── SASL PLAIN auth ──────────────────────────────────────────────────
233
+ tls.sendall(
234
+ f"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>{sasl_plain}</auth>".encode()
235
+ )
236
+ time.sleep(1)
237
+ auth_resp = recv_all(tls, 2)
238
+ if "success" not in auth_resp:
239
+ raise RuntimeError("Jabber authentication failed")
240
+
241
+ # ── Re-open stream after auth ────────────────────────────────────────
242
+ tls.sendall(
243
+ f"<?xml version='1.0'?><stream:stream xmlns='jabber:client' version='1.0' "
244
+ f"xmlns:stream='http://etherx.jabber.org/streams' to='{config['domain']}'>".encode()
245
+ )
246
+ time.sleep(0.5)
247
+ recv_all(tls, 1)
248
+
249
+ # ── Resource bind ────────────────────────────────────────────────────
250
+ tls.sendall(
251
+ f'<iq type="set" id="b1"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'
252
+ f'<resource>{config["resource"]}</resource></bind></iq>'.encode()
253
+ )
254
+ time.sleep(0.5)
255
+ bind_resp = recv_all(tls, 2)
256
+ bind_error = _extract_error_reason(bind_resp)
257
+ if bind_error:
258
+ raise RuntimeError(f"Jabber resource bind failed: {bind_error}")
259
+
260
+ safe_msg = message.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
261
+ msg_id = f"notify-{int(time.time() * 1000)}"
262
+
263
+ if config["to"]:
264
+ # ── Direct message ───────────────────────────────────────────────
265
+ tls.sendall(
266
+ f'<message id="{msg_id}" to="{config["to"]}" type="chat"><body>{safe_msg}</body></message>'.encode()
267
+ )
268
+ time.sleep(0.5)
269
+ else:
270
+ # ── Join MUC room ────────────────────────────────────────────────
271
+ tls.sendall(
272
+ f'<presence to="{config["room"]}/{config["nick"]}"><x xmlns="http://jabber.org/protocol/muc"/></presence>'.encode()
273
+ )
274
+ _wait_for_join_confirmation(tls, config["room"], config["nick"], timeout_seconds=20)
275
+
276
+ # ── Send MUC message ─────────────────────────────────────────────
277
+ tls.sendall(
278
+ f'<message id="{msg_id}" to="{config["room"]}" type="groupchat"><body>{safe_msg}</body></message>'.encode()
279
+ )
280
+ _wait_for_message_confirmation(tls, config["room"], msg_id, timeout_seconds=5)
281
+
282
+ # ── Leave room ───────────────────────────────────────────────────
283
+ tls.sendall(f'<presence to="{config["room"]}/{config["nick"]}" type="unavailable"/>'.encode())
284
+ time.sleep(0.3)
285
+ finally:
286
+ if tls is not None:
287
+ tls.close()
288
+ elif raw is not None:
289
+ raw.close()
290
+
291
+
292
+ if __name__ == "__main__":
293
+ if len(sys.argv) < 2:
294
+ print("Usage: python3 src/scripts/jabber_notify.py '<message>'")
295
+ sys.exit(1)
296
+ msg = sys.argv[1]
297
+ send_to_room(msg)
298
+ print("Jabber message sent")
@@ -570,7 +570,7 @@ export async function handleAutoGrayRelease(args, ctx) {
570
570
  });
571
571
  await notifier.notify(issueKey, '開始自動執行 GrayRelease 流程');
572
572
 
573
- const result = await executeGrayReleaseFlow(
573
+ const result = await executeAutoGrayReleaseFlow(
574
574
  issueKey,
575
575
  {
576
576
  maxBuildRetries: args.maxBuildRetries ?? 3,
@@ -655,7 +655,7 @@ export async function handleContinueGrayRelease(args, ctx) {
655
655
  currentStatus: status.currentStatus,
656
656
  });
657
657
 
658
- const result = await executeGrayReleaseFlow(
658
+ const result = await executeAutoGrayReleaseFlow(
659
659
  issueKey,
660
660
  {
661
661
  maxBuildRetries: args.maxBuildRetries ?? 3,
@@ -830,7 +830,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
830
830
  /**
831
831
  * 主執行流程:從當前狀態開始,自動執行到完成或需要人工介入
832
832
  */
833
- async function executeGrayReleaseFlow(issueKey, options, ctx) {
833
+ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
834
834
  const { jira, notifier } = ctx;
835
835
  const log = [];
836
836
  let buildAttempts = 0;
@@ -1007,16 +1007,16 @@ async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
1007
1007
  await notifier.notify(issueKey, '觸發 GrayRelease Deploy');
1008
1008
 
1009
1009
  // 等待部署完成:優先輪詢 deploy result、VERIFY 狀態,
1010
- // 或「To Verify」transition 是否出現。
1011
- // 最多等待 3 分鐘(避免 MCP client timeout),
1010
+ // 最多等待 10 分鐘(避免 MCP client timeout),
1012
1011
  // 若超時則回傳「部署中」訊息,請使用者稍後繼續。
1013
- const DEPLOY_WAIT_MS = Math.min(getPollTimeoutMs(), 3 * 60 * 1000);
1014
- const DEPLOY_POLL_MS = Math.min(getPollIntervalMs(), 10_000);
1012
+ // 3 分鐘 輪詢一次
1013
+ const DEPLOY_WAIT_MS = Math.min(getPollTimeoutMs(), 10 * 60 * 1000);
1014
+ const DEPLOY_POLL_MS = Math.min(getPollIntervalMs(), 3 * 60 * 1000);
1015
1015
  const deployDeadline = Date.now() + DEPLOY_WAIT_MS;
1016
1016
  let deployCompleted = false;
1017
1017
  let attempts = 0;
1018
1018
 
1019
- log.push(' ⏳ 等待部署完成(最多 3 分鐘)...');
1019
+ log.push(' ⏳ 等待部署完成(最多 10 分鐘)...');
1020
1020
  while (Date.now() <= deployDeadline) {
1021
1021
  attempts++;
1022
1022
  const issue = await jira.getIssue(issueKey);
@@ -1037,10 +1037,10 @@ async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
1037
1037
  nextPollMs: DEPLOY_POLL_MS,
1038
1038
  });
1039
1039
 
1040
- if (statusNow === 'VERIFY') {
1040
+ if (statusNow === 'VERIFY' && isPassingResult(deployResult)) {
1041
1041
  // Jira automation 或 Jenkins 已自動切到 VERIFY
1042
1042
  deployCompleted = true;
1043
- log.push(' ✅ 狀態已進入 VERIFY(自動切換),跳過手動 To Verify');
1043
+ log.push(' ✅ Deploy 完成,狀態已進入 VERIFY(cid jira worker 自動切換),進入驗證階段');
1044
1044
  break;
1045
1045
  }
1046
1046
 
@@ -1071,8 +1071,9 @@ async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
1071
1071
  await sleep(DEPLOY_POLL_MS);
1072
1072
  }
1073
1073
 
1074
+
1074
1075
  if (!deployCompleted) {
1075
- log.push(' ⚠️ 3 分鐘內未看到部署結果,部署可能仍在進行中');
1076
+ log.push(' ⚠️ 10 分鐘內未看到部署結果,部署可能仍在進行中');
1076
1077
  log.push(' 請稍後使用 get_grayrelease_status 或 deploy_grayrelease 繼續');
1077
1078
  await notifier.notify(issueKey, '部署仍在進行中,請稍後查詢狀態');
1078
1079
  return false;
@@ -1246,8 +1247,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
1246
1247
 
1247
1248
  /**
1248
1249
  * 判斷是否需要執行 Switch Execution Node
1249
- * 規則:查詢同系統上次 CD 部署的環境群組,若與本次不同則需切換
1250
- * 注意:不查詢 GrayRelease 歷史,因為 GrayRelease 永遠是 nonPrd
1250
+ * 規則:查詢同系統上次 CD or GrayRelease 部署的環境群組,若與本次不同則需切換
1251
1251
  */
1252
1252
  async function needSwitchExecutionNode(issueKey, systemCode, jira) {
1253
1253
  if (await hasRecentSwitchExecutionNodeComment(issueKey, systemCode, jira)) {
@@ -1260,12 +1260,13 @@ async function needSwitchExecutionNode(issueKey, systemCode, jira) {
1260
1260
  // 只查詢 CD 單的部署歷史(不包含 GrayRelease)
1261
1261
  const isPrdEnv = (env) => ['prd', 'dr', 'prd/dr', 'prd&dr'].includes(env?.toLowerCase()?.trim());
1262
1262
 
1263
- const jql_cd = `project = CID AND issuetype = CD AND "System Code[Select List (single choice)]" = "${systemCode}" AND status = Done AND issueKey != "${issueKey}" ORDER BY updated DESC`;
1263
+ const jql_cd = `project = CID AND (issuetype = CD OR issuetype = GrayRelease) AND text ~ "${systemCode}" AND status = Done AND issueKey != "${issueKey}" ORDER BY updated DESC`;
1264
1264
 
1265
1265
  try {
1266
1266
  const cdResults = await jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1);
1267
1267
  const cdIssue = cdResults[0] ?? null;
1268
1268
 
1269
+
1269
1270
  // 查不到歷史 → 保守執行 Switch
1270
1271
  if (!cdIssue) {
1271
1272
  return true;
@@ -1311,7 +1312,7 @@ async function hasRecentSwitchExecutionNodeComment(issueKey, systemCode, jira) {
1311
1312
  const lowerSystemCode = String(systemCode ?? '').toLowerCase();
1312
1313
  return comments.some((comment) => {
1313
1314
  const body = String(comment.body ?? '').toLowerCase();
1314
- const author = String(comment.author?.displayName ?? comment.author?.name ?? '').toLowerCase();
1315
+ const author = String(comment.author?.displayName ?? '').toLowerCase();
1315
1316
  return author === 'cid jira worker'
1316
1317
  && body.includes('instance_group')
1317
1318
  && body.includes('nonprd_executionnode')
package/tools/index.js CHANGED
@@ -727,8 +727,8 @@ export async function executeTool(name, args, deps) {
727
727
 
728
728
  // 查詢同系統最近一筆已完成的部署(CD 或 GrayRelease),回傳 'prd' | 'nonPrd' | null
729
729
  const getLastDeployEnvGroup = async (systemCode, excludeKey) => {
730
- const jql_cd = `project = CID AND issuetype = CD AND "System Code[Select List (single choice)]" = "${systemCode}" AND status = Done AND issueKey != "${excludeKey}" ORDER BY updated DESC`;
731
- const jql_gr = `project = CID AND issuetype = GrayRelease AND "System Code[Select List (single choice)]" = "${systemCode}" AND status = Done ORDER BY updated DESC`;
730
+ const jql_cd = `project = CID AND issuetype = CD AND text ~ "${systemCode}" AND status = Done AND issueKey != "${excludeKey}" ORDER BY updated DESC`;
731
+ const jql_gr = `project = CID AND issuetype = GrayRelease AND text ~ "${systemCode}" AND status = Done ORDER BY updated DESC`;
732
732
 
733
733
  const [cdResults, grResults] = await Promise.all([
734
734
  jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1).catch(() => []),
package/tools.test.js CHANGED
@@ -1666,7 +1666,7 @@ describe('auto_grayrelease — approval payload handling', () => {
1666
1666
  assert.ok(!result.content[0].text.startsWith('❌'), 'STG approval should continue');
1667
1667
  const data = JSON.parse(result.content[0].text);
1668
1668
  assert.equal(data.finalStatus, 'VERIFY');
1669
- assert.deepEqual(jira.calls.updateAssignee, [{ issueKey: 'CID-100', accountId: 'BK00619' }]);
1669
+ assert.deepEqual(jira.calls.updateAssignee, [{ issueKey: 'CID-100', accountId: 'BK00325' }]);
1670
1670
  } finally {
1671
1671
  await new Promise((resolve, reject) => {
1672
1672
  server.close((err) => (err ? reject(err) : resolve()));
@@ -1683,7 +1683,7 @@ describe('auto_grayrelease — approval payload handling', () => {
1683
1683
  process.env.POLL_TIMEOUT_MS = '10';
1684
1684
 
1685
1685
  const jira = makeApprovalJira('uat', {
1686
- comments: [{ body: 'Approved, please proceed', author: { name: 'BK00619', displayName: 'James Yu' } }],
1686
+ comments: [{ body: 'Approved, please proceed', author: { name: 'BK00325', displayName: 'James Yu' } }],
1687
1687
  });
1688
1688
  const result = await executeTool(
1689
1689
  'auto_grayrelease',
@@ -1695,8 +1695,8 @@ describe('auto_grayrelease — approval payload handling', () => {
1695
1695
  const data = JSON.parse(result.content[0].text);
1696
1696
  assert.equal(data.finalStatus, 'VERIFY');
1697
1697
  assert.deepEqual(jira.calls.updateAssignee, [
1698
- { issueKey: 'CID-101', accountId: 'BK00619' },
1699
- { issueKey: 'CID-101', accountId: 'BK00619' },
1698
+ { issueKey: 'CID-101', accountId: 'BK00325' },
1699
+ { issueKey: 'CID-101', accountId: 'BK00325' },
1700
1700
  ]);
1701
1701
  } finally {
1702
1702
  restoreEnv();
@@ -1711,7 +1711,7 @@ describe('auto_grayrelease — approval payload handling', () => {
1711
1711
  process.env.POLL_TIMEOUT_MS = '10';
1712
1712
 
1713
1713
  const jira = makeApprovalJira('uat', {
1714
- comments: [{ body: 'Approved, please proceed', author: { name: 'BK00619', displayName: 'James Yu' } }],
1714
+ comments: [{ body: 'Approved, please proceed', author: { name: 'BK00325', displayName: 'James Yu' } }],
1715
1715
  });
1716
1716
  const result = await executeTool(
1717
1717
  'auto_grayrelease',