@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.
@@ -0,0 +1,164 @@
1
+ import {
2
+ MODULE_TO_REPO_MAP,
3
+ REPO_MAPS,
4
+ SYSTEM_TO_CI_REPO_MAP,
5
+ } from '../constants/index.js';
6
+ import { error, ok } from './helpers.js';
7
+
8
+ const SUPPORTED_TICKET_TYPES = ['library', 'grayrelease', 'ci'];
9
+
10
+ export function getBranchPRToolDefinitions() {
11
+ return [
12
+ {
13
+ name: 'validate_branch_no_open_pr',
14
+ description:
15
+ '檢查開 Library、GrayRelease 或 CI 單前,對應 Bitbucket branch 是否有尚未 merge 的 OPEN pull request。Library/GrayRelease 使用輸入 branch;CI 固定檢查 master。CD 與 Deployment 不需要使用。',
16
+ inputSchema: {
17
+ type: 'object',
18
+ required: ['ticketType', 'systemCode'],
19
+ properties: {
20
+ ticketType: {
21
+ type: 'string',
22
+ enum: SUPPORTED_TICKET_TYPES,
23
+ description: '要檢查的開單類型:library / grayrelease / ci。',
24
+ },
25
+ systemCode: {
26
+ type: 'string',
27
+ description: '系統代碼,例如 IBK、CWA。CI 會用 systemCode 找 Golden Image repo。',
28
+ },
29
+ module: {
30
+ type: 'string',
31
+ description: 'Library 或 GrayRelease 模組名稱,例如 ssr、cwa。CI 不需要。',
32
+ },
33
+ branch: {
34
+ type: 'string',
35
+ description: 'Library / GrayRelease 要檢查的 Git branch。CI 會忽略此值並固定檢查 master。',
36
+ },
37
+ },
38
+ },
39
+ },
40
+ ];
41
+ }
42
+
43
+ export async function handleValidateBranchNoOpenPR(args, { jira }) {
44
+ try {
45
+ const result = await validateBranchNoOpenPR(args, { jira });
46
+ return ok(result);
47
+ } catch (err) {
48
+ return error(`validate_branch_no_open_pr 失敗: ${err.message}`);
49
+ }
50
+ }
51
+
52
+ export async function assertNoOpenPRBeforeCreate(args, { jira }) {
53
+ const result = await validateBranchNoOpenPR(args, { jira });
54
+ if (result.blocked) {
55
+ throw new Error(formatOpenPRBlockMessage(result));
56
+ }
57
+ return result;
58
+ }
59
+
60
+ export async function validateBranchNoOpenPR(args, { jira }) {
61
+ const target = resolveBranchPRCheckTarget(args);
62
+ const pullRequests = await jira.getBitbucketPullRequests(target.project, target.repo, {
63
+ branch: target.branch,
64
+ state: 'OPEN',
65
+ limit: 100,
66
+ });
67
+ const openPullRequests = normalizePullRequests(pullRequests, target.branch);
68
+
69
+ return {
70
+ ok: openPullRequests.length === 0,
71
+ blocked: openPullRequests.length > 0,
72
+ reason: openPullRequests.length > 0 ? 'branch_has_open_pull_requests' : null,
73
+ ticketType: target.ticketType,
74
+ systemCode: target.systemCode,
75
+ module: target.module,
76
+ repo: target.repo,
77
+ project: target.project,
78
+ branch: target.branch,
79
+ openPullRequests,
80
+ message: openPullRequests.length > 0
81
+ ? `${target.branch} 仍有尚未 merge 的 PR,請先完成 merge 後再開單。`
82
+ : `${target.branch} 沒有尚未 merge 的 OPEN PR,可繼續開單。`,
83
+ };
84
+ }
85
+
86
+ export function resolveBranchPRCheckTarget(args) {
87
+ const ticketType = String(args.ticketType ?? '').trim().toLowerCase();
88
+ const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
89
+ const module = String(args.module ?? '').trim().toLowerCase();
90
+ const branch = ticketType === 'ci'
91
+ ? 'master'
92
+ : String(args.branch ?? args.gitBranch ?? '').trim();
93
+
94
+ if (!SUPPORTED_TICKET_TYPES.includes(ticketType)) {
95
+ throw new Error(`不支援的 PR 檢查類型:${args.ticketType},僅支援 ${SUPPORTED_TICKET_TYPES.join(' / ')}`);
96
+ }
97
+ if (!systemCode) {
98
+ throw new Error('缺少 systemCode');
99
+ }
100
+ if (ticketType !== 'ci' && !module) {
101
+ throw new Error(`${ticketType} PR 檢查缺少 module`);
102
+ }
103
+ if (!branch) {
104
+ throw new Error(`${ticketType} PR 檢查缺少 branch`);
105
+ }
106
+
107
+ const repo = ticketType === 'ci'
108
+ ? SYSTEM_TO_CI_REPO_MAP[systemCode]
109
+ : MODULE_TO_REPO_MAP[module];
110
+ if (!repo) {
111
+ throw new Error(ticketType === 'ci'
112
+ ? `找不到系統 "${systemCode}" 的 CI repo 對應,請確認 SYSTEM_TO_CI_REPO_MAP`
113
+ : `找不到模組 "${module}" 的 repo 對應,請確認 MODULE_TO_REPO_MAP`);
114
+ }
115
+
116
+ const repoMeta = REPO_MAPS[repo];
117
+ if (!repoMeta?.project) {
118
+ throw new Error(`找不到 repo "${repo}" 的 Bitbucket 設定`);
119
+ }
120
+
121
+ return {
122
+ ticketType,
123
+ systemCode,
124
+ module: module || null,
125
+ repo,
126
+ project: repoMeta.project,
127
+ branch,
128
+ };
129
+ }
130
+
131
+ function normalizePullRequests(pullRequests, branch) {
132
+ return (pullRequests ?? [])
133
+ .filter((pr) => isOpenPullRequestFromBranch(pr, branch))
134
+ .map((pr) => ({
135
+ id: pr.id,
136
+ title: pr.title ?? '',
137
+ state: pr.state ?? '',
138
+ fromRef: pr.fromRef?.displayId ?? normalizeRefId(pr.fromRef?.id),
139
+ toRef: pr.toRef?.displayId ?? normalizeRefId(pr.toRef?.id),
140
+ author: pr.author?.user?.displayName ?? pr.author?.user?.name ?? '',
141
+ url: pr.links?.self?.[0]?.href ?? pr.link?.url ?? '',
142
+ }));
143
+ }
144
+
145
+ function isOpenPullRequestFromBranch(pr, branch) {
146
+ if (String(pr.state ?? '').toUpperCase() !== 'OPEN') {
147
+ return false;
148
+ }
149
+ const expectedRef = `refs/heads/${branch}`;
150
+ return pr.fromRef?.displayId === branch
151
+ || pr.fromRef?.id === expectedRef
152
+ || normalizeRefId(pr.fromRef?.id) === branch;
153
+ }
154
+
155
+ function normalizeRefId(refId) {
156
+ return String(refId ?? '').replace(/^refs\/heads\//, '');
157
+ }
158
+
159
+ function formatOpenPRBlockMessage(result) {
160
+ const prLines = result.openPullRequests
161
+ .map((pr) => `#${pr.id} ${pr.title}${pr.url ? ` ${pr.url}` : ''}`)
162
+ .join('; ');
163
+ return `${result.branch} 仍有尚未 merge 的 PR,請先完成 merge 後再開單。${prLines ? ` (${prLines})` : ''}`;
164
+ }
package/tools/build.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Build 相關共用 tools
3
+ * - wait_build_result
4
+ */
5
+ import { SHARED_FIELD_IDS } from '../constants/index.js';
6
+ import {
7
+ error,
8
+ getPollIntervalMs,
9
+ getPollTimeoutMs,
10
+ isFailingResult,
11
+ isPassingResult,
12
+ ok,
13
+ } from './helpers.js';
14
+
15
+ function progress(ctx, event) {
16
+ if (typeof ctx.progress === 'function') {
17
+ ctx.progress(event);
18
+ }
19
+ }
20
+
21
+ // ── Schema definitions ───────────────────────────────────────────
22
+ export function getBuildToolDefinitions() {
23
+ return [
24
+ {
25
+ name: 'wait_build_result',
26
+ description:
27
+ '輪詢等待 Jira issue 的共用 Build 結果欄位變成 pass 或 fail。支援 CI、Library、GrayRelease;通常接在 build_ci、build_library 或 build_grayrelease 成功觸發 Jenkins build 後使用。',
28
+ inputSchema: {
29
+ type: 'object',
30
+ required: ['issueKey'],
31
+ properties: {
32
+ issueKey: {
33
+ type: 'string',
34
+ description: '要等待 build 結果的 issue key,例如 CI、Library 或 GrayRelease 單 CID-822',
35
+ },
36
+ resultFieldId: {
37
+ type: 'string',
38
+ description: '(選填) Build 結果欄位 ID,預設 customfield_13432',
39
+ default: SHARED_FIELD_IDS.buildResult,
40
+ },
41
+ pollIntervalMs: {
42
+ type: 'number',
43
+ description: '輪詢間隔毫秒,預設讀 POLL_INTERVAL_MS',
44
+ },
45
+ timeoutMs: {
46
+ type: 'number',
47
+ description: '最長等待毫秒,預設讀 POLL_TIMEOUT_MS',
48
+ },
49
+ },
50
+ },
51
+ },
52
+ ];
53
+ }
54
+
55
+ // ── Handlers ─────────────────────────────────────────────────────
56
+ export async function handleWaitBuildResult(args, ctx) {
57
+ const { issueKey } = args;
58
+ const { jira } = ctx;
59
+ const fieldId = args.resultFieldId ?? SHARED_FIELD_IDS.buildResult;
60
+ const intervalMs = args.pollIntervalMs ?? getPollIntervalMs();
61
+ const timeoutMs = args.timeoutMs ?? getPollTimeoutMs();
62
+ const startedAt = Date.now();
63
+ const deadline = Date.now() + timeoutMs;
64
+ let attempts = 0;
65
+ let lastValue;
66
+ let currentStatus;
67
+
68
+ try {
69
+ while (true) {
70
+ attempts++;
71
+ const issue = await jira.getIssue(issueKey);
72
+ currentStatus = issue.fields.status?.name;
73
+ const fields = await jira.getIssueFields(issueKey, [fieldId]);
74
+ const raw = fields[fieldId];
75
+ lastValue = raw?.value ?? raw;
76
+
77
+ progress(ctx, {
78
+ phase: 'polling',
79
+ title: '等待 Build 結果',
80
+ detail: `${fieldId}: ${lastValue ?? 'empty'}`,
81
+ issueKey,
82
+ currentStatus,
83
+ attempts,
84
+ elapsedMs: Date.now() - startedAt,
85
+ timeoutMs,
86
+ nextPollMs: intervalMs,
87
+ });
88
+
89
+ if (isPassingResult(lastValue)) {
90
+ return ok({
91
+ issueKey,
92
+ fieldId,
93
+ result: lastValue,
94
+ buildResult: lastValue,
95
+ currentStatus,
96
+ attempts,
97
+ });
98
+ }
99
+
100
+ if (isFailingResult(lastValue)) {
101
+ return error(`Build 失敗,${fieldId}: ${lastValue}`);
102
+ }
103
+
104
+ if (Date.now() >= deadline) {
105
+ return error(`Build 結果等待逾時,${fieldId}: ${lastValue ?? 'empty'}`);
106
+ }
107
+
108
+ await sleep(intervalMs);
109
+ }
110
+ } catch (err) {
111
+ return error(`wait_build_result 失敗: ${err.message}`);
112
+ }
113
+ }
114
+
115
+ function sleep(ms) {
116
+ return new Promise((resolve) => setTimeout(resolve, ms));
117
+ }