@spaceflow/publish 0.21.1

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,602 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { ConfigService } from "@nestjs/config";
3
+ import {
4
+ GitProviderService,
5
+ ConfigReaderService,
6
+ type BranchProtection,
7
+ type CiConfig,
8
+ } from "@spaceflow/core";
9
+ import { type PublishConfig } from "./publish.config";
10
+ import { MonorepoService, type PackageInfo } from "./monorepo.service";
11
+ import type { Config } from "release-it";
12
+ import { join } from "path";
13
+ import { execSync } from "child_process";
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const releaseItModule = require("release-it") as
17
+ | { default: (opts: Config & Record<string, unknown>) => Promise<void> }
18
+ | ((opts: Config & Record<string, unknown>) => Promise<void>);
19
+
20
+ const releaseIt = typeof releaseItModule === "function" ? releaseItModule : releaseItModule.default;
21
+
22
+ import type { PublishOptions } from "./publish.command";
23
+
24
+ export interface PublishContext extends PublishOptions {
25
+ owner: string;
26
+ repo: string;
27
+ branch: string;
28
+ }
29
+
30
+ export interface PublishResult {
31
+ success: boolean;
32
+ message: string;
33
+ protection?: BranchProtection | null;
34
+ }
35
+
36
+ interface ReleaseItConfigOptions {
37
+ dryRun: boolean;
38
+ prerelease?: string;
39
+ ci: boolean;
40
+ /** 预演模式:执行 hooks 但不修改文件/git */
41
+ rehearsal: boolean;
42
+ /** 包目录(monorepo 模式)或 "."(单包模式) */
43
+ pkgDir: string;
44
+ /** 包名称(monorepo 模式)或 undefined(单包模式) */
45
+ pkgName?: string;
46
+ /** package.json 所在目录的名称,只有最后一节 */
47
+ pkgBase: string;
48
+ publishConf: PublishConfig;
49
+ }
50
+
51
+ @Injectable()
52
+ export class PublishService {
53
+ private cleanupOnExit: (() => void) | null = null;
54
+ private uncaughtExceptionHandler: ((err: Error) => void) | null = null;
55
+ private branchUnlocked = false;
56
+
57
+ constructor(
58
+ protected readonly gitProvider: GitProviderService,
59
+ protected readonly configService: ConfigService,
60
+ protected readonly configReader: ConfigReaderService,
61
+ protected readonly monorepoService: MonorepoService,
62
+ ) {}
63
+
64
+ getContextFromEnv(options: PublishOptions): PublishContext {
65
+ this.gitProvider.validateConfig();
66
+
67
+ const ciConf = this.configService.get<CiConfig>("ci");
68
+ const repository = ciConf?.repository;
69
+ const branch = ciConf?.refName;
70
+
71
+ if (!repository) {
72
+ throw new Error("缺少配置 ci.repository (环境变量 GITHUB_REPOSITORY)");
73
+ }
74
+
75
+ if (!branch) {
76
+ throw new Error("缺少配置 ci.refName (环境变量 GITHUB_REF_NAME)");
77
+ }
78
+
79
+ const [owner, repo] = repository.split("/");
80
+ if (!owner || !repo) {
81
+ throw new Error(`ci.repository 格式不正确,期望 "owner/repo",实际: "${repository}"`);
82
+ }
83
+
84
+ return {
85
+ owner,
86
+ repo,
87
+ branch,
88
+ dryRun: options.dryRun ?? false,
89
+ prerelease: options.prerelease,
90
+ ci: options.ci,
91
+ rehearsal: options.rehearsal ?? false,
92
+ };
93
+ }
94
+
95
+ async execute(context: PublishContext): Promise<PublishResult> {
96
+ const publishConf = this.configReader.getPluginConfig<PublishConfig>("publish");
97
+ const monorepoConf = publishConf.monorepo;
98
+
99
+ // CI 环境下自动 fetch tags,确保 release-it 能正确计算版本
100
+ if (context.ci) {
101
+ await this.ensureTagsFetched();
102
+ }
103
+
104
+ // 检查是否启用 monorepo 模式
105
+ if (monorepoConf?.enabled) {
106
+ return this.executeMonorepo(context, publishConf);
107
+ }
108
+
109
+ // 单包发布模式
110
+ return this.executeSinglePackage(context, publishConf);
111
+ }
112
+
113
+ /**
114
+ * Monorepo 发布模式:扫描变更包,按依赖顺序发布
115
+ */
116
+ private async executeMonorepo(
117
+ context: PublishContext,
118
+ publishConf: PublishConfig,
119
+ ): Promise<PublishResult> {
120
+ const { dryRun } = context;
121
+
122
+ console.log("\n📦 Monorepo 发布模式");
123
+ console.log("=".repeat(50));
124
+
125
+ const propagateDeps = publishConf.monorepo?.propagateDeps ?? true;
126
+
127
+ // 分析变更包
128
+ const analysis = await this.monorepoService.analyze(dryRun, propagateDeps);
129
+
130
+ if (analysis.packagesToPublish.length === 0) {
131
+ console.log("\n✅ 没有需要发布的包");
132
+ return { success: true, message: "没有需要发布的包" };
133
+ }
134
+
135
+ console.log(`\n🚀 将发布 ${analysis.packagesToPublish.length} 个包`);
136
+
137
+ await this.handleBegin(context, publishConf);
138
+
139
+ try {
140
+ // 按顺序发布每个包
141
+ for (let i = 0; i < analysis.packagesToPublish.length; i++) {
142
+ const pkg = analysis.packagesToPublish[i];
143
+ console.log(`\n[${i + 1}/${analysis.packagesToPublish.length}] 发布 ${pkg.name}`);
144
+ console.log("-".repeat(40));
145
+
146
+ await this.executePackageRelease(context, publishConf, pkg);
147
+ }
148
+
149
+ await this.handleEnd(context, publishConf);
150
+ return {
151
+ success: true,
152
+ message: `成功发布 ${analysis.packagesToPublish.length} 个包`,
153
+ };
154
+ } catch (error) {
155
+ console.error("\n❌ Monorepo 发布失败:", error instanceof Error ? error.message : error);
156
+ try {
157
+ await this.handleEnd(context, publishConf);
158
+ } catch (unlockError) {
159
+ console.error(
160
+ "⚠️ 解锁分支失败:",
161
+ unlockError instanceof Error ? unlockError.message : unlockError,
162
+ );
163
+ }
164
+ return { success: false, message: "Monorepo 发布失败" };
165
+ }
166
+ }
167
+
168
+ /**
169
+ * 发布单个包(monorepo 模式)
170
+ */
171
+ private async executePackageRelease(
172
+ context: PublishContext,
173
+ publishConf: PublishConfig,
174
+ pkg: PackageInfo,
175
+ ): Promise<void> {
176
+ const { dryRun, prerelease, ci, rehearsal } = context;
177
+
178
+ if (rehearsal) {
179
+ console.log(`🎭 [REHEARSAL] 将发布包: ${pkg.name} (${pkg.dir})`);
180
+ } else if (dryRun) {
181
+ console.log(`🔍 [DRY-RUN] 将发布包: ${pkg.name} (${pkg.dir})`);
182
+ }
183
+
184
+ const pkgDir = join(process.cwd(), pkg.dir);
185
+ const originalCwd = process.cwd();
186
+
187
+ const config = this.buildReleaseItConfig({
188
+ dryRun,
189
+ prerelease,
190
+ ci,
191
+ rehearsal,
192
+ pkgDir,
193
+ pkgName: pkg.name,
194
+ pkgBase: pkg.dir.split("/").pop() || pkg.dir,
195
+ publishConf,
196
+ });
197
+
198
+ // 切换到包目录运行 release-it,确保读取正确的 package.json
199
+ process.chdir(pkgDir);
200
+ try {
201
+ await releaseIt(config);
202
+ console.log(`✅ ${pkg.name} 发布完成`);
203
+ } finally {
204
+ // 恢复原工作目录
205
+ process.chdir(originalCwd);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * 单包发布模式
211
+ */
212
+ private async executeSinglePackage(
213
+ context: PublishContext,
214
+ publishConf: PublishConfig,
215
+ ): Promise<PublishResult> {
216
+ const { dryRun, prerelease, ci, rehearsal } = context;
217
+
218
+ await this.handleBegin(context, publishConf);
219
+
220
+ try {
221
+ const config = this.buildReleaseItConfig({
222
+ dryRun,
223
+ prerelease,
224
+ ci,
225
+ rehearsal,
226
+ pkgDir: process.cwd(),
227
+ pkgBase: process.cwd().split("/").pop() || ".",
228
+ publishConf,
229
+ });
230
+
231
+ await releaseIt(config);
232
+ } catch (error) {
233
+ console.error("执行失败:", error instanceof Error ? error.message : error);
234
+ try {
235
+ await this.handleEnd(context, publishConf);
236
+ } catch (unlockError) {
237
+ console.error(
238
+ "⚠️ 解锁分支失败:",
239
+ unlockError instanceof Error ? unlockError.message : unlockError,
240
+ );
241
+ }
242
+ return { success: false, message: "执行失败" };
243
+ }
244
+
245
+ await this.handleEnd(context, publishConf);
246
+ return { success: true, message: "执行完成", protection: null };
247
+ }
248
+
249
+ /**
250
+ * 构建 release-it 配置(公共方法)
251
+ */
252
+ private buildReleaseItConfig(opts: ReleaseItConfigOptions): Config & Record<string, unknown> {
253
+ const { dryRun, prerelease, ci, rehearsal, pkgName, pkgBase, publishConf } = opts;
254
+ const changelogConf = publishConf.changelog;
255
+ const releaseConf = publishConf.release;
256
+ const npmConf = publishConf.npm;
257
+ const gitConf = publishConf.git;
258
+
259
+ // 预演模式:设置环境变量,hooks 可以通过它判断当前模式
260
+ if (rehearsal) {
261
+ process.env.PUBLISH_REHEARSAL = "true";
262
+ }
263
+
264
+ const isMonorepo = !!pkgName;
265
+ const tagMatchOpts = !prerelease ? { tagExclude: `*[-]*` } : {};
266
+
267
+ // monorepo: @scope/pkg@1.0.0, 单包: v1.0.0
268
+ const tagPrefix = isMonorepo ? `${pkgName}@` : "v";
269
+ const tagName = isMonorepo ? `${pkgName}@\${version}` : "v${version}";
270
+ const releaseName = isMonorepo ? `${pkgName}@\${version}` : "v${version}";
271
+ const releaseTitle = isMonorepo ? `🎉 ${pkgName}@\${version}` : "🎉 v${version}";
272
+ // monorepo 模式下在包目录运行,git commitsPath 为 "."
273
+ const commitsPath = ".";
274
+ const commitMessage = isMonorepo
275
+ ? `chore(${pkgBase}): released version \${version} [no ci]`
276
+ : "chore: released version v${version} [no ci]";
277
+
278
+ // 预演模式:禁用文件/git 修改,但保留 hooks
279
+ // dryRun 模式:完全跳过所有操作(包括 hooks)
280
+ const skipWrite = dryRun || rehearsal;
281
+
282
+ return {
283
+ "dry-run": dryRun,
284
+ d: dryRun,
285
+ ci: ci || dryRun, // dry-run 模式也启用 ci 模式,避免交互式提示
286
+ plugins: {
287
+ // 预演模式下禁用 changelog 写入
288
+ ...(!skipWrite && changelogConf
289
+ ? {
290
+ "@release-it/conventional-changelog": {
291
+ // 现在在包目录下运行,使用相对路径
292
+ infile: join(
293
+ changelogConf.infileDir || ".",
294
+ `CHANGELOG${!prerelease ? "" : "-" + prerelease.toUpperCase()}.md`,
295
+ ),
296
+ preset: {
297
+ name: changelogConf.preset?.name || "conventionalcommits",
298
+ types: changelogConf.preset?.type || [],
299
+ },
300
+ },
301
+ }
302
+ : {}),
303
+ // 预演模式下禁用 release 创建
304
+ ...(!skipWrite && releaseConf
305
+ ? {
306
+ "release-it-gitea": {
307
+ releaseTitle,
308
+ releaseNotes: this.formatReleaseNotes,
309
+ assets: this.buildReleaseAssets(releaseConf),
310
+ },
311
+ }
312
+ : {}),
313
+ },
314
+ git: {
315
+ // 预演模式:禁用 push/commit/tag
316
+ push: !skipWrite,
317
+ commit: !skipWrite,
318
+ tag: !skipWrite,
319
+ tagName,
320
+ commitsPath,
321
+ commitMessage,
322
+ requireCommits: false,
323
+ requireCommitsFail: false,
324
+ getLatestTagFromAllRefs: true,
325
+ requireBranch: (gitConf?.requireBranch ?? ["main", "dev", "develop"]) as any,
326
+ requireCleanWorkingDir: !skipWrite,
327
+ ...(isMonorepo ? { tagMatch: `${tagPrefix}*` } : {}),
328
+ ...tagMatchOpts,
329
+ },
330
+ // 预演模式:禁用 npm
331
+ // 如果使用 pnpm,禁用内置 npm 发布,但保留版本更新功能
332
+ npm: skipWrite
333
+ ? (false as any)
334
+ : {
335
+ // pnpm 模式:禁用 publish(通过 hooks 实现),但保留版本更新
336
+ publish: npmConf?.packageManager === "pnpm" ? false : (npmConf?.publish ?? false),
337
+ ignoreVersion: npmConf?.ignoreVersion ?? true,
338
+ tag: prerelease || npmConf?.tag || "latest",
339
+ versionArgs: npmConf?.versionArgs ?? ["--workspaces false"],
340
+ publishArgs: npmConf?.publishArgs ?? [],
341
+ ...(npmConf?.registry ? { publishConfig: { registry: npmConf.registry } } : {}),
342
+ },
343
+ github: {
344
+ release: false,
345
+ releaseName: `Release ${releaseName}`,
346
+ autoGenerate: true,
347
+ skipChecks: true,
348
+ host: releaseConf?.host || "localhost",
349
+ },
350
+ // 合并用户 hooks 和内部 pnpm 发布 hook
351
+ hooks: this.buildHooks({
352
+ userHooks: publishConf.hooks,
353
+ npmConf,
354
+ prerelease,
355
+ skipWrite,
356
+ dryRun,
357
+ rehearsal,
358
+ }),
359
+ };
360
+ }
361
+
362
+ /**
363
+ * 格式化 release notes
364
+ */
365
+ private formatReleaseNotes(t: { changelog: string }): string {
366
+ const lines = t.changelog.split("\n");
367
+ const cateLines = lines.filter(
368
+ (line: string) => line.startsWith("###") || line.startsWith("* "),
369
+ );
370
+
371
+ const cateMap: Record<string, string[]> = {};
372
+ let currentCate = "";
373
+
374
+ cateLines.forEach((line: string) => {
375
+ if (line.startsWith("###")) {
376
+ currentCate = line;
377
+ cateMap[currentCate] = cateMap[currentCate] || [];
378
+ } else {
379
+ cateMap[currentCate].push(line);
380
+ }
381
+ });
382
+
383
+ return Object.entries(cateMap)
384
+ .map(([cate, catLines]) => `${cate}\n\n${catLines.join("\n")}\n`)
385
+ .join("\n");
386
+ }
387
+
388
+ /**
389
+ * 构建 release assets 配置
390
+ */
391
+ private buildReleaseAssets(releaseConf: NonNullable<PublishConfig["release"]>) {
392
+ const assets = releaseConf.assetSourcemap
393
+ ? [
394
+ {
395
+ path: releaseConf.assetSourcemap.path,
396
+ name: releaseConf.assetSourcemap.name,
397
+ type: "zip",
398
+ },
399
+ ]
400
+ : [];
401
+ return assets.concat(releaseConf.assets || []);
402
+ }
403
+
404
+ /**
405
+ * 构建 hooks 配置,合并用户 hooks 和内部 pnpm 发布逻辑
406
+ */
407
+ private buildHooks(opts: {
408
+ userHooks?: Record<string, string | string[]>;
409
+ npmConf?: PublishConfig["npm"];
410
+ prerelease?: string;
411
+ skipWrite: boolean;
412
+ dryRun: boolean;
413
+ rehearsal: boolean;
414
+ }): Record<string, string | string[]> | undefined {
415
+ const { userHooks, npmConf, prerelease, skipWrite, dryRun, rehearsal } = opts;
416
+
417
+ // dryRun 模式下不执行任何 hooks
418
+ if (dryRun && !rehearsal) {
419
+ return undefined;
420
+ }
421
+
422
+ // 复制用户 hooks
423
+ let hooks: Record<string, string | string[]> = { ...userHooks };
424
+
425
+ // rehearsal 模式下过滤 after 前缀的 hooks(不执行实际的发布后操作)
426
+ if (rehearsal) {
427
+ hooks = Object.fromEntries(
428
+ Object.entries(hooks).filter(([key]) => !key.startsWith("after:")),
429
+ );
430
+ // rehearsal 模式下也不添加 pnpm publish
431
+ return Object.keys(hooks).length > 0 ? hooks : undefined;
432
+ }
433
+
434
+ // 如果使用 pnpm 且需要发布
435
+ if (npmConf?.packageManager === "pnpm" && npmConf?.publish && !skipWrite) {
436
+ const tag = prerelease || npmConf.tag || "latest";
437
+ const publishArgs = npmConf.publishArgs ?? [];
438
+ const registry = npmConf.registry;
439
+
440
+ // 构建 pnpm publish 命令
441
+ // monorepo 模式下已切换到包目录,不需要 -C 参数
442
+ let publishCmd = `pnpm publish --tag ${tag} --no-git-checks`;
443
+ if (registry) {
444
+ publishCmd += ` --registry ${registry}`;
445
+ }
446
+ if (publishArgs.length > 0) {
447
+ publishCmd += ` ${publishArgs.join(" ")}`;
448
+ }
449
+
450
+ // 合并到 after:bump hook
451
+ const existingAfterBump = hooks["after:bump"];
452
+ if (existingAfterBump) {
453
+ hooks["after:bump"] = Array.isArray(existingAfterBump)
454
+ ? [...existingAfterBump, publishCmd]
455
+ : [existingAfterBump, publishCmd];
456
+ } else {
457
+ hooks["after:bump"] = publishCmd;
458
+ }
459
+ }
460
+
461
+ return Object.keys(hooks).length > 0 ? hooks : undefined;
462
+ }
463
+
464
+ protected async handleBegin(
465
+ context: PublishContext,
466
+ publishConf: PublishConfig,
467
+ ): Promise<PublishResult> {
468
+ const { owner, repo, branch, dryRun } = context;
469
+ const shouldLockBranch = publishConf.git?.lockBranch ?? true;
470
+
471
+ if (!shouldLockBranch) {
472
+ console.log(`⏭️ 跳过分支锁定(已禁用)`);
473
+ return { success: true, message: "分支锁定已禁用", protection: null };
474
+ }
475
+
476
+ const pushWhitelistUsernames = [...(publishConf.git?.pushWhitelistUsernames ?? [])];
477
+
478
+ if (dryRun) {
479
+ console.log(`🔍 [DRY-RUN] 将锁定分支: ${owner}/${repo}#${branch}`);
480
+ return { success: true, message: "DRY-RUN: 分支锁定已跳过", protection: null };
481
+ }
482
+
483
+ console.log(`🔒 正在锁定分支: ${owner}/${repo}#${branch}`);
484
+ const protection = await this.gitProvider.lockBranch(owner, repo, branch, {
485
+ pushWhitelistUsernames,
486
+ });
487
+ console.log(`✅ 分支已锁定`);
488
+ console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
489
+ if (pushWhitelistUsernames?.length) {
490
+ console.log(` 允许推送用户: ${pushWhitelistUsernames.join(", ")}`);
491
+ } else {
492
+ console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
493
+ }
494
+
495
+ // 注册进程退出时的清理函数,确保即使 release-it 调用 process.exit() 也能解锁分支
496
+ this.branchUnlocked = false;
497
+ this.cleanupOnExit = () => {
498
+ if (this.branchUnlocked) return;
499
+ this.branchUnlocked = true;
500
+ console.log("\n🔓 进程退出,正在同步解锁分支...");
501
+ try {
502
+ this.unlockBranchSync(context, publishConf);
503
+ } catch (e) {
504
+ console.error("⚠️ 同步解锁分支失败:", e instanceof Error ? e.message : e);
505
+ }
506
+ };
507
+ this.uncaughtExceptionHandler = (err: Error) => {
508
+ console.error("\n❌ 未捕获的异常:", err.message);
509
+ if (this.cleanupOnExit) this.cleanupOnExit();
510
+ process.exit(1);
511
+ };
512
+ process.on("exit", this.cleanupOnExit);
513
+ process.on("SIGINT", this.cleanupOnExit);
514
+ process.on("SIGTERM", this.cleanupOnExit);
515
+ process.on("uncaughtException", this.uncaughtExceptionHandler);
516
+
517
+ return { success: true, message: "分支锁定完成", protection };
518
+ }
519
+
520
+ protected async handleEnd(
521
+ context: PublishContext,
522
+ publishConf: PublishConfig,
523
+ ): Promise<PublishResult> {
524
+ const { owner, repo, branch, dryRun } = context;
525
+ const shouldLockBranch = publishConf.git?.lockBranch ?? true;
526
+
527
+ if (!shouldLockBranch) {
528
+ return { success: true, message: "分支锁定已禁用,无需解锁", protection: null };
529
+ }
530
+
531
+ if (dryRun) {
532
+ console.log(`🔍 [DRY-RUN] 将解锁分支: ${owner}/${repo}#${branch}`);
533
+ return { success: true, message: "DRY-RUN: 分支解锁已跳过", protection: null };
534
+ }
535
+
536
+ console.log(`🔓 正在解锁分支: ${owner}/${repo}#${branch}`);
537
+ const protection = await this.gitProvider.unlockBranch(owner, repo, branch);
538
+
539
+ // 标记已解锁,防止清理函数重复执行
540
+ this.branchUnlocked = true;
541
+
542
+ // 移除事件监听器
543
+ if (this.cleanupOnExit) {
544
+ process.removeListener("exit", this.cleanupOnExit);
545
+ process.removeListener("SIGINT", this.cleanupOnExit);
546
+ process.removeListener("SIGTERM", this.cleanupOnExit);
547
+ this.cleanupOnExit = null;
548
+ }
549
+ if (this.uncaughtExceptionHandler) {
550
+ process.removeListener("uncaughtException", this.uncaughtExceptionHandler);
551
+ this.uncaughtExceptionHandler = null;
552
+ }
553
+
554
+ if (protection) {
555
+ console.log(`✅ 分支已解锁`);
556
+ console.log(` 规则名称: ${protection.rule_name || protection.branch_name}`);
557
+ console.log(` 允许推送: ${protection.enable_push ? "是" : "否"}`);
558
+ return { success: true, message: "分支解锁完成", protection };
559
+ } else {
560
+ console.log(`✅ 分支本身没有保护规则,无需解锁`);
561
+ return { success: true, message: "分支本身没有保护规则,无需解锁", protection: null };
562
+ }
563
+ }
564
+
565
+ /**
566
+ * 同步解锁分支(用于进程退出时的清理)
567
+ */
568
+ private unlockBranchSync(context: PublishContext, publishConf: PublishConfig): void {
569
+ const { owner, repo, branch, dryRun } = context;
570
+ const shouldLockBranch = publishConf.git?.lockBranch ?? true;
571
+
572
+ if (!shouldLockBranch || dryRun) {
573
+ return;
574
+ }
575
+
576
+ this.gitProvider.unlockBranchSync(owner, repo, branch);
577
+ }
578
+
579
+ /**
580
+ * 确保 git tags 已获取(CI 环境中 shallow clone 可能缺失 tags)
581
+ * 这对于 release-it 正确计算版本号至关重要
582
+ */
583
+ private async ensureTagsFetched(): Promise<void> {
584
+ try {
585
+ // 检查是否有 tags
586
+ const existingTags = execSync("git tag --list 2>/dev/null || echo ''", {
587
+ encoding: "utf-8",
588
+ }).trim();
589
+
590
+ if (!existingTags) {
591
+ console.log("🏷️ 正在获取 git tags...");
592
+ execSync("git fetch --tags --force", { stdio: "inherit" });
593
+ console.log("✅ Git tags 已获取");
594
+ }
595
+ } catch (error) {
596
+ console.warn("⚠️ 获取 git tags 失败:", error instanceof Error ? error.message : error);
597
+ console.warn(
598
+ " 版本计算可能不准确,建议在 CI checkout 时添加 fetch-depth: 0 和 fetch-tags: true",
599
+ );
600
+ }
601
+ }
602
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../core/tsconfig.skill.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }