@cloudbase/manager-node 5.4.0-beta.2 → 5.5.0

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/lib/env/index.js CHANGED
@@ -136,18 +136,33 @@ class EnvService {
136
136
  * @returns {Promise<IEnvInfoRes>}
137
137
  */
138
138
  async getEnvInfo() {
139
- // NOTE: DescribeEnv 接口废弃,需要使用 DescribeEnvs 接口
139
+ var _a;
140
+ // 使用 DescribeEnvInfo 进行单环境查询
140
141
  const params = {
141
142
  EnvId: this.envId
142
143
  };
143
- if (this.envType === 'run') {
144
- params.EnvType = 'run';
145
- }
146
- const { EnvList, RequestId } = await this.describeEnvs(params);
144
+ const response = await this.describeEnvInfo(params);
145
+ const envBaseInfo = (_a = response === null || response === void 0 ? void 0 : response.EnvInfo) === null || _a === void 0 ? void 0 : _a.EnvBaseInfo;
146
+ // 成功获取环境信息,将 EnvBaseInfo 映射到 EnvInfo 类型
147
+ const envInfo = {
148
+ EnvId: envBaseInfo.EnvId,
149
+ Alias: envBaseInfo.Alias,
150
+ Status: envBaseInfo.Status,
151
+ Databases: envBaseInfo.Databases,
152
+ Storages: envBaseInfo.Storages,
153
+ Functions: envBaseInfo.Functions,
154
+ PackageId: envBaseInfo.PackageId || '',
155
+ PackageName: envBaseInfo.PackageName || '',
156
+ LogServices: envBaseInfo.LogServices || [],
157
+ CustomLogServices: envBaseInfo.CustomLogServices || [],
158
+ Region: envBaseInfo.Region || '',
159
+ Source: envBaseInfo.Source,
160
+ CreateTime: envBaseInfo.CreateTime,
161
+ UpdateTime: envBaseInfo.UpdateTime
162
+ };
147
163
  return {
148
- // @ts-ignore
149
- EnvInfo: (EnvList === null || EnvList === void 0 ? void 0 : EnvList.length) ? EnvList[0] : {},
150
- RequestId
164
+ EnvInfo: envInfo,
165
+ RequestId: response.RequestId || ''
151
166
  };
152
167
  }
153
168
  /**
@@ -156,6 +171,18 @@ class EnvService {
156
171
  async describeEnvs(params) {
157
172
  return this.cloudService.request('DescribeEnvs', params);
158
173
  }
174
+ /**
175
+ * 查询单个环境信息
176
+ *
177
+ * 用于单个环境查询时,规避 DescribeEnvs 的 backend issues。
178
+ * 当 EnvId 参数存在时,应优先使用本方法而非 describeEnvs。
179
+ *
180
+ * @param params 查询参数,必须包含 EnvId
181
+ * @returns 环境信息响应
182
+ */
183
+ async describeEnvInfo(params) {
184
+ return this.cloudService.request('DescribeEnvInfo', params);
185
+ }
159
186
  /**
160
187
  * 修改环境名称
161
188
  * @param {string} alias 环境名称
@@ -476,28 +503,28 @@ class EnvService {
476
503
  async calculatePackageRenewPrice(params) {
477
504
  var _a, _b, _c, _d, _e, _f;
478
505
  const { envId, period = 1, currency = 'CNY' } = params;
479
- const [packageRes, envRes, billingRes] = await Promise.all([
506
+ const [packageRes, envInfoRes, billingRes] = await Promise.all([
480
507
  this.describeBaasPackageList({
481
508
  Source: 'qcloud',
482
509
  PackageTypeList: ['default', 'vip', 'basic', 'trial']
483
510
  }),
484
- this.describeEnvs({
511
+ this.describeEnvInfo({
485
512
  EnvId: envId
486
513
  }),
487
514
  this.describeBillingInfo({
488
515
  EnvId: envId
489
516
  })
490
517
  ]);
491
- const envInfo = (_a = envRes.EnvList) === null || _a === void 0 ? void 0 : _a[0];
518
+ const envBaseInfo = (_a = envInfoRes === null || envInfoRes === void 0 ? void 0 : envInfoRes.EnvInfo) === null || _a === void 0 ? void 0 : _a.EnvBaseInfo;
492
519
  const billingInfo = (_b = billingRes.EnvBillingInfoList) === null || _b === void 0 ? void 0 : _b[0];
493
- if (!envInfo || !billingInfo) {
520
+ if (!(envBaseInfo === null || envBaseInfo === void 0 ? void 0 : envBaseInfo.EnvId) || !billingInfo) {
494
521
  throw new error_1.CloudBaseError(`Invalid env id: ${envId}`);
495
522
  }
496
- const packageInfo = (_c = packageRes.PackageList) === null || _c === void 0 ? void 0 : _c.find(item => item.PackageName === envInfo.PackageId);
523
+ const packageInfo = (_c = packageRes.PackageList) === null || _c === void 0 ? void 0 : _c.find(item => item.PackageName === envBaseInfo.PackageId);
497
524
  if (!packageInfo) {
498
525
  throw new error_1.CloudBaseError(`Invalid env id: ${envId}`);
499
526
  }
500
- const region = envInfo.Region;
527
+ const region = envBaseInfo.Region;
501
528
  const extPackageData = ((_d = billingInfo.ExtPackageCode) === null || _d === void 0 ? void 0 : _d.reduce((acc, { Code, Num }) => {
502
529
  acc[Code] = Num;
503
530
  return acc;
@@ -530,21 +557,21 @@ class EnvService {
530
557
  async calculatePackageModifyPrice(params) {
531
558
  var _a, _b, _c, _d, _e, _f, _g;
532
559
  const { newPackageId, envId, currency = 'CNY' } = params;
533
- const [packageRes, envRes, billingRes] = await Promise.all([
560
+ const [packageRes, envInfoRes, billingRes] = await Promise.all([
534
561
  this.describeBaasPackageList({
535
562
  Source: 'qcloud',
536
563
  PackageTypeList: ['default', 'vip', 'basic', 'trial']
537
564
  }),
538
- this.describeEnvs({
565
+ this.describeEnvInfo({
539
566
  EnvId: envId
540
567
  }),
541
568
  this.describeBillingInfo({
542
569
  EnvId: envId
543
570
  })
544
571
  ]);
545
- const envInfo = (_a = envRes.EnvList) === null || _a === void 0 ? void 0 : _a[0];
572
+ const envBaseInfo = (_a = envInfoRes === null || envInfoRes === void 0 ? void 0 : envInfoRes.EnvInfo) === null || _a === void 0 ? void 0 : _a.EnvBaseInfo;
546
573
  const billingInfo = (_b = billingRes.EnvBillingInfoList) === null || _b === void 0 ? void 0 : _b[0];
547
- if (!envInfo || !billingInfo || !billingInfo.PackageId) {
574
+ if (!(envBaseInfo === null || envBaseInfo === void 0 ? void 0 : envBaseInfo.EnvId) || !billingInfo || !billingInfo.PackageId) {
548
575
  throw new error_1.CloudBaseError(`Invalid env id: ${envId}`);
549
576
  }
550
577
  const oldPackageInfo = (_c = packageRes.PackageList) === null || _c === void 0 ? void 0 : _c.find(item => item.PackageName === billingInfo.PackageId);
@@ -555,7 +582,7 @@ class EnvService {
555
582
  if (!newPackageInfo) {
556
583
  throw new error_1.CloudBaseError(`Invalid package id: ${newPackageId}`);
557
584
  }
558
- const region = envInfo.Region;
585
+ const region = envBaseInfo.Region;
559
586
  const newBillTags = this.parseBillTags(newPackageInfo.BillTags);
560
587
  const oldBillTags = this.parseBillTags(oldPackageInfo.BillTags);
561
588
  let userNumber = 0;
@@ -342,39 +342,57 @@ class FunctionService {
342
342
  return res;
343
343
  }
344
344
  catch (e) {
345
- // 函数存在
345
+ // 函数已存在(同名冲突)
346
346
  const functionExist = e.code === 'ResourceInUse.FunctionName' || e.code === 'ResourceInUse.Function';
347
- // 已存在同名函数,强制更新
348
- if (functionExist && force) {
349
- // 1. 更新函数代码(通过 deployMode 区分镜像部署和代码部署)
350
- const codeRes = await this.updateFunctionCode({
351
- func,
352
- base64Code,
353
- functionPath,
354
- functionRootPath,
355
- codeSecret: codeSecret,
356
- deployMode: isImageDeploy ? 'image' : undefined
357
- });
358
- // 等待函数状态正常
359
- await this.waitFunctionActive(funcName, codeSecret);
360
- // 2. 更新函数配置
361
- const configRes = await this.updateFunctionConfig(func);
362
- // 等待函数状态正常
363
- await this.waitFunctionActive(funcName, codeSecret);
364
- // 3. 创建函数触发器
365
- const triggerRes = await this.retryCreateTrigger(funcName, func.triggers);
366
- // 设置路径,创建云接入路径
367
- if (func.path) {
368
- await this.createAccessPath(funcName, func.path);
347
+ if (functionExist) {
348
+ // 检查同名函数是否是 CREATE_FAILED 残留
349
+ // 场景:上次部署因依赖安装失败留下了残留函数,用户修正参数后重新部署(不传 force)
350
+ // 此时应自动清理残留并重新创建,而不是报错要求用户手动删除或传 force
351
+ try {
352
+ const { Status } = await this.getFunctionDetail(funcName, codeSecret);
353
+ if (Status === constant_1.SCF_STATUS.CREATE_FAILED) {
354
+ console.warn(`[${funcName}] 检测到同名函数处于 CREATE_FAILED 状态,自动清理后重新创建...`);
355
+ await this.deleteFunction(funcName);
356
+ // 重新执行创建(递归调用,此时同名函数已删除,不会再碰到 ResourceInUse)
357
+ return this.createFunction(funcParam);
358
+ }
359
+ }
360
+ catch (checkErr) {
361
+ // 查询或删除失败,继续走原有逻辑(force 判断)
362
+ console.warn(`[${funcName}] 检查残留状态失败(可忽略): ${checkErr.message}`);
363
+ }
364
+ // 已存在且状态正常,强制覆盖更新
365
+ if (force) {
366
+ // 1. 更新函数代码(通过 deployMode 区分镜像部署和代码部署)
367
+ const codeRes = await this.updateFunctionCode({
368
+ func,
369
+ base64Code,
370
+ functionPath,
371
+ functionRootPath,
372
+ codeSecret: codeSecret,
373
+ deployMode: isImageDeploy ? 'image' : undefined
374
+ });
375
+ // 等待函数状态正常
376
+ await this.waitFunctionActive(funcName, codeSecret);
377
+ // 2. 更新函数配置
378
+ const configRes = await this.updateFunctionConfig(func);
379
+ // 等待函数状态正常
380
+ await this.waitFunctionActive(funcName, codeSecret);
381
+ // 3. 创建函数触发器
382
+ const triggerRes = await this.retryCreateTrigger(funcName, func.triggers);
383
+ // 设置路径,创建云接入路径
384
+ if (func.path) {
385
+ await this.createAccessPath(funcName, func.path);
386
+ }
387
+ // 检查函数状态
388
+ await this.waitFunctionActive(funcName, codeSecret);
389
+ // 返回全部操作的响应值
390
+ return {
391
+ triggerRes,
392
+ configRes,
393
+ codeRes
394
+ };
369
395
  }
370
- // 检查函数状态
371
- await this.waitFunctionActive(funcName, codeSecret);
372
- // 返回全部操作的响应值
373
- return {
374
- triggerRes,
375
- configRes,
376
- codeRes
377
- };
378
396
  }
379
397
  // 不强制覆盖,抛出错误
380
398
  if (e.message && !force) {
@@ -399,8 +417,8 @@ class FunctionService {
399
417
  async getFunctionList(limit = 20, offset = 0) {
400
418
  // 获取Function 环境配置
401
419
  const { namespace } = this.getFunctionConfig();
402
- const res = await this.scfService.request('ListFunctions', {
403
- Namespace: namespace,
420
+ const res = await this.tcbService.request('ListFunctions', {
421
+ EnvId: namespace,
404
422
  Limit: limit,
405
423
  Offset: offset
406
424
  });
@@ -415,8 +433,8 @@ class FunctionService {
415
433
  async listFunctions(limit = 20, offset = 0) {
416
434
  // 获取Function 环境配置
417
435
  const { namespace } = this.getFunctionConfig();
418
- const res = await this.scfService.request('ListFunctions', {
419
- Namespace: namespace,
436
+ const res = await this.tcbService.request('ListFunctions', {
437
+ EnvId: namespace,
420
438
  Limit: limit,
421
439
  Offset: offset
422
440
  });
@@ -447,8 +465,8 @@ class FunctionService {
447
465
  const { envId } = options;
448
466
  while (true) {
449
467
  try {
450
- const res = await this.scfService.request('ListFunctions', {
451
- Namespace: envId,
468
+ const res = await this.tcbService.request('ListFunctions', {
469
+ EnvId: envId,
452
470
  Limit: pageSize,
453
471
  Offset: currentOffset
454
472
  });
package/lib/log/index.js CHANGED
@@ -19,7 +19,7 @@ exports.isLogServiceEnabled = isLogServiceEnabled;
19
19
  const utils_1 = require("../utils");
20
20
  __exportStar(require("./types"), exports);
21
21
  /**
22
- * 纯函数:根据 DescribeEnvs 返回的 LogServices 列表判断日志服务是否已开通。
22
+ * 纯函数:根据 DescribeEnvInfo 返回的 LogServices 列表判断日志服务是否已开通。
23
23
  * 已开通条件:至少一个条目有有效的 TopicId。
24
24
  */
25
25
  function isLogServiceEnabled(logServices) {
@@ -36,21 +36,17 @@ class LogService {
36
36
  /**
37
37
  * 检查当前环境的日志服务是否已开通
38
38
  *
39
- * 通过 DescribeEnvs 接口获取环境信息,判断 LogServices 字段中是否包含有效的
39
+ * 通过 DescribeEnvInfo 接口获取环境信息,判断 LogServices 字段中是否包含有效的
40
40
  * 日志主题(TopicId 不为空)。
41
41
  *
42
42
  * @returns true 表示已开通,false 表示未开通或开通中
43
43
  */
44
44
  async checkLogServiceEnabled() {
45
- var _a;
46
- const res = await this.cloudService.request('DescribeEnvs', {
45
+ var _a, _b;
46
+ const res = await this.cloudService.request('DescribeEnvInfo', {
47
47
  EnvId: this.envId
48
48
  });
49
- const envInfo = (_a = res === null || res === void 0 ? void 0 : res.EnvList) === null || _a === void 0 ? void 0 : _a[0];
50
- if (!envInfo) {
51
- return false;
52
- }
53
- const logServices = envInfo.LogServices;
49
+ const logServices = (_b = (_a = res === null || res === void 0 ? void 0 : res.EnvInfo) === null || _a === void 0 ? void 0 : _a.EnvBaseInfo) === null || _b === void 0 ? void 0 : _b.LogServices;
54
50
  if (!logServices || logServices.length === 0) {
55
51
  return false;
56
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/manager-node",
3
- "version": "5.4.0-beta.2",
3
+ "version": "5.5.0",
4
4
  "description": "The node manage service api for cloudbase.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CI Changelog 生成脚本(MR 粒度)
5
+ *
6
+ * 从上一个 tag 到当前 HEAD 的 merge commit 中提取 MR 信息,
7
+ * 按 conventional commit 分类,生成 changelog 并追加到 changelog.md 头部。
8
+ *
9
+ * 优势:
10
+ * - 每个 MR 对应一条 changelog,避免零散 commit 造成条目过多
11
+ * - 从 MR body 或分支名智能推断变更类型
12
+ * - 自动提取 MR 编号,生成可跳转链接
13
+ *
14
+ * 用法: node scripts/ci/generate-changelog.js
15
+ * 环境变量:
16
+ * MANAGER_NODE_VERSION - 当前发布的版本号(必须)
17
+ * GIT_REPO_URL - 仓库地址(可选,用于生成 MR 链接,默认为工蜂地址)
18
+ * CHANGELOG_OUTPUT - changelog 输出文件路径(可选,设置后将内容写入该文件,供下游步骤读取)
19
+ */
20
+
21
+ const { execSync } = require('child_process')
22
+ const fs = require('fs')
23
+ const path = require('path')
24
+
25
+ const VERSION = process.env.MANAGER_NODE_VERSION
26
+ if (!VERSION) {
27
+ console.error('❌ 环境变量 MANAGER_NODE_VERSION 未设置')
28
+ process.exit(1)
29
+ }
30
+
31
+ // 仓库 URL,用于生成 MR 链接(当前 formatMR 暂未使用,预留供后续扩展)
32
+ // const REPO_URL = process.env.GIT_REPO_URL || 'https://git.woa.com/QBase/server-side-sdk/cloudbase-manager-node'
33
+
34
+ // 获取上一个 tag
35
+ // 获取上一个 tag(按版本号排序,排除当前版本)
36
+ // 不使用 git describe(它依赖 commit 祖先关系,在 release 分支上可能跳过中间 tag)
37
+ function getLastTag() {
38
+ try {
39
+ const tags = execSync('git tag --sort=-version:refname', {
40
+ encoding: 'utf8'
41
+ }).trim().split('\n').filter(Boolean)
42
+
43
+ const currentTag = `v${VERSION}`
44
+ for (const tag of tags) {
45
+ // 跳过当前版本,找到第一个有效的 semver tag
46
+ if (tag !== currentTag && /^v?\d+\.\d+\.\d+/.test(tag)) {
47
+ return tag
48
+ }
49
+ }
50
+ return ''
51
+ } catch {
52
+ return ''
53
+ }
54
+ }
55
+
56
+ // 获取 merge commits(MR 粒度)
57
+ function getMergeCommits(fromTag) {
58
+ const range = fromTag ? `${fromTag}..HEAD` : 'HEAD'
59
+ const separator = '__|__'
60
+ const recordSeparator = '==RECORD=='
61
+ try {
62
+ // %s = subject, %h = short hash, %an = author, %b = body
63
+ const log = execSync(
64
+ `git log ${range} --merges --pretty=format:"%s${separator}%h${separator}%an${separator}%b${recordSeparator}"`,
65
+ { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }
66
+ ).trim()
67
+ if (!log) return []
68
+
69
+ return log
70
+ .split(recordSeparator)
71
+ .filter((record) => record.trim())
72
+ .map((record) => {
73
+ const parts = record.trim().split(separator)
74
+ const subject = (parts[0] || '').trim()
75
+ const hash = (parts[1] || '').trim()
76
+ const author = (parts[2] || '').trim()
77
+ const body = (parts[3] || '').trim()
78
+ return { subject, hash, author, body }
79
+ })
80
+ } catch {
81
+ return []
82
+ }
83
+ }
84
+
85
+ // 从 merge commit subject 中提取 MR 编号和分支名
86
+ // 格式: "Merge branch '<branch>' into '<target>' (merge request !<number>)"
87
+ function parseMergeSubject(subject) {
88
+ const mrMatch = subject.match(/\(merge request !(\d+)\)/)
89
+ const branchMatch = subject.match(/Merge branch '([^']+)' into/)
90
+
91
+ return {
92
+ mrNumber: mrMatch ? mrMatch[1] : '',
93
+ branch: branchMatch ? branchMatch[1] : ''
94
+ }
95
+ }
96
+
97
+ // 从分支名推断变更类型
98
+ // 常见: feature/xxx, fix/xxx, hotfix/xxx, refactor/xxx, docs/xxx, chore/xxx
99
+ function inferTypeFromBranch(branch) {
100
+ const branchLower = branch.toLowerCase()
101
+ if (branchLower.startsWith('feature/') || branchLower.startsWith('feat/')) return 'feat'
102
+ if (branchLower.startsWith('fix/') || branchLower.startsWith('hotfix/') || branchLower.startsWith('bugfix/')) return 'fix'
103
+ if (branchLower.startsWith('refactor/')) return 'refactor'
104
+ if (branchLower.startsWith('perf/')) return 'perf'
105
+ if (branchLower.startsWith('docs/')) return 'docs'
106
+ if (branchLower.startsWith('chore/') || branchLower.startsWith('ci/')) return 'chore'
107
+ if (branchLower.startsWith('revert')) return 'revert'
108
+ return ''
109
+ }
110
+
111
+ // 从分支名提取简短描述
112
+ function getDescriptionFromBranch(branch) {
113
+ // 去掉 feature/, fix/ 等前缀
114
+ const desc = branch.replace(/^(feature|feat|fix|hotfix|bugfix|refactor|perf|docs|chore|ci|revert)[/-]/, '')
115
+ // 将 kebab-case 或 camelCase 转为可读文本
116
+ return desc
117
+ .replace(/[-_]/g, ' ')
118
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
119
+ .trim()
120
+ }
121
+
122
+ // 解析 conventional commit 格式的文本
123
+ function parseConventional(text) {
124
+ const match = text.match(/^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/)
125
+ if (!match) return null
126
+ const [, type, scope = '', breaking, description] = match
127
+ return { type: type.toLowerCase(), scope, description, breaking: !!breaking }
128
+ }
129
+
130
+ // 早期过滤:基于 subject 和分支名判断是否为噪音 MR
131
+ function shouldFilterEarly(subject, branch) {
132
+ // 1. 过滤 "Merge branch 'master' into xxx" — 中间合并不是功能变更
133
+ // 注意:有些带 MR 编号的也是中间合并,subject 格式为
134
+ // "Merge branch 'feature/xxx' into 'master' (merge request !N)"
135
+ // 但 body 里是 "Merge branch 'master' into feature/xxx"
136
+ if (/Merge branch '(master|main|dev|develop)' into/i.test(subject)) return true
137
+
138
+ // 2. 分支名为纯 merge 操作的
139
+ if (/^merge[/-](master|main|dev)/i.test(branch)) return true
140
+
141
+ // 3. 分支名就是纯版本号的,如 "2.9.4", "v3.0.1"
142
+ if (/^v?\d+\.\d+(\.\d+)?(-\w+)?$/.test(branch)) return true
143
+
144
+ return false
145
+ }
146
+
147
+ // 后期过滤:基于解析后的描述判断是否为噪音 MR
148
+ function shouldFilterByDescription(description, body) {
149
+ // 1. 纯 "update v" 类发版 MR(bump version)
150
+ if (/^update\s*v(\d|$)/i.test(description)) return true
151
+ if (/^bump\s*(version|v)/i.test(description)) return true
152
+ if (/^update\s*version/i.test(description)) return true
153
+
154
+ // 2. 描述就是版本号的,如 "2.9.4", "cli 2.11.7"
155
+ if (/^(cli\s+)?v?\d+\.\d+(\.\d+)?$/i.test(description.trim())) return true
156
+
157
+ // 3. 描述为纯合并操作的
158
+ if (/^合并\s*(master|main)\s*到/i.test(description)) return true
159
+ if (/^Merge\s+branch\s+'(master|main|dev|develop)'\s+into/i.test(description)) return true
160
+ if (/^Merge\s+(master|main)\s+(to|into)/i.test(description)) return true
161
+
162
+ // 4. body 中描述为纯合并的(fallback)
163
+ const bodyTrimmed = body.trim()
164
+ if (/^合并\s*(master|main)\s*到/i.test(bodyTrimmed)) return true
165
+ if (/^Merge\s+branch\s+'(master|main|dev|develop)'\s+into/i.test(bodyTrimmed)) return true
166
+
167
+ return false
168
+ }
169
+
170
+ // 从描述内容推断更智能的类型(优先于分支名推断)
171
+ function inferTypeFromDescription(description) {
172
+ const descLower = description.toLowerCase()
173
+ // 文档相关关键词
174
+ if (/^(文档|docs?|readme|changelog|注释|说明文档)/.test(descLower)) return 'docs'
175
+ if (/更新文档|补充文档|修改文档|添加文档/.test(description)) return 'docs'
176
+ // 重构相关
177
+ if (/^(重构|refactor)/.test(descLower)) return 'refactor'
178
+ // 性能优化
179
+ if (/^(优化|性能|perf)/.test(descLower)) return 'perf'
180
+ return ''
181
+ }
182
+
183
+ // 解析一个 MR 的完整信息
184
+ function parseMR(mergeCommit) {
185
+ const { subject, hash, author, body } = mergeCommit
186
+ const { mrNumber, branch } = parseMergeSubject(subject)
187
+
188
+ // 跳过非 MR 的 merge(如 merge master into feature branch)
189
+ if (!mrNumber) return null
190
+
191
+ // 早期过滤:基于 subject 和分支名
192
+ if (shouldFilterEarly(subject, branch)) return null
193
+
194
+ // 优先从 body 中找 conventional commit 描述
195
+ let type = 'other'
196
+ let scope = ''
197
+ let description = ''
198
+ let breaking = false
199
+
200
+ // body 可能有多行,逐行找第一个有意义的 conventional commit 格式
201
+ const bodyLines = body.split('\n').map((l) => l.trim()).filter(Boolean)
202
+ for (const line of bodyLines) {
203
+ // 跳过分支名本身的重复
204
+ if (line === branch) continue
205
+ const parsed = parseConventional(line)
206
+ if (parsed) {
207
+ type = parsed.type
208
+ scope = parsed.scope
209
+ description = parsed.description
210
+ breaking = parsed.breaking
211
+ break
212
+ }
213
+ // 如果不是 conventional 格式但也不是分支名,用第一个有意义的行作为描述
214
+ if (!description && line.length > 3) {
215
+ description = line
216
+ }
217
+ }
218
+
219
+ // 如果 body 没有提供有效信息,从分支名推断
220
+ if (type === 'other' && branch) {
221
+ const inferredType = inferTypeFromBranch(branch)
222
+ if (inferredType) type = inferredType
223
+ }
224
+
225
+ if (!description && branch) {
226
+ description = getDescriptionFromBranch(branch)
227
+ }
228
+
229
+ if (!description) {
230
+ description = subject
231
+ }
232
+
233
+ // 后期过滤:基于解析后的描述内容(过滤 "update v" 等发版 MR)
234
+ if (shouldFilterByDescription(description, body)) return null
235
+
236
+ // 更智能的分类:描述内容优先于分支名推断
237
+ // 例如 feature/ 分支但描述是"文档内容"的,应归类为 docs
238
+ if (type === 'feat' || type === 'fix' || type === 'other') {
239
+ const descType = inferTypeFromDescription(description)
240
+ if (descType) type = descType
241
+ }
242
+
243
+ return { type, scope, description, hash, author, mrNumber, breaking }
244
+ }
245
+
246
+ // 分类 MR
247
+ function categorizeMRs(mergeCommits) {
248
+ const categories = {
249
+ breaking: [],
250
+ feat: [],
251
+ fix: [],
252
+ perf: [],
253
+ refactor: [],
254
+ revert: [],
255
+ docs: [],
256
+ chore: [],
257
+ other: []
258
+ }
259
+
260
+ for (const mc of mergeCommits) {
261
+ const parsed = parseMR(mc)
262
+ if (!parsed) continue // 跳过非 MR 的 merge
263
+
264
+ if (parsed.breaking) {
265
+ categories.breaking.push(parsed)
266
+ } else if (categories[parsed.type]) {
267
+ categories[parsed.type].push(parsed)
268
+ } else {
269
+ categories.other.push(parsed)
270
+ }
271
+ }
272
+
273
+ return categories
274
+ }
275
+
276
+ // 格式化一条 MR
277
+ // 类型 → 中文前缀映射
278
+ const TYPE_PREFIX = {
279
+ breaking: '不兼容变更',
280
+ feat: '新增',
281
+ fix: '修复',
282
+ perf: '优化',
283
+ refactor: '重构',
284
+ revert: '回滚',
285
+ docs: '文档',
286
+ chore: '优化',
287
+ other: ''
288
+ }
289
+
290
+ function formatMR(parsed) {
291
+ const prefix = TYPE_PREFIX[parsed.type] || ''
292
+ const desc = parsed.description
293
+ // 如果描述已经以前缀开头(如"新增 xxx"),不重复添加
294
+ const needPrefix = prefix && !desc.startsWith(prefix)
295
+ const text = needPrefix ? `${prefix} ${desc}` : desc
296
+ return `- ${text}`
297
+ }
298
+
299
+ // 生成 changelog 内容(扁平列表格式)
300
+ function generateChangelog(categories) {
301
+ const date = new Date().toISOString().split('T')[0]
302
+ const lines = [`## @cloudbase/manager-node v${VERSION} 版本发布 🚀(${date})`, '']
303
+
304
+ // 按优先级排列:不兼容变更 → 新功能 → 优化/重构 → 修复 → 其他
305
+ const order = ['breaking', 'feat', 'perf', 'refactor', 'chore', 'revert', 'docs', 'fix', 'other']
306
+
307
+ let hasContent = false
308
+ for (const key of order) {
309
+ const items = categories[key]
310
+ if (items && items.length > 0) {
311
+ hasContent = true
312
+ for (const item of items) {
313
+ lines.push(formatMR(item))
314
+ }
315
+ }
316
+ }
317
+
318
+ // 如果完全没有 MR
319
+ if (!hasContent) {
320
+ lines.push('- 版本发布')
321
+ }
322
+
323
+ lines.push('')
324
+ return lines.join('\n')
325
+ }
326
+
327
+ // 确保 CI 环境有完整的 git 历史和 tags
328
+ function ensureFullGitHistory() {
329
+ try {
330
+ // CI 通常是浅克隆,需要 unshallow 才能拿到完整历史
331
+ const isShallow = fs.existsSync(path.join(
332
+ execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim(),
333
+ 'shallow'
334
+ ))
335
+ if (isShallow) {
336
+ console.log('🔄 检测到浅克隆,正在获取完整历史...')
337
+ execSync('git fetch --unshallow', { encoding: 'utf8', stdio: 'inherit' })
338
+ }
339
+ } catch {
340
+ // 非浅克隆或 unshallow 失败,忽略
341
+ }
342
+
343
+ try {
344
+ // 确保拉取所有 tags(CI 默认可能不拉 tags)
345
+ console.log('🏷️ 拉取所有 tags...')
346
+ execSync('git fetch --tags', { encoding: 'utf8', stdio: 'inherit' })
347
+ } catch {
348
+ console.warn('⚠️ 拉取 tags 失败,将使用本地已有的 tags')
349
+ }
350
+ }
351
+
352
+ // 主流程
353
+ function main() {
354
+ console.log(`📝 正在为 v${VERSION} 生成 changelog(MR 粒度)...`)
355
+
356
+ // CI 环境需要确保有完整历史和 tags
357
+ ensureFullGitHistory()
358
+
359
+ const lastTag = getLastTag()
360
+ if (lastTag) {
361
+ console.log(` 上一个 tag: ${lastTag}`)
362
+ } else {
363
+ console.log(' 未找到历史 tag,将使用所有 merge commit')
364
+ }
365
+
366
+ const mergeCommits = getMergeCommits(lastTag)
367
+ console.log(` 找到 ${mergeCommits.length} 个 merge commit`)
368
+
369
+ const categories = categorizeMRs(mergeCommits)
370
+ const totalMRs = Object.values(categories).reduce((sum, arr) => sum + arr.length, 0)
371
+ console.log(` 有效 MR: ${totalMRs} 个`)
372
+
373
+ const newChangelog = generateChangelog(categories)
374
+
375
+ console.log('\n--- 生成的 changelog ---')
376
+ console.log(newChangelog)
377
+ console.log('--- end ---\n')
378
+
379
+ // 读取现有 changelog
380
+ const changelogPath = path.join(__dirname, '../../CHANGELOG.md')
381
+ let existingContent = ''
382
+ if (fs.existsSync(changelogPath)) {
383
+ existingContent = fs.readFileSync(changelogPath, 'utf8')
384
+ }
385
+
386
+ // 插入新内容到头部
387
+ const finalContent = newChangelog + '\n---\n\n' + existingContent
388
+
389
+ fs.writeFileSync(changelogPath, finalContent, 'utf8')
390
+ console.log('📄 changelog.md 已更新')
391
+
392
+ // 将本次生成的 changelog 内容单独输出到指定文件
393
+ // 用于 CI 流水线中传递给下游步骤(如企微通知)
394
+ // 用法: export CHANGELOG_OUTPUT="/path/to/changelog_content.md"
395
+ const outputPath = process.env.CHANGELOG_OUTPUT
396
+ if (outputPath) {
397
+ // 企微消息有 4KB 左右的大小限制,超长时截断并添加提示
398
+ const MAX_WEWORK_SIZE = 3800 // 预留一些余量
399
+ let outputContent = newChangelog
400
+ if (Buffer.byteLength(outputContent, 'utf8') > MAX_WEWORK_SIZE) {
401
+ // 按行截断,保留标题和尽量多的条目
402
+ const lines = outputContent.split('\n')
403
+ let truncated = ''
404
+ for (const line of lines) {
405
+ const next = truncated + line + '\n'
406
+ if (Buffer.byteLength(next, 'utf8') > MAX_WEWORK_SIZE - 100) {
407
+ break
408
+ }
409
+ truncated = next
410
+ }
411
+ const totalItems = newChangelog.split('\n').filter(l => l.startsWith('- ')).length
412
+ const keptItems = truncated.split('\n').filter(l => l.startsWith('- ')).length
413
+ truncated += `\n... 等共 ${totalItems} 项变更(已截断,完整内容见 changelog.md)\n`
414
+ outputContent = truncated
415
+ console.log(`⚠️ changelog 过长 (${Buffer.byteLength(newChangelog, 'utf8')} bytes),已截断为 ${keptItems}/${totalItems} 项`)
416
+ }
417
+ fs.writeFileSync(outputPath, outputContent, 'utf8')
418
+ console.log(`📄 changelog 内容已输出到: ${outputPath}`)
419
+ }
420
+ }
421
+
422
+ main()
@@ -1,7 +1,7 @@
1
1
  import { Environment } from '../environment';
2
2
  import { IResponseInfo, AuthDomain, EnvInfo, LoginConfigItem, ICheckTcbServiceRes, ICreatePostpayRes, EnvBillingInfoItem, PriceResult } from '../interfaces';
3
3
  import { CalculatePackageCreatePriceParams, CalculatePackageRenewPriceParams, CalculatePackageModifyPriceParams, DescribeHttpServiceRouteParams, DescribeHttpServiceRouteRes, CreateHttpServiceRouteParams, CreateHttpServiceRouteRes, ModifyHttpServiceRouteParams, ModifyHttpServiceRouteRes, DeleteHttpServiceRouteParams, DeleteHttpServiceRouteRes, BindCustomDomainParams, BindCustomDomainRes, DeleteCustomDomainParams, DeleteCustomDomainRes } from './type';
4
- import { CreateBillingDealParams, CreateEnvParams, BaasPackageInfo, DescribeBaasPackageListParams, ModifyEnvPlanParams, RenewEnvParams, DestroyEnvParams, DescribeEnvsParams, DescribeEnvAccountCircleParams, DescribeEnvAccountCircleRes, DescribeCreditsUsageDetailParams, EnvPkgCreditsUsage } from '../interfaces/tcb.interface';
4
+ import { CreateBillingDealParams, CreateEnvParams, BaasPackageInfo, DescribeBaasPackageListParams, ModifyEnvPlanParams, RenewEnvParams, DestroyEnvParams, DescribeEnvsParams, DescribeEnvInfoParams, DescribeEnvInfoResponse, DescribeEnvAccountCircleParams, DescribeEnvAccountCircleRes, DescribeCreditsUsageDetailParams, EnvPkgCreditsUsage } from '../interfaces/tcb.interface';
5
5
  type SOURCE = 'miniapp' | 'qcloud';
6
6
  interface IDeleteDomainRes {
7
7
  RequestId: string;
@@ -629,6 +629,16 @@ export declare class EnvService {
629
629
  EnvList: EnvInfo[];
630
630
  Total: number;
631
631
  }>;
632
+ /**
633
+ * 查询单个环境信息
634
+ *
635
+ * 用于单个环境查询时,规避 DescribeEnvs 的 backend issues。
636
+ * 当 EnvId 参数存在时,应优先使用本方法而非 describeEnvs。
637
+ *
638
+ * @param params 查询参数,必须包含 EnvId
639
+ * @returns 环境信息响应
640
+ */
641
+ describeEnvInfo(params: DescribeEnvInfoParams): Promise<DescribeEnvInfoResponse>;
632
642
  /**
633
643
  * 修改环境名称
634
644
  * @param {string} alias 环境名称
@@ -460,3 +460,67 @@ export interface EnvPkgCreditsUsage {
460
460
  ReportValue?: number;
461
461
  OriginCredits?: number;
462
462
  }
463
+ /**
464
+ * DescribeEnvInfo API - 查询单个环境信息
465
+ * 替代 DescribeEnvs 用于单环境查询,用于规避 backend issues when filtering by EnvId
466
+ */
467
+ export interface DescribeEnvInfoParams {
468
+ EnvId: string;
469
+ }
470
+ export interface EnvBaseInfo {
471
+ EnvId?: string;
472
+ Alias?: string;
473
+ Status?: string;
474
+ EnvType?: string;
475
+ Region?: string;
476
+ PackageId?: string;
477
+ PackageName?: string;
478
+ Databases?: DatabasesInfo[];
479
+ Storages?: StorageInfo[];
480
+ Functions?: FunctionInfo[];
481
+ PayMode?: string;
482
+ Source?: string;
483
+ Tags?: Array<{
484
+ Key: string;
485
+ Value: string;
486
+ }>;
487
+ CreateTime?: string;
488
+ UpdateTime?: string;
489
+ EnvPreferences?: Record<string, any>;
490
+ IsAutoDegrade?: boolean;
491
+ IsDauPackage?: boolean;
492
+ PostgreSQL?: Record<string, any>;
493
+ LogServices?: Array<{
494
+ LogsetId?: string;
495
+ TopicId?: string;
496
+ TopicName?: string;
497
+ Region?: string;
498
+ }> | null;
499
+ CustomLogServices?: Array<{
500
+ CreateTime: string;
501
+ ClsLogsetId: string;
502
+ ClsTopicId: string;
503
+ ClsRegion: string;
504
+ }> | null;
505
+ }
506
+ export interface BillingInfo {
507
+ StartTime?: string;
508
+ EndTime?: string;
509
+ Status?: string;
510
+ }
511
+ export interface UserInfo {
512
+ AppId?: string;
513
+ Uin?: string;
514
+ WxAppId?: string;
515
+ }
516
+ export interface DescribeEnvInfoResponse extends IResponseInfo {
517
+ EnvInfo?: {
518
+ EnvBaseInfo?: EnvBaseInfo;
519
+ BillingInfo?: BillingInfo;
520
+ UserInfo?: UserInfo;
521
+ Eid?: number;
522
+ CircleStartTime?: string;
523
+ CircleEndTime?: string;
524
+ SiteFlag?: number;
525
+ };
526
+ }
@@ -11,7 +11,7 @@ export interface ILogServiceEntry {
11
11
  Region?: string;
12
12
  }
13
13
  /**
14
- * 纯函数:根据 DescribeEnvs 返回的 LogServices 列表判断日志服务是否已开通。
14
+ * 纯函数:根据 DescribeEnvInfo 返回的 LogServices 列表判断日志服务是否已开通。
15
15
  * 已开通条件:至少一个条目有有效的 TopicId。
16
16
  */
17
17
  export declare function isLogServiceEnabled(logServices?: ILogServiceEntry[]): boolean;
@@ -23,7 +23,7 @@ export declare class LogService {
23
23
  /**
24
24
  * 检查当前环境的日志服务是否已开通
25
25
  *
26
- * 通过 DescribeEnvs 接口获取环境信息,判断 LogServices 字段中是否包含有效的
26
+ * 通过 DescribeEnvInfo 接口获取环境信息,判断 LogServices 字段中是否包含有效的
27
27
  * 日志主题(TopicId 不为空)。
28
28
  *
29
29
  * @returns true 表示已开通,false 表示未开通或开通中
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(TCB_BASE_URL=https://tcb.pre.tencentcloudapi.woa.com npm test -- --testPathPattern=cloudApp 2>&1)",
5
- "Bash(TCB_BASE_URL=https://tcb.pre.tencentcloudapi.woa.com npm test 2>&1)",
6
- "Bash(TCB_BASE_URL=https://tcb.pre.tencentcloudapi.woa.com npm test -- --testPathPattern=cloudApp --testNamePattern=\"获取版本列表\")"
7
- ]
8
- }
9
- }