@applift/core 0.5.0 → 0.7.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/dist/{plugin-CTt0LO8c.d.mts → frameworks-D83HVg8t.d.mts} +120 -59
- package/dist/index.d.mts +71 -10
- package/dist/index.mjs +481 -903
- package/dist/plugins/index.d.mts +74 -3
- package/dist/plugins/index.mjs +437 -2
- package/dist/project-4_orxby_.mjs +806 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { C as readJson, S as findArtifacts, T as subdirs, _ as suggestFramework, a as electronFramework, b as deps, c as knownFrameworkAdapters, d as nativeHarmonyAdapter, f as nativeHarmonyFramework, g as rnFramework, h as rnAdapter, i as electronAdapter, l as nativeAndroidAdapter, m as nativeIosFramework, n as writeProjectVersions, o as flutterAdapter, p as nativeIosAdapter, r as redactText, s as flutterFramework, t as readProjectVersions, u as nativeAndroidFramework, v as tauriAdapter, w as resolveFramework, x as detectBundleIds, y as tauriFramework } from "./project-4_orxby_.mjs";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -41,15 +42,32 @@ profiles: z.record(z.string(), CredentialsProfileSchema) });
|
|
|
41
42
|
/** 单渠道配置(appId、pkgName 等渠道侧键) */
|
|
42
43
|
const StoreConfigSchema = z.record(z.string(), z.string());
|
|
43
44
|
/**
|
|
44
|
-
* stores
|
|
45
|
+
* stores 等价写法,统一规范化为 record:
|
|
45
46
|
* ① 对象:{ huawei: { appId: 'x' } }(原有写法,原样保留)
|
|
46
47
|
* ② 数组:['oppo', 'vivo'] → { oppo: {}, vivo: {} }(无配置渠道)
|
|
47
48
|
* ③ 字符串值:{ huawei: '10xxx' } → { huawei: { appId: '10xxx' } }(appId 简写)
|
|
49
|
+
* ④ 混排数组(README 首例):['huawei', { yyb: 'appId值' }] → 元素对象须单键 { 渠道: 值 },
|
|
50
|
+
* 值走 ③ 同款 appId 简写归一
|
|
48
51
|
*/
|
|
49
|
-
const
|
|
50
|
-
if (Array.isArray(stores)) return Object.fromEntries(stores.
|
|
52
|
+
const storesNormalize = (stores) => {
|
|
53
|
+
if (Array.isArray(stores)) return Object.fromEntries(stores.flatMap((el) => {
|
|
54
|
+
if (typeof el === "string") return [[el, {}]];
|
|
55
|
+
const [id, cfg] = Object.entries(el)[0] ?? [];
|
|
56
|
+
if (id === void 0 || cfg === void 0) return [];
|
|
57
|
+
return [[id, typeof cfg === "string" ? { appId: cfg } : cfg]];
|
|
58
|
+
}));
|
|
51
59
|
return Object.fromEntries(Object.entries(stores).map(([id, cfg]) => [id, typeof cfg === "string" ? { appId: cfg } : cfg]));
|
|
52
|
-
}
|
|
60
|
+
};
|
|
61
|
+
const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z.string()])), z.array(z.union([z.string(), StoreConfigSchema]))]).superRefine((val, ctx) => {
|
|
62
|
+
if (!Array.isArray(val)) return;
|
|
63
|
+
for (const el of val) {
|
|
64
|
+
if (typeof el === "string") continue;
|
|
65
|
+
if (Object.keys(el).length !== 1) ctx.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
message: `stores 数组内的对象元素必须形如 { 渠道id: appId简写 }(单键),收到: ${JSON.stringify(el)}`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}).transform(storesNormalize).default({});
|
|
53
71
|
/** 项目级配置(applift.config.ts 的形状);beta 段见上方 BetaConfigSchema */
|
|
54
72
|
/**
|
|
55
73
|
* 内测分发配置(applift beta 无平台参数时并行发布的渠道开关):
|
|
@@ -58,8 +76,6 @@ const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z
|
|
|
58
76
|
* - testflight: App Store Connect 上传(ipa 进 TestFlight)
|
|
59
77
|
*/
|
|
60
78
|
const BetaConfigSchema = z.object({
|
|
61
|
-
/** 自动 version:true(走推断)| major/minor/patch/prerelease | <semver>;CLI --bump/--no-bump 覆盖 */
|
|
62
|
-
bump: z.union([z.boolean(), z.string()]).optional(),
|
|
63
79
|
pgyer: z.object({
|
|
64
80
|
platforms: z.array(z.enum([
|
|
65
81
|
"android",
|
|
@@ -86,35 +102,29 @@ const VersionCodeConfigSchema = z.union([z.literal("increment"), z.object({
|
|
|
86
102
|
platforms: z.array(z.string()).optional()
|
|
87
103
|
})]);
|
|
88
104
|
const VersionConfigSchema = z.object({ versionCode: VersionCodeConfigSchema.default("increment") });
|
|
89
|
-
/** 正式发版段的自动 version 开关(语义同 beta.bump) */
|
|
90
|
-
const ReleaseConfigSchema = z.object({ bump: z.union([z.boolean(), z.string()]).optional() });
|
|
91
105
|
const ProjectConfigSchema = z.object({
|
|
92
106
|
/** 项目显示名 */
|
|
93
107
|
name: z.string().optional(),
|
|
94
108
|
/** 团队默认凭据 profile(个人用 applift use / --profile 覆盖),缺省 default */
|
|
95
109
|
profile: z.string().default("default"),
|
|
96
110
|
android: z.object({
|
|
97
|
-
|
|
111
|
+
/** 缺省经 detectBundleIds 从工程文件自动推导(README 首例即只有 stores 的最小配置) */
|
|
112
|
+
pkgName: z.string().optional(),
|
|
98
113
|
/** 渠道 → 渠道侧配置(appId 等);stores 里列出的渠道才发 */
|
|
99
114
|
stores: StoresSchema
|
|
100
115
|
}).optional(),
|
|
101
116
|
harmony: z.object({
|
|
102
|
-
|
|
117
|
+
/** 缺省自动推导(AppScope/app.json5) */
|
|
118
|
+
bundleName: z.string().optional(),
|
|
103
119
|
appId: z.string().optional()
|
|
104
120
|
}).optional(),
|
|
105
121
|
ios: z.object({
|
|
106
|
-
scheme
|
|
122
|
+
/** 缺省自动推导(工程 scheme 探测) */
|
|
123
|
+
scheme: z.string().optional(),
|
|
107
124
|
workspace: z.string().optional(),
|
|
108
125
|
/** App Store Connect 按 bundleId 定位 App(asc 渠道 submit 用) */
|
|
109
126
|
bundleId: z.string().optional()
|
|
110
127
|
}).optional(),
|
|
111
|
-
/** 构建命令(缺省用项目自带 npm scripts,见 pipeline 约定) */
|
|
112
|
-
build: z.object({
|
|
113
|
-
android: z.string().optional(),
|
|
114
|
-
harmony: z.string().optional(),
|
|
115
|
-
harmonyApp: z.string().optional(),
|
|
116
|
-
ios: z.string().optional()
|
|
117
|
-
}).optional(),
|
|
118
128
|
/** 产物目录覆盖(非标准工程):每平台 glob 数组,优先于框架内置 pattern(桌面平台 + 插件自报平台经 catchall 收) */
|
|
119
129
|
artifacts: z.object({
|
|
120
130
|
android: z.array(z.string()).optional(),
|
|
@@ -126,8 +136,6 @@ const ProjectConfigSchema = z.object({
|
|
|
126
136
|
}).catchall(z.array(z.string()).optional()).optional(),
|
|
127
137
|
/** 版本管理(versionCode 策略等) */
|
|
128
138
|
version: VersionConfigSchema.optional(),
|
|
129
|
-
/** 正式发版自动 version 开关 */
|
|
130
|
-
release: ReleaseConfigSchema.optional(),
|
|
131
139
|
/** 内测分发(beta 无平台参数时的多渠道并行配置) */
|
|
132
140
|
beta: BetaConfigSchema.optional()
|
|
133
141
|
});
|
|
@@ -208,14 +216,16 @@ function resolveProfile(creds, profile, knownKeys) {
|
|
|
208
216
|
for (const [envName, value] of Object.entries(process.env)) if (envName.startsWith("APPLIFT_") && value) merged[envName.slice(8)] = value;
|
|
209
217
|
return merged;
|
|
210
218
|
}
|
|
211
|
-
/**
|
|
219
|
+
/** 全局配置装载(缺失/损坏降级默认值;警告经返回值交给调用方,不阻断) */
|
|
212
220
|
function loadGlobalConfig(filePath = GLOBAL_CONFIG_PATH) {
|
|
213
|
-
if (!fs.existsSync(filePath)) return GlobalConfigSchema.parse({});
|
|
221
|
+
if (!fs.existsSync(filePath)) return { config: GlobalConfigSchema.parse({}) };
|
|
214
222
|
try {
|
|
215
|
-
return GlobalConfigSchema.parse(parse(fs.readFileSync(filePath, "utf8")));
|
|
223
|
+
return { config: GlobalConfigSchema.parse(parse(fs.readFileSync(filePath, "utf8"))) };
|
|
216
224
|
} catch (e) {
|
|
217
|
-
|
|
218
|
-
|
|
225
|
+
return {
|
|
226
|
+
config: GlobalConfigSchema.parse({}),
|
|
227
|
+
warning: `全局配置解析失败,使用默认值: ${e.message}`
|
|
228
|
+
};
|
|
219
229
|
}
|
|
220
230
|
}
|
|
221
231
|
/** 写凭据(目录自动创建,文件强制 0600) */
|
|
@@ -240,10 +250,19 @@ function appliftAlias() {
|
|
|
240
250
|
const distEntry = fileURLToPath(import.meta.resolve("@applift/core"));
|
|
241
251
|
const coreEntry = fs.existsSync(distEntry) ? distEntry : path.join(path.dirname(path.dirname(distEntry)), "src", "index.ts");
|
|
242
252
|
if (!fs.existsSync(coreEntry)) return {};
|
|
243
|
-
|
|
253
|
+
const alias = {
|
|
244
254
|
"@applift/core": coreEntry,
|
|
245
255
|
"@applift/cli": coreEntry
|
|
246
256
|
};
|
|
257
|
+
try {
|
|
258
|
+
const distPlugins = fileURLToPath(import.meta.resolve("@applift/core/plugins"));
|
|
259
|
+
const pluginsEntry = fs.existsSync(distPlugins) ? distPlugins : path.join(path.dirname(coreEntry), "plugins", "index.ts");
|
|
260
|
+
if (fs.existsSync(pluginsEntry)) {
|
|
261
|
+
alias["@applift/core/plugins"] = pluginsEntry;
|
|
262
|
+
alias["@applift/cli/plugins"] = pluginsEntry;
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
return alias;
|
|
247
266
|
} catch {
|
|
248
267
|
return {};
|
|
249
268
|
}
|
|
@@ -266,8 +285,32 @@ async function loadProjectConfig(cwd) {
|
|
|
266
285
|
packageJson: false,
|
|
267
286
|
defaults: ProjectConfigSchema.parse({})
|
|
268
287
|
});
|
|
288
|
+
const parsed = ProjectConfigSchema.safeParse(result.config ?? {});
|
|
289
|
+
if (!parsed.success) {
|
|
290
|
+
const details = parsed.error.issues.map((i) => `${i.path.join(".") || "(根)"}: ${i.message}`).join(";");
|
|
291
|
+
throw new Error(`applift.config.ts 字段校验失败 → ${details}`);
|
|
292
|
+
}
|
|
293
|
+
const config = parsed.data;
|
|
294
|
+
const rawRec = result.config ?? {};
|
|
295
|
+
const deprecated = [];
|
|
296
|
+
if ("build" in rawRec) deprecated.push({
|
|
297
|
+
key: "build",
|
|
298
|
+
hint: "构建已回归项目自身(applift 不接管构建),请删除该键"
|
|
299
|
+
});
|
|
300
|
+
if ("bump" in (rawRec.beta ?? {})) deprecated.push({
|
|
301
|
+
key: "beta.bump",
|
|
302
|
+
hint: "改用 applift version bump 显式升版本(applift 不再代构建)"
|
|
303
|
+
});
|
|
304
|
+
if ("release" in rawRec && "bump" in (rawRec.release ?? {})) deprecated.push({
|
|
305
|
+
key: "release.bump",
|
|
306
|
+
hint: "改用 applift version bump 显式升版本(applift 不再代构建)"
|
|
307
|
+
});
|
|
308
|
+
if (deprecated.length > 0) {
|
|
309
|
+
const lines = deprecated.map((d) => ` - ${d.key}: ${d.hint}`).join("\n");
|
|
310
|
+
process.stderr.write(`⚠ applift.config.ts 含已废弃配置键(将被忽略):\n${lines}\n`);
|
|
311
|
+
}
|
|
269
312
|
return {
|
|
270
|
-
config
|
|
313
|
+
config,
|
|
271
314
|
raw: result.config ?? {},
|
|
272
315
|
cwd: result.cwd ?? cwd
|
|
273
316
|
};
|
|
@@ -305,13 +348,27 @@ function setProjectProfile(cwd, profile, filePath = PROJECTS_MAP_PATH) {
|
|
|
305
348
|
saveProjectProfileMap(map, filePath);
|
|
306
349
|
}
|
|
307
350
|
/**
|
|
308
|
-
* 解析项目生效 profile
|
|
309
|
-
* CLI 的 --profile 旗标在调用方以 `opts.profile ??
|
|
351
|
+
* 解析项目生效 profile 及其来源:projects.json 个人选择 > 项目配置 profile(团队默认)> 'default'。
|
|
352
|
+
* CLI 的 --profile 旗标在调用方以 `opts.profile ?? ...` 再盖一层(最高优先级,来源即"旗标")。
|
|
310
353
|
*/
|
|
311
|
-
function
|
|
354
|
+
function resolveProfileSource(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
|
|
312
355
|
const mapped = loadProjectProfileMap(filePath)[path.resolve(cwd)];
|
|
313
|
-
if (mapped) return
|
|
314
|
-
|
|
356
|
+
if (mapped) return {
|
|
357
|
+
profile: mapped,
|
|
358
|
+
source: "applift use(个人)"
|
|
359
|
+
};
|
|
360
|
+
if (projectCfgProfile) return {
|
|
361
|
+
profile: projectCfgProfile,
|
|
362
|
+
source: "项目配置 profile(团队默认)"
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
profile: "default",
|
|
366
|
+
source: "默认 default"
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** 解析项目生效 profile(只取名不问来源;来源用 resolveProfileSource) */
|
|
370
|
+
function resolveProfileFor(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
|
|
371
|
+
return resolveProfileSource(cwd, projectCfgProfile, filePath).profile;
|
|
315
372
|
}
|
|
316
373
|
//#endregion
|
|
317
374
|
//#region src/channels/shared/sign.ts
|
|
@@ -370,8 +427,24 @@ async function formPost(url, params, { timeout = 6e4 } = {}) {
|
|
|
370
427
|
}
|
|
371
428
|
}
|
|
372
429
|
//#endregion
|
|
430
|
+
//#region src/channels/shared/file-blob.ts
|
|
431
|
+
/**
|
|
432
|
+
* 惰性文件 Blob:FormData multipart 上传的文件部件。
|
|
433
|
+
* 优先 fs.openAsBlob(Node ≥19.8,按需分片读,零整读内存);
|
|
434
|
+
* 能力检测不存在时回退整读(旧运行时兜底)。
|
|
435
|
+
*/
|
|
436
|
+
async function fileBlob(p) {
|
|
437
|
+
if (typeof fs.openAsBlob === "function") try {
|
|
438
|
+
return await fs.openAsBlob(p);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
const cause = e instanceof Error ? e.message : String(e);
|
|
441
|
+
throw new Error(`打开上传文件失败 ${p}: ${cause}`);
|
|
442
|
+
}
|
|
443
|
+
return new Blob([await fs.promises.readFile(p)]);
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
373
446
|
//#region src/util.ts
|
|
374
|
-
/** core
|
|
447
|
+
/** core 内部共用小工具(sleep/formatSize 仅内部使用;sha256File 经 index 导出,CLI 历史指纹用) */
|
|
375
448
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
376
449
|
/** 分块计算文件 SHA256(产物可达上百 MB,8MB 分块避免整读内存,同 sign.ts md5File 先例) */
|
|
377
450
|
function sha256File(filePath) {
|
|
@@ -413,7 +486,8 @@ async function pgyerPost(host, apiPath, form) {
|
|
|
413
486
|
const res = await fetch(`https://${host}${apiPath}`, {
|
|
414
487
|
method: "POST",
|
|
415
488
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
416
|
-
body
|
|
489
|
+
body,
|
|
490
|
+
signal: AbortSignal.timeout(3e4)
|
|
417
491
|
});
|
|
418
492
|
const text = await res.text();
|
|
419
493
|
try {
|
|
@@ -422,6 +496,20 @@ async function pgyerPost(host, apiPath, form) {
|
|
|
422
496
|
throw new Error(`蒲公英 ${apiPath} 返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
423
497
|
}
|
|
424
498
|
}
|
|
499
|
+
/** 按 HOSTS 序尝试的 POST(任一域名成功即返回;全挂抛最后一个错误)——状态查询等幂等接口用 */
|
|
500
|
+
async function pgyerPostFailover(ctx, apiPath, form) {
|
|
501
|
+
let lastErr;
|
|
502
|
+
for (const host of HOSTS) try {
|
|
503
|
+
return await pgyerPost(host, apiPath, form);
|
|
504
|
+
} catch (e) {
|
|
505
|
+
lastErr = e;
|
|
506
|
+
ctx.emit({
|
|
507
|
+
type: "warn",
|
|
508
|
+
message: `${host} 不可用,尝试备用域名`
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
throw lastErr instanceof Error ? lastErr : /* @__PURE__ */ new Error(`蒲公英 ${apiPath} 请求失败`);
|
|
512
|
+
}
|
|
425
513
|
var PgyerChannel = class {
|
|
426
514
|
id = "pgyer";
|
|
427
515
|
label = "蒲公英";
|
|
@@ -431,6 +519,8 @@ var PgyerChannel = class {
|
|
|
431
519
|
scheduled: false,
|
|
432
520
|
status: false
|
|
433
521
|
};
|
|
522
|
+
/** 平台错误码 → 人话(1247:源码注释已证 = buildKey 尚未就绪/COS 仍在入库) */
|
|
523
|
+
errorHints = { "1247": "buildKey 尚未就绪(COS 上传仍在入库),属处理中状态,稍候即可" };
|
|
434
524
|
async plan(ctx) {
|
|
435
525
|
return {
|
|
436
526
|
channel: this.id,
|
|
@@ -487,7 +577,7 @@ var PgyerChannel = class {
|
|
|
487
577
|
form.append("x-cos-security-token", params["x-cos-security-token"]);
|
|
488
578
|
form.append("key", params.key);
|
|
489
579
|
form.append("x-cos-meta-file-name", path.basename(ctx.artifactPath));
|
|
490
|
-
form.append("file",
|
|
580
|
+
form.append("file", await fileBlob(ctx.artifactPath), path.basename(ctx.artifactPath));
|
|
491
581
|
const upRes = await fetch(endpoint, {
|
|
492
582
|
method: "POST",
|
|
493
583
|
body: form,
|
|
@@ -500,12 +590,29 @@ var PgyerChannel = class {
|
|
|
500
590
|
message: "等待蒲公英处理"
|
|
501
591
|
});
|
|
502
592
|
const deadline = Date.now() + 9e5;
|
|
593
|
+
let consecutiveErrors = 0;
|
|
503
594
|
while (Date.now() < deadline) {
|
|
504
595
|
await sleep(3e3);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
596
|
+
let info;
|
|
597
|
+
try {
|
|
598
|
+
info = await pgyerPostFailover(ctx, "/apiv2/app/buildInfo", {
|
|
599
|
+
_api_key: apiKey,
|
|
600
|
+
buildKey
|
|
601
|
+
});
|
|
602
|
+
consecutiveErrors = 0;
|
|
603
|
+
} catch (e) {
|
|
604
|
+
consecutiveErrors++;
|
|
605
|
+
const lastErr = e instanceof Error ? e.message : String(e);
|
|
606
|
+
ctx.emit({
|
|
607
|
+
type: "warn",
|
|
608
|
+
message: `buildInfo 状态查询失败(连续 ${consecutiveErrors}/10): ${lastErr}`
|
|
609
|
+
});
|
|
610
|
+
if (consecutiveErrors >= 10) return {
|
|
611
|
+
ok: false,
|
|
612
|
+
error: `蒲公英状态查询持续失败(包已上传,可到控制台确认): ${lastErr}`
|
|
613
|
+
};
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
509
616
|
if (info.code === 0 && info.data?.buildShortcutUrl) {
|
|
510
617
|
ctx.emit({
|
|
511
618
|
type: "phase",
|
|
@@ -556,9 +663,8 @@ function platformOf(artifactPath) {
|
|
|
556
663
|
}
|
|
557
664
|
//#endregion
|
|
558
665
|
//#region src/channels/shared/agc-token.ts
|
|
559
|
-
/** 华为 AGC token(安卓/鸿蒙同源)——自 v1 lib/agc.mjs 迁移 */
|
|
560
666
|
async function getAgcToken({ clientId, clientSecret }) {
|
|
561
|
-
const
|
|
667
|
+
const res = await fetch("https://connect-api.cloud.huawei.com/api/oauth2/v1/token", {
|
|
562
668
|
method: "POST",
|
|
563
669
|
headers: { "Content-Type": "application/json" },
|
|
564
670
|
body: JSON.stringify({
|
|
@@ -567,7 +673,14 @@ async function getAgcToken({ clientId, clientSecret }) {
|
|
|
567
673
|
client_secret: clientSecret
|
|
568
674
|
}),
|
|
569
675
|
signal: AbortSignal.timeout(3e4)
|
|
570
|
-
})
|
|
676
|
+
});
|
|
677
|
+
const text = await res.text();
|
|
678
|
+
let json;
|
|
679
|
+
try {
|
|
680
|
+
json = JSON.parse(text);
|
|
681
|
+
} catch {
|
|
682
|
+
throw new Error(`AGC token 接口返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
683
|
+
}
|
|
571
684
|
if (!json.access_token) {
|
|
572
685
|
const desc = json.error_description ?? json.ret?.msg ?? json.error ?? JSON.stringify(json).slice(0, 200);
|
|
573
686
|
throw new Error(`AGC 获取 token 失败: ${desc}(检查 client_id/client_secret)`);
|
|
@@ -752,6 +865,7 @@ var HuaweiAndroidChannel = class {
|
|
|
752
865
|
if (remarkLen < 10 || remarkLen > 300) throw new Error(`华为提审备注(remark)长度需在 10-300 字之间,当前 ${remarkLen} 字`);
|
|
753
866
|
}
|
|
754
867
|
if (ctx.updateDescription !== void 0 && [...ctx.updateDescription].length > 500) throw new Error(`华为更新说明(newFeatures)不能超过 500 字符,当前 ${[...ctx.updateDescription].length} 字符`);
|
|
868
|
+
if (ctx.releaseTime && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(ctx.releaseTime)) throw new Error(`华为 releaseTime 格式异常(需 RFC3339 带时区,如 2026-06-20T10:00:00+08:00): ${ctx.releaseTime}`);
|
|
755
869
|
const { headers } = await this.authHeaders(ctx);
|
|
756
870
|
if (ctx.updateDescription) try {
|
|
757
871
|
await hwJson(`${BASE$1}/publish/v2/app-language-info?appId=${encodeURIComponent(appId)}`, {
|
|
@@ -970,6 +1084,11 @@ var XiaomiChannel = class {
|
|
|
970
1084
|
scheduled: false,
|
|
971
1085
|
status: true
|
|
972
1086
|
};
|
|
1087
|
+
/** 平台错误码 → 人话(result 码:依据为本仓测试已固化的真实失败路径文案) */
|
|
1088
|
+
errorHints = {
|
|
1089
|
+
"401": "签名无效或账号无该包名权限(检查 MI_CLIENT_SECRET 与 /dev/query 权限)",
|
|
1090
|
+
"1005": "push SIG 校验失败(检查证书公钥与密码是否匹配)"
|
|
1091
|
+
};
|
|
973
1092
|
async plan(_ctx) {
|
|
974
1093
|
return {
|
|
975
1094
|
channel: this.id,
|
|
@@ -1051,8 +1170,8 @@ var XiaomiChannel = class {
|
|
|
1051
1170
|
const form = new FormData();
|
|
1052
1171
|
form.append("RequestData", requestJson);
|
|
1053
1172
|
form.append("SIG", sig);
|
|
1054
|
-
form.append("apk",
|
|
1055
|
-
form.append("icon",
|
|
1173
|
+
form.append("apk", await fileBlob(ctx.artifactPath), fileName);
|
|
1174
|
+
form.append("icon", await fileBlob(iconPath), path.basename(iconPath));
|
|
1056
1175
|
const res = await fetch(`${MI_API_BASE}/dev/push`, {
|
|
1057
1176
|
method: "POST",
|
|
1058
1177
|
body: form,
|
|
@@ -1375,7 +1494,7 @@ var OppoChannel = class {
|
|
|
1375
1494
|
const form = new FormData();
|
|
1376
1495
|
form.append("type", "apk");
|
|
1377
1496
|
form.append("sign", sign);
|
|
1378
|
-
form.append("file",
|
|
1497
|
+
form.append("file", await fileBlob(ctx.artifactPath), fileName);
|
|
1379
1498
|
const upRes = await fetch(uploadUrl, {
|
|
1380
1499
|
method: "POST",
|
|
1381
1500
|
body: form,
|
|
@@ -1444,7 +1563,7 @@ var OppoChannel = class {
|
|
|
1444
1563
|
ctx.emit({
|
|
1445
1564
|
type: "phase",
|
|
1446
1565
|
phase: "done",
|
|
1447
|
-
message:
|
|
1566
|
+
message: `已提交(versionCode=${versionCode})`
|
|
1448
1567
|
});
|
|
1449
1568
|
return {
|
|
1450
1569
|
ok: true,
|
|
@@ -1670,7 +1789,7 @@ var VivoChannel = class {
|
|
|
1670
1789
|
params.sign = hmacSign(accessSecret, params);
|
|
1671
1790
|
const form = new FormData();
|
|
1672
1791
|
for (const [k, v] of Object.entries(params)) form.append(k, v);
|
|
1673
|
-
form.append("file",
|
|
1792
|
+
form.append("file", await fileBlob(ctx.artifactPath), fileName);
|
|
1674
1793
|
const upRes = await fetch(API_BASE$2, {
|
|
1675
1794
|
method: "POST",
|
|
1676
1795
|
body: form,
|
|
@@ -1925,7 +2044,7 @@ var HonorChannel = class {
|
|
|
1925
2044
|
message: `上传 ${fileName}(${formatSize(fileSize)})`
|
|
1926
2045
|
});
|
|
1927
2046
|
const form = new FormData();
|
|
1928
|
-
form.append("file",
|
|
2047
|
+
form.append("file", await fileBlob(ctx.artifactPath), fileName);
|
|
1929
2048
|
const upRes = await fetch(uploadUrl, {
|
|
1930
2049
|
method: "POST",
|
|
1931
2050
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -2117,6 +2236,8 @@ var YybChannel = class {
|
|
|
2117
2236
|
scheduled: true,
|
|
2118
2237
|
status: true
|
|
2119
2238
|
};
|
|
2239
|
+
/** 平台错误码 → 人话(ret 码:依据为本仓测试已固化的真实失败路径) */
|
|
2240
|
+
errorHints = { "10010": "应用不存在(检查 YYB_APP_ID 与 user_id 是否匹配该应用)" };
|
|
2120
2241
|
/** stage 产物暂存(serial_number + md5),submit 按 appId 取回 */
|
|
2121
2242
|
staged = /* @__PURE__ */ new Map();
|
|
2122
2243
|
required(ctx, key, where) {
|
|
@@ -2205,8 +2326,17 @@ var YybChannel = class {
|
|
|
2205
2326
|
}
|
|
2206
2327
|
async submit(ctx) {
|
|
2207
2328
|
const { userId, accessSecret, appId, pkgName } = this.creds(ctx);
|
|
2208
|
-
|
|
2209
|
-
if (!stagedInfo)
|
|
2329
|
+
let stagedInfo = this.staged.get(appId);
|
|
2330
|
+
if (!stagedInfo) {
|
|
2331
|
+
ctx.emit({
|
|
2332
|
+
type: "warn",
|
|
2333
|
+
message: "无本进程 stage 记录(--skip-stage 跨进程),重新上传当前包后提审"
|
|
2334
|
+
});
|
|
2335
|
+
const r = await this.stage(ctx);
|
|
2336
|
+
if (!r.ok) throw new Error(`应用宝重新上传失败: ${r.error ?? "未知错误"}`);
|
|
2337
|
+
stagedInfo = this.staged.get(appId);
|
|
2338
|
+
if (!stagedInfo) throw new Error("应用宝重新上传后仍缺少流水号(内部状态异常)");
|
|
2339
|
+
}
|
|
2210
2340
|
ctx.emit({
|
|
2211
2341
|
type: "phase",
|
|
2212
2342
|
phase: "submit"
|
|
@@ -2232,14 +2362,14 @@ var YybChannel = class {
|
|
|
2232
2362
|
businessParams["deploy_type"] = "2";
|
|
2233
2363
|
businessParams["deploy_time"] = String(deployTime);
|
|
2234
2364
|
} else businessParams["deploy_type"] = "1";
|
|
2235
|
-
|
|
2365
|
+
await yybApi("/update_app", {
|
|
2236
2366
|
userId,
|
|
2237
2367
|
accessSecret
|
|
2238
2368
|
}, businessParams);
|
|
2239
2369
|
ctx.emit({
|
|
2240
2370
|
type: "phase",
|
|
2241
2371
|
phase: "done",
|
|
2242
|
-
message:
|
|
2372
|
+
message: `已提交审核(流水号 ${stagedInfo.serialNumber})`
|
|
2243
2373
|
});
|
|
2244
2374
|
return {
|
|
2245
2375
|
ok: true,
|
|
@@ -2319,7 +2449,10 @@ const COMPILE_POLL_MS = 15e3;
|
|
|
2319
2449
|
const APP_TEST_BASE = "https://connect-api.cloud.huawei.com/api/app-test/v1";
|
|
2320
2450
|
/** AGC JSON API 封装:判 ret.code !== 0 抛错 —— v1 agc.mjs agcJson 原样 */
|
|
2321
2451
|
async function agcJson(url, init, context) {
|
|
2322
|
-
const res = await fetch(url,
|
|
2452
|
+
const res = await fetch(url, {
|
|
2453
|
+
...init,
|
|
2454
|
+
signal: init.signal ?? AbortSignal.timeout(6e4)
|
|
2455
|
+
});
|
|
2323
2456
|
let json;
|
|
2324
2457
|
try {
|
|
2325
2458
|
json = await res.json();
|
|
@@ -2443,8 +2576,9 @@ var AgcHarmonyChannel = class {
|
|
|
2443
2576
|
ref: packageId || objectId
|
|
2444
2577
|
};
|
|
2445
2578
|
}
|
|
2446
|
-
/** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 →
|
|
2447
|
-
|
|
2579
|
+
/** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 → 提审(带重试)。
|
|
2580
|
+
* stagedRef = stage() 返回的 packageId:包已在草稿区,跳过重传直接等编译+提审(.app 常为数百 MB~GB,双倍上传不可接受) */
|
|
2581
|
+
async submit(ctx, stagedRef) {
|
|
2448
2582
|
if (ctx.target.releaseType === "6") return {
|
|
2449
2583
|
ok: false,
|
|
2450
2584
|
error: "AGC 测试发布(releaseType=6)的自动提审走 beta 配置 test['agc'].submit=true(上传绑定后自动提交测试版本审核)"
|
|
@@ -2456,7 +2590,13 @@ var AgcHarmonyChannel = class {
|
|
|
2456
2590
|
phase: "auth",
|
|
2457
2591
|
message: "token 获取成功"
|
|
2458
2592
|
});
|
|
2459
|
-
|
|
2593
|
+
let packageId = stagedRef;
|
|
2594
|
+
if (packageId) ctx.emit({
|
|
2595
|
+
type: "phase",
|
|
2596
|
+
phase: "upload",
|
|
2597
|
+
message: `复用 stage 已上传的软件包(pkgId: ${packageId},跳过重传)`
|
|
2598
|
+
});
|
|
2599
|
+
else ({packageId} = await this.uploadFormal(ctx, appId, headers));
|
|
2460
2600
|
if (packageId) await this.awaitCompile(ctx, appId, packageId, headers);
|
|
2461
2601
|
if (!ctx.updateDescription) ctx.emit({
|
|
2462
2602
|
type: "warn",
|
|
@@ -2567,7 +2707,8 @@ var AgcHarmonyChannel = class {
|
|
|
2567
2707
|
const upRes = await fetch(urlInfo.url, {
|
|
2568
2708
|
method,
|
|
2569
2709
|
headers: upHeaders,
|
|
2570
|
-
body: fs.
|
|
2710
|
+
body: Readable.toWeb(fs.createReadStream(ctx.artifactPath)),
|
|
2711
|
+
duplex: "half",
|
|
2571
2712
|
signal: AbortSignal.timeout(18e5)
|
|
2572
2713
|
});
|
|
2573
2714
|
if (![
|
|
@@ -2779,10 +2920,13 @@ var AgcHarmonyChannel = class {
|
|
|
2779
2920
|
current: String(current),
|
|
2780
2921
|
pageSize: "50"
|
|
2781
2922
|
});
|
|
2782
|
-
const res = await fetch(`${APP_TEST_BASE}/test-group/list?${qs}`, {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2923
|
+
const res = await fetch(`${APP_TEST_BASE}/test-group/list?${qs}`, {
|
|
2924
|
+
headers: {
|
|
2925
|
+
...headers,
|
|
2926
|
+
appId
|
|
2927
|
+
},
|
|
2928
|
+
signal: AbortSignal.timeout(6e4)
|
|
2929
|
+
});
|
|
2786
2930
|
if (!res.ok) throw new Error(`AGC 查询测试群组失败 HTTP ${res.status}`);
|
|
2787
2931
|
const json = await res.json();
|
|
2788
2932
|
if (Number(json.rtnCode) !== 0) throw new Error(`AGC 查询测试群组失败: rtnCode=${json.rtnCode ?? "?"}${json.rtnDesc ? ` ${json.rtnDesc}` : ""}`);
|
|
@@ -2870,7 +3014,13 @@ async function ascGet(jwt, apiPath) {
|
|
|
2870
3014
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
2871
3015
|
signal: AbortSignal.timeout(3e4)
|
|
2872
3016
|
});
|
|
2873
|
-
const
|
|
3017
|
+
const text = await res.text();
|
|
3018
|
+
let json;
|
|
3019
|
+
try {
|
|
3020
|
+
json = JSON.parse(text);
|
|
3021
|
+
} catch {
|
|
3022
|
+
throw new Error(`ASC API ${apiPath} 返回非 JSON(HTTP ${res.status}): ${text.slice(0, 300)}`);
|
|
3023
|
+
}
|
|
2874
3024
|
if (!res.ok) throw new Error(`ASC API ${apiPath} 失败 HTTP ${res.status}: ${JSON.stringify(json.errors ?? json).slice(0, 300)}`);
|
|
2875
3025
|
return json;
|
|
2876
3026
|
}
|
|
@@ -2970,9 +3120,13 @@ var AscChannel = class {
|
|
|
2970
3120
|
issuerId
|
|
2971
3121
|
], {
|
|
2972
3122
|
cwd,
|
|
2973
|
-
encoding: "utf8"
|
|
3123
|
+
encoding: "utf8",
|
|
3124
|
+
timeout: 18e5
|
|
2974
3125
|
});
|
|
2975
|
-
if (r.error)
|
|
3126
|
+
if (r.error) {
|
|
3127
|
+
if ("code" in r.error && r.error.code === "ETIMEDOUT") throw new Error("xcrun altool 超时(30min)——检查网络/钥匙串/CI 无 TTY 弹窗");
|
|
3128
|
+
throw new Error(`无法执行 xcrun: ${r.error.message}(altool 需 macOS + Xcode)`);
|
|
3129
|
+
}
|
|
2976
3130
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().slice(-2e3);
|
|
2977
3131
|
if (r.status !== 0) throw new Error(`altool 上传失败(exit ${r.status}): ${output || "无输出"}`);
|
|
2978
3132
|
if (output) ctx.emit({
|
|
@@ -3247,6 +3401,8 @@ var GooglePlayChannel = class {
|
|
|
3247
3401
|
scheduled: false,
|
|
3248
3402
|
status: true
|
|
3249
3403
|
};
|
|
3404
|
+
/** 平台错误码 → 人话(HTTP 状态:依据为本仓测试已固化的失败路径) */
|
|
3405
|
+
errorHints = { "403": "权限不足(检查 Service Account 是否已关联 Play Console 且有发布权限)" };
|
|
3250
3406
|
async plan(_ctx) {
|
|
3251
3407
|
return {
|
|
3252
3408
|
channel: this.id,
|
|
@@ -3263,12 +3419,40 @@ var GooglePlayChannel = class {
|
|
|
3263
3419
|
ref: await this.uploadAndAssign(ctx, "internal", void 0)
|
|
3264
3420
|
};
|
|
3265
3421
|
}
|
|
3266
|
-
async submit(ctx) {
|
|
3422
|
+
async submit(ctx, stagedRef) {
|
|
3423
|
+
const reusedVc = stagedRef?.split(":")[1];
|
|
3424
|
+
if (reusedVc !== void 0 && reusedVc !== "") return {
|
|
3425
|
+
ok: true,
|
|
3426
|
+
ref: await this.assignExisting(ctx, "production", Number(reusedVc))
|
|
3427
|
+
};
|
|
3267
3428
|
return {
|
|
3268
3429
|
ok: true,
|
|
3269
3430
|
ref: await this.uploadAndAssign(ctx, "production", ctx.updateDescription)
|
|
3270
3431
|
};
|
|
3271
3432
|
}
|
|
3433
|
+
/** 复用 stage 已上传的 versionCode:开新 edit(仍需 auth)→ 部署 production → commit,跳过上传 */
|
|
3434
|
+
async assignExisting(ctx, track, versionCode) {
|
|
3435
|
+
const pkgName = this.requirePkgName(ctx);
|
|
3436
|
+
const token = await getAccessToken(ctx.credentials);
|
|
3437
|
+
ctx.emit({
|
|
3438
|
+
type: "phase",
|
|
3439
|
+
phase: "auth",
|
|
3440
|
+
message: "OAuth2 token 获取成功"
|
|
3441
|
+
});
|
|
3442
|
+
const edit = await apiFetch(`${API_BASE}/${pkgName}/edits`, token, {
|
|
3443
|
+
method: "POST",
|
|
3444
|
+
body: "{}"
|
|
3445
|
+
}, "创建 edit");
|
|
3446
|
+
if (!edit.id) throw new Error(`Google Play 创建 edit 响应缺少 id: ${JSON.stringify(edit).slice(0, 300)}`);
|
|
3447
|
+
const editId = edit.id;
|
|
3448
|
+
ctx.emit({
|
|
3449
|
+
type: "phase",
|
|
3450
|
+
phase: "upload",
|
|
3451
|
+
message: `复用 stage 上传的 versionCode ${versionCode}(跳过重复上传)`
|
|
3452
|
+
});
|
|
3453
|
+
await this.deployAndCommit(ctx, pkgName, token, editId, track, versionCode, ctx.updateDescription);
|
|
3454
|
+
return `${editId}:${versionCode}`;
|
|
3455
|
+
}
|
|
3272
3456
|
/** 上传 + 部署到指定轨道 + commit,返回 `editId:versionCode` */
|
|
3273
3457
|
async uploadAndAssign(ctx, track, releaseNotes) {
|
|
3274
3458
|
const pkgName = this.requirePkgName(ctx);
|
|
@@ -3307,6 +3491,15 @@ var GooglePlayChannel = class {
|
|
|
3307
3491
|
type: "progress",
|
|
3308
3492
|
detail: `上传成功(versionCode: ${versionCode})`
|
|
3309
3493
|
});
|
|
3494
|
+
return this.deployAndCommit(ctx, pkgName, token, editId, track, versionCode, releaseNotes);
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* 部署 versionCode 到指定轨道并 commit:
|
|
3498
|
+
* 先 GET 轨道现有 releases 再合并 PUT——目标 release(同 status=completed 配置)追加新 versionCode,
|
|
3499
|
+
* 其余 releases 原样保留(status/userFraction 不变),避免整轨覆盖把旧版本从轨道剔除;
|
|
3500
|
+
* :commit 冒号语法(对拍 apkgo googleplay.go 权威;对 production 含提交审核语义,internal 即生效)
|
|
3501
|
+
*/
|
|
3502
|
+
async deployAndCommit(ctx, pkgName, token, editId, track, versionCode, releaseNotes) {
|
|
3310
3503
|
const releases = [...(await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, { method: "GET" }, `读取 ${track} 轨道现有 releases`)).releases ?? []];
|
|
3311
3504
|
const release = {
|
|
3312
3505
|
versionCodes: [versionCode],
|
|
@@ -3374,6 +3567,11 @@ var GooglePlayChannel = class {
|
|
|
3374
3567
|
};
|
|
3375
3568
|
//#endregion
|
|
3376
3569
|
//#region src/pipeline.ts
|
|
3570
|
+
/**
|
|
3571
|
+
* 发布编排引擎:构建计划 → 确认协议 → 并发执行 → 汇总。
|
|
3572
|
+
* headless:确认通过注入的 confirm 回调完成(CLI 用 clack/弹窗),core 不碰终端。
|
|
3573
|
+
* 插件:可选 PluginHost,在 plan/stage/submit 生命周期触发钩子(可拦截可修改)。
|
|
3574
|
+
*/
|
|
3377
3575
|
async function runLimited(items, limit, fn) {
|
|
3378
3576
|
const queue = [...items];
|
|
3379
3577
|
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
@@ -3381,6 +3579,14 @@ async function runLimited(items, limit, fn) {
|
|
|
3381
3579
|
});
|
|
3382
3580
|
await Promise.all(workers);
|
|
3383
3581
|
}
|
|
3582
|
+
/** 错误消息(带一层 cause 解包:fetch 网络层的真实原因如 NotReadableError 藏在 cause 里) */
|
|
3583
|
+
function errText(e) {
|
|
3584
|
+
if (e instanceof Error) {
|
|
3585
|
+
const cause = e.cause instanceof Error ? `(${e.cause.message})` : "";
|
|
3586
|
+
return `${e.message}${cause}`;
|
|
3587
|
+
}
|
|
3588
|
+
return String(e);
|
|
3589
|
+
}
|
|
3384
3590
|
/** stage 阶段:并发传包到各渠道草稿(不提审);'stage:before' 返回 false 跳过该渠道 */
|
|
3385
3591
|
async function runStage(input) {
|
|
3386
3592
|
const plugins = input.plugins;
|
|
@@ -3415,7 +3621,8 @@ async function runStage(input) {
|
|
|
3415
3621
|
channel: channel.id,
|
|
3416
3622
|
ok: r.ok,
|
|
3417
3623
|
ref: r.ref,
|
|
3418
|
-
error: r.error
|
|
3624
|
+
error: r.error,
|
|
3625
|
+
artifactPath: effectiveCtx.artifactPath
|
|
3419
3626
|
});
|
|
3420
3627
|
const plan = await safePlan(channel, effectiveCtx);
|
|
3421
3628
|
summary.push({
|
|
@@ -3438,7 +3645,7 @@ async function runStage(input) {
|
|
|
3438
3645
|
channel: channel.id,
|
|
3439
3646
|
label: channel.label,
|
|
3440
3647
|
ok: false,
|
|
3441
|
-
stageError: e
|
|
3648
|
+
stageError: errText(e),
|
|
3442
3649
|
plan
|
|
3443
3650
|
});
|
|
3444
3651
|
input.historySink?.({
|
|
@@ -3497,12 +3704,13 @@ async function runSubmit(input, options) {
|
|
|
3497
3704
|
label: channel.label,
|
|
3498
3705
|
ok: false,
|
|
3499
3706
|
submitError: "被插件跳过",
|
|
3500
|
-
plan: planFor(plans, basePlans, channel.id)
|
|
3707
|
+
plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
|
|
3501
3708
|
});
|
|
3502
3709
|
return;
|
|
3503
3710
|
}
|
|
3504
3711
|
effectiveCtx = r.value;
|
|
3505
3712
|
}
|
|
3713
|
+
let stagedRef;
|
|
3506
3714
|
if (!options.skipStage && channel.capabilities.stage) {
|
|
3507
3715
|
const stageResult = await channel.stage(effectiveCtx);
|
|
3508
3716
|
if (!stageResult.ok) {
|
|
@@ -3511,7 +3719,7 @@ async function runSubmit(input, options) {
|
|
|
3511
3719
|
label: channel.label,
|
|
3512
3720
|
ok: false,
|
|
3513
3721
|
stageError: stageResult.error,
|
|
3514
|
-
plan: planFor(plans, basePlans, channel.id)
|
|
3722
|
+
plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
|
|
3515
3723
|
});
|
|
3516
3724
|
input.historySink?.({
|
|
3517
3725
|
channel: channel.id,
|
|
@@ -3521,6 +3729,7 @@ async function runSubmit(input, options) {
|
|
|
3521
3729
|
});
|
|
3522
3730
|
return;
|
|
3523
3731
|
}
|
|
3732
|
+
stagedRef = stageResult.ref;
|
|
3524
3733
|
input.historySink?.({
|
|
3525
3734
|
channel: channel.id,
|
|
3526
3735
|
action: "stage",
|
|
@@ -3528,14 +3737,23 @@ async function runSubmit(input, options) {
|
|
|
3528
3737
|
ref: stageResult.ref,
|
|
3529
3738
|
artifactVersion: plans.find((p) => p.channel === channel.id)?.artifactVersion || void 0
|
|
3530
3739
|
});
|
|
3740
|
+
if (plugins) await plugins.broadcast("stage:after", {
|
|
3741
|
+
channel: channel.id,
|
|
3742
|
+
cwd: input.cwd
|
|
3743
|
+
}, {
|
|
3744
|
+
channel: channel.id,
|
|
3745
|
+
ok: true,
|
|
3746
|
+
ref: stageResult.ref,
|
|
3747
|
+
artifactPath: effectiveCtx.artifactPath
|
|
3748
|
+
});
|
|
3531
3749
|
}
|
|
3532
|
-
const r = await channel.submit(effectiveCtx);
|
|
3750
|
+
const r = await channel.submit(effectiveCtx, stagedRef);
|
|
3533
3751
|
summary.push({
|
|
3534
3752
|
channel: channel.id,
|
|
3535
3753
|
label: channel.label,
|
|
3536
3754
|
ok: r.ok,
|
|
3537
3755
|
submitError: r.error,
|
|
3538
|
-
plan: planFor(plans, basePlans, channel.id)
|
|
3756
|
+
plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
|
|
3539
3757
|
});
|
|
3540
3758
|
input.historySink?.({
|
|
3541
3759
|
channel: channel.id,
|
|
@@ -3549,8 +3767,8 @@ async function runSubmit(input, options) {
|
|
|
3549
3767
|
channel: channel.id,
|
|
3550
3768
|
label: channel.label,
|
|
3551
3769
|
ok: false,
|
|
3552
|
-
submitError: e
|
|
3553
|
-
plan: planFor(plans, basePlans, channel.id)
|
|
3770
|
+
submitError: errText(e),
|
|
3771
|
+
plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
|
|
3554
3772
|
});
|
|
3555
3773
|
input.historySink?.({
|
|
3556
3774
|
channel: channel.id,
|
|
@@ -3573,22 +3791,189 @@ async function safePlan(channel, ctx) {
|
|
|
3573
3791
|
try {
|
|
3574
3792
|
return await channel.plan(ctx);
|
|
3575
3793
|
} catch {
|
|
3576
|
-
return fallbackPlan(channel.id);
|
|
3794
|
+
return fallbackPlan(channel.id, ctx.artifactPath);
|
|
3577
3795
|
}
|
|
3578
3796
|
}
|
|
3579
|
-
/**
|
|
3580
|
-
function
|
|
3797
|
+
/** 按产物扩展名推断平台(兜底条目用):.ipa→ios,.hap→harmony,.apk/.aab 及其余→android */
|
|
3798
|
+
function platformOfArtifact(artifactPath) {
|
|
3799
|
+
const ext = path.extname(artifactPath).toLowerCase();
|
|
3800
|
+
if (ext === ".ipa") return "ios";
|
|
3801
|
+
if (ext === ".hap") return "harmony";
|
|
3802
|
+
return "android";
|
|
3803
|
+
}
|
|
3804
|
+
/** plan 失败/被插件删除时的兜底条目(保 summary.plan 非可选契约;平台按产物推断不再硬编码 android) */
|
|
3805
|
+
function fallbackPlan(channelId, artifactPath) {
|
|
3581
3806
|
return {
|
|
3582
3807
|
channel: channelId,
|
|
3583
|
-
platform:
|
|
3808
|
+
platform: platformOfArtifact(artifactPath),
|
|
3584
3809
|
artifactVersion: "",
|
|
3585
3810
|
action: "stage+submit",
|
|
3586
3811
|
afterApproval: "auto-publish"
|
|
3587
3812
|
};
|
|
3588
3813
|
}
|
|
3589
3814
|
/** 汇总条目取计划:plan:before 改写后的 plans 优先,被插件删除时回退改写前快照,永不返回 undefined */
|
|
3590
|
-
function planFor(plans, base, id) {
|
|
3591
|
-
return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id);
|
|
3815
|
+
function planFor(plans, base, id, artifactPath) {
|
|
3816
|
+
return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id, artifactPath);
|
|
3817
|
+
}
|
|
3818
|
+
//#endregion
|
|
3819
|
+
//#region src/version/git.ts
|
|
3820
|
+
/**
|
|
3821
|
+
* git 只读/写小助手(bump 编排与推断共用)。
|
|
3822
|
+
* spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
|
|
3823
|
+
*/
|
|
3824
|
+
function git(cwd, args) {
|
|
3825
|
+
const r = spawnSync("git", args, {
|
|
3826
|
+
cwd,
|
|
3827
|
+
encoding: "utf8",
|
|
3828
|
+
timeout: 6e4
|
|
3829
|
+
});
|
|
3830
|
+
return {
|
|
3831
|
+
ok: !r.error && r.status === 0,
|
|
3832
|
+
stdout: (r.stdout ?? "").trim(),
|
|
3833
|
+
stderr: (r.stderr ?? "").trim()
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
function isGitRepo(cwd) {
|
|
3837
|
+
return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
|
|
3838
|
+
}
|
|
3839
|
+
/** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
|
|
3840
|
+
function looseSemver(tag) {
|
|
3841
|
+
const direct = semver.valid(tag);
|
|
3842
|
+
if (direct) return direct;
|
|
3843
|
+
const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
|
|
3844
|
+
return m ? semver.valid(m[1]) ?? void 0 : void 0;
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
|
|
3848
|
+
* 无 tag / 非 git 仓库返回 undefined。
|
|
3849
|
+
*/
|
|
3850
|
+
function latestSemverTag(cwd) {
|
|
3851
|
+
const r = git(cwd, ["tag", "-l"]);
|
|
3852
|
+
if (!r.ok) return void 0;
|
|
3853
|
+
let best;
|
|
3854
|
+
for (const raw of r.stdout.split("\n")) {
|
|
3855
|
+
const t = raw.trim();
|
|
3856
|
+
if (!t) continue;
|
|
3857
|
+
const v = looseSemver(t);
|
|
3858
|
+
if (!v) continue;
|
|
3859
|
+
if (!best || semver.gt(v, best.v)) best = {
|
|
3860
|
+
raw: t,
|
|
3861
|
+
v
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
return best?.raw;
|
|
3865
|
+
}
|
|
3866
|
+
/** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
|
|
3867
|
+
function commitsSince(cwd, ref) {
|
|
3868
|
+
const r = git(cwd, [
|
|
3869
|
+
"log",
|
|
3870
|
+
ref ? `${ref}..HEAD` : "HEAD",
|
|
3871
|
+
"--format=%B%x00",
|
|
3872
|
+
"--no-merges"
|
|
3873
|
+
]);
|
|
3874
|
+
if (!r.ok) return [];
|
|
3875
|
+
return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
|
|
3876
|
+
}
|
|
3877
|
+
/** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
|
|
3878
|
+
* 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
|
|
3879
|
+
function statusShort(cwd) {
|
|
3880
|
+
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
3881
|
+
cwd,
|
|
3882
|
+
encoding: "utf8",
|
|
3883
|
+
timeout: 3e4
|
|
3884
|
+
});
|
|
3885
|
+
if (r.error || r.status !== 0) return [];
|
|
3886
|
+
return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
3887
|
+
}
|
|
3888
|
+
/**
|
|
3889
|
+
* release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
|
|
3890
|
+
* - tag 已存在(多端发布,前一端已打)→ 跳过
|
|
3891
|
+
* - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
|
|
3892
|
+
*/
|
|
3893
|
+
function tagReleaseVersion(cwd, version, opts = {}) {
|
|
3894
|
+
if (!version) return {
|
|
3895
|
+
tagged: false,
|
|
3896
|
+
reason: "无版本"
|
|
3897
|
+
};
|
|
3898
|
+
if (opts.dryRun) return {
|
|
3899
|
+
tagged: false,
|
|
3900
|
+
reason: "dry-run"
|
|
3901
|
+
};
|
|
3902
|
+
const tag = `v${version}`;
|
|
3903
|
+
if (spawnSync("git", [
|
|
3904
|
+
"rev-parse",
|
|
3905
|
+
"-q",
|
|
3906
|
+
"--verify",
|
|
3907
|
+
`refs/tags/${tag}`
|
|
3908
|
+
], {
|
|
3909
|
+
cwd,
|
|
3910
|
+
encoding: "utf8"
|
|
3911
|
+
}).status === 0) return {
|
|
3912
|
+
tagged: false,
|
|
3913
|
+
reason: `tag ${tag} 已存在(前一端已发布)`
|
|
3914
|
+
};
|
|
3915
|
+
const r = spawnSync("git", ["tag", tag], {
|
|
3916
|
+
cwd,
|
|
3917
|
+
encoding: "utf8"
|
|
3918
|
+
});
|
|
3919
|
+
if (r.status !== 0) return {
|
|
3920
|
+
tagged: false,
|
|
3921
|
+
reason: `tag ${tag} 创建失败:${r.stderr.trim()}`
|
|
3922
|
+
};
|
|
3923
|
+
return { tagged: true };
|
|
3924
|
+
}
|
|
3925
|
+
//#endregion
|
|
3926
|
+
//#region src/update.ts
|
|
3927
|
+
/**
|
|
3928
|
+
* 热更能力契约:applift 的第三维扩展(framework × channel 之外的 update 维度)。
|
|
3929
|
+
*
|
|
3930
|
+
* provider 与 framework 绑定(如 react-native-update 只服务 RN 工程),由插件经
|
|
3931
|
+
* 'update:register' 注册;beta/release 管线的投递路由(热更 vs 原生包)消费 judge 结果,
|
|
3932
|
+
* 只做推荐与证据展示,最终投递方式由用户在确认界面拍板(CI 非交互时 uncertain 即中止)。
|
|
3933
|
+
*/
|
|
3934
|
+
/**
|
|
3935
|
+
* 投递路由:按当前 framework 匹配 provider 并执行判定。
|
|
3936
|
+
* - 无 provider / 无匹配 → null(走原生包路径,原始行为不受影响)
|
|
3937
|
+
* - 多个 provider 匹配同一 framework → 配置错误(一个框架一个热更实现)
|
|
3938
|
+
* 结论是推荐值:最终投递方式由用户在确认界面拍板,CI 非交互时 uncertain 中止
|
|
3939
|
+
*/
|
|
3940
|
+
async function routeDelivery(providers, ctx, diff, platform) {
|
|
3941
|
+
const matched = providers.filter((p) => p.frameworks.includes(ctx.framework.id) && (!platform || !p.platforms || p.platforms.includes(platform)));
|
|
3942
|
+
if (matched.length === 0) return null;
|
|
3943
|
+
if (matched.length > 1) throw new Error(`多个热更 provider 匹配 framework「${ctx.framework.id}」:${matched.map((p) => p.id).join(", ")}——一个框架只应有一个热更实现`);
|
|
3944
|
+
const provider = matched[0];
|
|
3945
|
+
return {
|
|
3946
|
+
provider,
|
|
3947
|
+
judgement: await provider.judge(diff, ctx)
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
|
|
3952
|
+
* 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
|
|
3953
|
+
* 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
|
|
3954
|
+
*/
|
|
3955
|
+
async function collectReleaseDiff(cwd) {
|
|
3956
|
+
const anchor = latestSemverTag(cwd);
|
|
3957
|
+
if (!anchor) return null;
|
|
3958
|
+
const r = spawnSync("git", [
|
|
3959
|
+
"diff",
|
|
3960
|
+
"--name-only",
|
|
3961
|
+
anchor
|
|
3962
|
+
], {
|
|
3963
|
+
cwd,
|
|
3964
|
+
encoding: "utf8"
|
|
3965
|
+
});
|
|
3966
|
+
const untracked = spawnSync("git", [
|
|
3967
|
+
"ls-files",
|
|
3968
|
+
"--others",
|
|
3969
|
+
"--exclude-standard"
|
|
3970
|
+
], {
|
|
3971
|
+
cwd,
|
|
3972
|
+
encoding: "utf8"
|
|
3973
|
+
});
|
|
3974
|
+
if (r.status !== 0 || untracked.status !== 0) return null;
|
|
3975
|
+
const files = [...r.stdout.split("\n"), ...untracked.stdout.split("\n")].map((l) => l.trim());
|
|
3976
|
+
return [...new Set(files)].filter(Boolean);
|
|
3592
3977
|
}
|
|
3593
3978
|
//#endregion
|
|
3594
3979
|
//#region src/plugin.ts
|
|
@@ -3599,6 +3984,8 @@ var PluginHost = class {
|
|
|
3599
3984
|
}
|
|
3600
3985
|
/**
|
|
3601
3986
|
* 顺序执行某钩子;支持「链式替换」(返回对象则替换后继续传给下一插件)与「拦截」(返回 false 立即中止)。
|
|
3987
|
+
* 信任边界:返回值按 T 强转、不做形状校验(vite 同款取舍)——插件作者自负其责,
|
|
3988
|
+
* 坏形状会在后续消费点显式抛错,宿主不代为静默兜底。
|
|
3602
3989
|
* @returns {[被拦截?, 最终值]} false 表示被某插件拦截
|
|
3603
3990
|
*/
|
|
3604
3991
|
async apply(hook, value, ctx) {
|
|
@@ -3635,403 +4022,6 @@ function resolvePlugins(userConfig) {
|
|
|
3635
4022
|
return (userConfig.plugins ?? []).map((p) => typeof p === "function" ? p() : p);
|
|
3636
4023
|
}
|
|
3637
4024
|
//#endregion
|
|
3638
|
-
//#region src/framework.ts
|
|
3639
|
-
/**
|
|
3640
|
-
* 框架适配器:让 applift 框架无关(RN / Flutter / 原生安卓 / 原生 iOS / 原生鸿蒙)。
|
|
3641
|
-
* 探测规则 + 产物 pattern + 缺省构建命令;插件可用 'channel:register' 旁路注册自定义 FrameworkAdapter。
|
|
3642
|
-
*/
|
|
3643
|
-
/** 一层子目录列表(探测用,跳过隐藏目录与 node_modules/Pods) */
|
|
3644
|
-
function subdirs(cwd, depth) {
|
|
3645
|
-
if (depth <= 0) return [];
|
|
3646
|
-
const out = [];
|
|
3647
|
-
let names;
|
|
3648
|
-
try {
|
|
3649
|
-
names = fs.readdirSync(cwd, { withFileTypes: true });
|
|
3650
|
-
} catch {
|
|
3651
|
-
return [];
|
|
3652
|
-
}
|
|
3653
|
-
for (const name of names) {
|
|
3654
|
-
if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules" || name.name === "Pods") continue;
|
|
3655
|
-
const p = path.join(cwd, name.name);
|
|
3656
|
-
out.push(p, ...subdirs(p, depth - 1));
|
|
3657
|
-
}
|
|
3658
|
-
return out;
|
|
3659
|
-
}
|
|
3660
|
-
/** package.json 读取(容错) */
|
|
3661
|
-
function readJson(filePath) {
|
|
3662
|
-
try {
|
|
3663
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
3664
|
-
} catch {
|
|
3665
|
-
return null;
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
function deps(pkg) {
|
|
3669
|
-
if (!pkg) return {};
|
|
3670
|
-
return {
|
|
3671
|
-
...pkg.dependencies,
|
|
3672
|
-
...pkg.devDependencies
|
|
3673
|
-
};
|
|
3674
|
-
}
|
|
3675
|
-
const rnAdapter = {
|
|
3676
|
-
id: "rn",
|
|
3677
|
-
label: "React Native",
|
|
3678
|
-
detect(cwd) {
|
|
3679
|
-
return "react-native" in deps(readJson(path.join(cwd, "package.json")));
|
|
3680
|
-
},
|
|
3681
|
-
/** Expo 项目(依赖 expo 包):同走 RN 产物路径(prebuild 生成原生工程),label 区分展示 */
|
|
3682
|
-
expoAwareLabel(cwd) {
|
|
3683
|
-
return "expo" in deps(readJson(path.join(cwd, "package.json"))) ? "Expo (React Native)" : "React Native";
|
|
3684
|
-
},
|
|
3685
|
-
artifactPatterns(platform) {
|
|
3686
|
-
if (platform === "android") return ["android/app/build/outputs/apk/release/*-release.apk"];
|
|
3687
|
-
if (platform === "ios") return ["build/ios/ipa/*.ipa"];
|
|
3688
|
-
return [
|
|
3689
|
-
"harmony/entry/build/default/outputs/**/entry-*-signed.hap",
|
|
3690
|
-
"harmony/entry/build/default/outputs/**/entry-*.app",
|
|
3691
|
-
"harmony/build/outputs/**/*.app",
|
|
3692
|
-
"build/outputs/**/*.app"
|
|
3693
|
-
];
|
|
3694
|
-
},
|
|
3695
|
-
buildCommand(platform) {
|
|
3696
|
-
if (platform === "android") return [
|
|
3697
|
-
"npm",
|
|
3698
|
-
"run",
|
|
3699
|
-
"build:test",
|
|
3700
|
-
"--",
|
|
3701
|
-
"--platform",
|
|
3702
|
-
"android"
|
|
3703
|
-
];
|
|
3704
|
-
if (platform === "ios") return null;
|
|
3705
|
-
return [
|
|
3706
|
-
"npm",
|
|
3707
|
-
"run",
|
|
3708
|
-
"build:test",
|
|
3709
|
-
"--",
|
|
3710
|
-
"--platform",
|
|
3711
|
-
"harmony"
|
|
3712
|
-
];
|
|
3713
|
-
}
|
|
3714
|
-
};
|
|
3715
|
-
const flutterAdapter = {
|
|
3716
|
-
id: "flutter",
|
|
3717
|
-
label: "Flutter",
|
|
3718
|
-
detect(cwd) {
|
|
3719
|
-
if (readJson(path.join(cwd, "pubspec.yaml"))) return true;
|
|
3720
|
-
try {
|
|
3721
|
-
const text = fs.readFileSync(path.join(cwd, "pubspec.yaml"), "utf8");
|
|
3722
|
-
return /^sdk:\s*flutter$/m.test(text) || /^name:/m.test(text);
|
|
3723
|
-
} catch {
|
|
3724
|
-
return false;
|
|
3725
|
-
}
|
|
3726
|
-
},
|
|
3727
|
-
artifactPatterns(platform) {
|
|
3728
|
-
if (platform === "android") return ["build/app/outputs/flutter-apk/app-release.apk"];
|
|
3729
|
-
if (platform === "ios") return ["build/ios/ipa/*.ipa"];
|
|
3730
|
-
return [
|
|
3731
|
-
"build/**/outputs/**/*-signed.hap",
|
|
3732
|
-
"harmony/entry/build/**/outputs/**/*-signed.hap",
|
|
3733
|
-
"ohos/entry/build/**/outputs/**/*-signed.hap",
|
|
3734
|
-
"build/**/outputs/**/*.app"
|
|
3735
|
-
];
|
|
3736
|
-
},
|
|
3737
|
-
buildCommand(platform) {
|
|
3738
|
-
if (platform === "android") return [
|
|
3739
|
-
"flutter",
|
|
3740
|
-
"build",
|
|
3741
|
-
"apk",
|
|
3742
|
-
"--release"
|
|
3743
|
-
];
|
|
3744
|
-
if (platform === "ios") return [
|
|
3745
|
-
"flutter",
|
|
3746
|
-
"build",
|
|
3747
|
-
"ipa",
|
|
3748
|
-
"--release"
|
|
3749
|
-
];
|
|
3750
|
-
return [
|
|
3751
|
-
"flutter",
|
|
3752
|
-
"build",
|
|
3753
|
-
"hap",
|
|
3754
|
-
"--release"
|
|
3755
|
-
];
|
|
3756
|
-
}
|
|
3757
|
-
};
|
|
3758
|
-
const nativeAndroidAdapter = {
|
|
3759
|
-
id: "native-android",
|
|
3760
|
-
label: "原生 Android",
|
|
3761
|
-
detect(cwd) {
|
|
3762
|
-
if (rnAdapter.detect(cwd)) return false;
|
|
3763
|
-
return fs.existsSync(path.join(cwd, "settings.gradle")) || fs.existsSync(path.join(cwd, "settings.gradle.kts"));
|
|
3764
|
-
},
|
|
3765
|
-
artifactPatterns() {
|
|
3766
|
-
return ["app/build/outputs/apk/release/*-release.apk"];
|
|
3767
|
-
},
|
|
3768
|
-
buildCommand() {
|
|
3769
|
-
return ["./gradlew", "assembleRelease"];
|
|
3770
|
-
}
|
|
3771
|
-
};
|
|
3772
|
-
const nativeIosAdapter = {
|
|
3773
|
-
id: "native-ios",
|
|
3774
|
-
label: "原生 iOS",
|
|
3775
|
-
detect(cwd) {
|
|
3776
|
-
if (rnAdapter.detect(cwd)) return false;
|
|
3777
|
-
for (const dir of [cwd, ...subdirs(cwd, 1)]) {
|
|
3778
|
-
let entries;
|
|
3779
|
-
try {
|
|
3780
|
-
entries = fs.readdirSync(dir);
|
|
3781
|
-
} catch {
|
|
3782
|
-
continue;
|
|
3783
|
-
}
|
|
3784
|
-
if (entries.some((f) => String(f).endsWith(".xcodeproj") || String(f).endsWith(".xcworkspace"))) return true;
|
|
3785
|
-
}
|
|
3786
|
-
return false;
|
|
3787
|
-
},
|
|
3788
|
-
artifactPatterns() {
|
|
3789
|
-
return ["build/**/*.ipa"];
|
|
3790
|
-
},
|
|
3791
|
-
buildCommand() {
|
|
3792
|
-
return null;
|
|
3793
|
-
}
|
|
3794
|
-
};
|
|
3795
|
-
const nativeHarmonyAdapter = {
|
|
3796
|
-
id: "native-harmony",
|
|
3797
|
-
label: "原生鸿蒙 (ArkTS)",
|
|
3798
|
-
detect(cwd) {
|
|
3799
|
-
return fs.existsSync(path.join(cwd, "oh-package.json5")) || fs.existsSync(path.join(cwd, "harmony/oh-package.json5"));
|
|
3800
|
-
},
|
|
3801
|
-
artifactPatterns() {
|
|
3802
|
-
return [
|
|
3803
|
-
"entry/build/default/outputs/**/entry-*-signed.hap",
|
|
3804
|
-
"entry/build/default/outputs/**/entry-*.app",
|
|
3805
|
-
"harmony/entry/build/default/outputs/**/*-signed.hap",
|
|
3806
|
-
"build/outputs/**/*.app",
|
|
3807
|
-
"harmony/build/outputs/**/*.app",
|
|
3808
|
-
"ohos/build/outputs/**/*.app"
|
|
3809
|
-
];
|
|
3810
|
-
},
|
|
3811
|
-
buildCommand() {
|
|
3812
|
-
return [
|
|
3813
|
-
"hvigorw",
|
|
3814
|
-
"--mode",
|
|
3815
|
-
"module",
|
|
3816
|
-
"-p",
|
|
3817
|
-
"module=entry@default",
|
|
3818
|
-
"-p",
|
|
3819
|
-
"product=default",
|
|
3820
|
-
"-p",
|
|
3821
|
-
"buildMode=release",
|
|
3822
|
-
"assembleHap",
|
|
3823
|
-
"--no-daemon"
|
|
3824
|
-
];
|
|
3825
|
-
}
|
|
3826
|
-
};
|
|
3827
|
-
const electronAdapter = {
|
|
3828
|
-
id: "electron",
|
|
3829
|
-
label: "Electron",
|
|
3830
|
-
detect(cwd) {
|
|
3831
|
-
return "electron" in deps(readJson(path.join(cwd, "package.json")));
|
|
3832
|
-
},
|
|
3833
|
-
artifactPatterns(platform) {
|
|
3834
|
-
if (platform === "windows") return [
|
|
3835
|
-
"dist/*.exe",
|
|
3836
|
-
"dist/**/*.exe",
|
|
3837
|
-
"out/make/*.exe"
|
|
3838
|
-
];
|
|
3839
|
-
if (platform === "macos") return [
|
|
3840
|
-
"dist/*.dmg",
|
|
3841
|
-
"dist/**/*.dmg",
|
|
3842
|
-
"out/make/*.dmg"
|
|
3843
|
-
];
|
|
3844
|
-
if (platform === "linux") return [
|
|
3845
|
-
"dist/*.AppImage",
|
|
3846
|
-
"dist/**/*.AppImage",
|
|
3847
|
-
"out/make/*.AppImage"
|
|
3848
|
-
];
|
|
3849
|
-
return [];
|
|
3850
|
-
},
|
|
3851
|
-
buildCommand(platform) {
|
|
3852
|
-
if (platform === "windows") return [
|
|
3853
|
-
"npx",
|
|
3854
|
-
"electron-forge",
|
|
3855
|
-
"make",
|
|
3856
|
-
"--platform",
|
|
3857
|
-
"win32"
|
|
3858
|
-
];
|
|
3859
|
-
if (platform === "macos") return [
|
|
3860
|
-
"npx",
|
|
3861
|
-
"electron-forge",
|
|
3862
|
-
"make",
|
|
3863
|
-
"--platform",
|
|
3864
|
-
"darwin"
|
|
3865
|
-
];
|
|
3866
|
-
return [
|
|
3867
|
-
"npx",
|
|
3868
|
-
"electron-forge",
|
|
3869
|
-
"make",
|
|
3870
|
-
"--platform",
|
|
3871
|
-
"linux"
|
|
3872
|
-
];
|
|
3873
|
-
}
|
|
3874
|
-
};
|
|
3875
|
-
const tauriAdapter = {
|
|
3876
|
-
id: "tauri",
|
|
3877
|
-
label: "Tauri",
|
|
3878
|
-
detect(cwd) {
|
|
3879
|
-
return "@tauri-apps/cli" in deps(readJson(path.join(cwd, "package.json"))) || fs.existsSync(path.join(cwd, "src-tauri"));
|
|
3880
|
-
},
|
|
3881
|
-
artifactPatterns(platform) {
|
|
3882
|
-
if (platform === "windows") return ["src-tauri/target/release/bundle/nsis/*.exe", "src-tauri/target/release/bundle/msi/*.msi"];
|
|
3883
|
-
if (platform === "macos") return ["src-tauri/target/release/bundle/dmg/*.dmg", "src-tauri/target/release/bundle/macos/*.app"];
|
|
3884
|
-
if (platform === "linux") return ["src-tauri/target/release/bundle/deb/*.deb", "src-tauri/target/release/bundle/appimage/*.AppImage"];
|
|
3885
|
-
return [];
|
|
3886
|
-
},
|
|
3887
|
-
buildCommand(platform) {
|
|
3888
|
-
if (platform === "macos") return [
|
|
3889
|
-
"npx",
|
|
3890
|
-
"tauri",
|
|
3891
|
-
"build"
|
|
3892
|
-
];
|
|
3893
|
-
return [
|
|
3894
|
-
"npx",
|
|
3895
|
-
"tauri",
|
|
3896
|
-
"build"
|
|
3897
|
-
];
|
|
3898
|
-
}
|
|
3899
|
-
};
|
|
3900
|
-
const BUILTIN_ADAPTERS = [
|
|
3901
|
-
rnAdapter,
|
|
3902
|
-
flutterAdapter,
|
|
3903
|
-
electronAdapter,
|
|
3904
|
-
tauriAdapter,
|
|
3905
|
-
nativeAndroidAdapter,
|
|
3906
|
-
nativeIosAdapter,
|
|
3907
|
-
nativeHarmonyAdapter
|
|
3908
|
-
];
|
|
3909
|
-
/** 探测项目框架(可注入额外适配器,插件自定义框架的扩展点) */
|
|
3910
|
-
function detectFramework(cwd, extra = []) {
|
|
3911
|
-
for (const adapter of [...extra, ...BUILTIN_ADAPTERS]) try {
|
|
3912
|
-
if (adapter.detect(cwd)) return adapter;
|
|
3913
|
-
} catch {}
|
|
3914
|
-
return null;
|
|
3915
|
-
}
|
|
3916
|
-
/** 按 pattern 扫描产物(支持 ** 与 * 简单通配,返回绝对路径,多产物按 mtime 新者在前) */
|
|
3917
|
-
function findArtifacts(cwd, patterns) {
|
|
3918
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
3919
|
-
for (const pattern of patterns) for (const p of globWalk(path.resolve(cwd), pattern.split("/"))) {
|
|
3920
|
-
if (byPath.has(p)) continue;
|
|
3921
|
-
try {
|
|
3922
|
-
byPath.set(p, fs.statSync(p).mtimeMs);
|
|
3923
|
-
} catch {}
|
|
3924
|
-
}
|
|
3925
|
-
return [...byPath].sort((a, b) => b[1] - a[1]).map(([p]) => p);
|
|
3926
|
-
}
|
|
3927
|
-
/** 文本读取(容错:不存在/不可读返回 null,绝不抛;detectBundleIds 各源共用) */
|
|
3928
|
-
function readText$1(filePath) {
|
|
3929
|
-
try {
|
|
3930
|
-
return fs.readFileSync(filePath, "utf8");
|
|
3931
|
-
} catch {
|
|
3932
|
-
return null;
|
|
3933
|
-
}
|
|
3934
|
-
}
|
|
3935
|
-
/** 从 JSON 对象按路径取非空字符串(任一层缺失/非字符串返回 undefined) */
|
|
3936
|
-
function pickString(obj, ...keys) {
|
|
3937
|
-
let cur = obj;
|
|
3938
|
-
for (const k of keys) {
|
|
3939
|
-
if (cur === null || typeof cur !== "object") return void 0;
|
|
3940
|
-
cur = cur[k];
|
|
3941
|
-
}
|
|
3942
|
-
return typeof cur === "string" && cur.trim() ? cur.trim() : void 0;
|
|
3943
|
-
}
|
|
3944
|
-
/** 原生安卓 applicationId(groovy/kts 两种赋值语法;app/ 与 android/app/ 两处布局;不中返回 undefined) */
|
|
3945
|
-
function gradleApplicationId(cwd) {
|
|
3946
|
-
const candidates = [
|
|
3947
|
-
path.join(cwd, "app", "build.gradle"),
|
|
3948
|
-
path.join(cwd, "app", "build.gradle.kts"),
|
|
3949
|
-
path.join(cwd, "android", "app", "build.gradle"),
|
|
3950
|
-
path.join(cwd, "android", "app", "build.gradle.kts")
|
|
3951
|
-
];
|
|
3952
|
-
for (const file of candidates) {
|
|
3953
|
-
const text = readText$1(file);
|
|
3954
|
-
if (!text) continue;
|
|
3955
|
-
const m = /(?:^|\s)applicationId\s*[=:]?\s*["']([^"'\s]+)["']/.exec(text);
|
|
3956
|
-
if (m?.[1]) return m[1];
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
/** 鸿蒙 AppScope/app.json5 的 bundleName(json5 允许注释/尾逗号,正则取值而非 JSON 解析) */
|
|
3960
|
-
function appJson5BundleName(file) {
|
|
3961
|
-
const text = readText$1(file);
|
|
3962
|
-
return (text && /"bundleName"\s*:\s*"([^"]+)"/.exec(text))?.[1];
|
|
3963
|
-
}
|
|
3964
|
-
/** 两层子目录内找首个 *.xcodeproj:pbxproj 第一个非变量(含 $ 的 $(...) 与 ${...})的 PRODUCT_BUNDLE_IDENTIFIER + 工程名(Pods 已被 subdirs 排除) */
|
|
3965
|
-
function findPbxproj(cwd) {
|
|
3966
|
-
for (const dir of [cwd, ...subdirs(cwd, 2)]) {
|
|
3967
|
-
let entries;
|
|
3968
|
-
try {
|
|
3969
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3970
|
-
} catch {
|
|
3971
|
-
continue;
|
|
3972
|
-
}
|
|
3973
|
-
const projects = entries.filter((e) => e.isDirectory() && e.name.endsWith(".xcodeproj")).map((e) => e.name).sort();
|
|
3974
|
-
for (const name of projects) {
|
|
3975
|
-
const pbx = readText$1(path.join(dir, name, "project.pbxproj"));
|
|
3976
|
-
if (!pbx) continue;
|
|
3977
|
-
for (const m of pbx.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
|
|
3978
|
-
const value = m[1].trim().replace(/^["']|["']$/g, "");
|
|
3979
|
-
if (value && !value.includes("$")) return {
|
|
3980
|
-
bundleId: value,
|
|
3981
|
-
scheme: name.slice(0, -10)
|
|
3982
|
-
};
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
return null;
|
|
3987
|
-
}
|
|
3988
|
-
/**
|
|
3989
|
-
* 自动推导三端应用标识(项目配置缺省时的兜底,配置可全空):
|
|
3990
|
-
* android:RN app.json android.applicationId > 原生 gradle applicationId;
|
|
3991
|
-
* harmony:AppScope/app.json5(根或 harmony/ 下)> RN app.json harmony.bundleName;
|
|
3992
|
-
* ios:RN app.json ios.bundleIdentifier > 两层内 xcodeproj 的 PRODUCT_BUNDLE_IDENTIFIER(scheme=工程名)。
|
|
3993
|
-
* 容错:文件不存在/正则不中只置 undefined,绝不抛。
|
|
3994
|
-
*/
|
|
3995
|
-
function detectBundleIds(cwd) {
|
|
3996
|
-
const appJson = readJson(path.join(cwd, "app.json"));
|
|
3997
|
-
const pbx = findPbxproj(cwd);
|
|
3998
|
-
const expo = appJson?.expo ?? null;
|
|
3999
|
-
const expoAndroid = expo ? pickString(expo, "android", "package") : void 0;
|
|
4000
|
-
const expoIos = expo ? pickString(expo, "ios", "bundleIdentifier") : void 0;
|
|
4001
|
-
const expoName = expo && typeof expo.name === "string" ? expo.name : void 0;
|
|
4002
|
-
return {
|
|
4003
|
-
androidPkgName: expoAndroid ?? pickString(appJson, "android", "applicationId") ?? gradleApplicationId(cwd),
|
|
4004
|
-
harmonyBundleName: appJson5BundleName(path.join(cwd, "AppScope", "app.json5")) ?? appJson5BundleName(path.join(cwd, "harmony", "AppScope", "app.json5")) ?? pickString(appJson, "harmony", "bundleName"),
|
|
4005
|
-
iosBundleId: expoIos ?? pickString(appJson, "ios", "bundleIdentifier") ?? pbx?.bundleId,
|
|
4006
|
-
iosScheme: pbx?.scheme ?? expoName
|
|
4007
|
-
};
|
|
4008
|
-
}
|
|
4009
|
-
/** 极简 glob 目录游走(** 跨层,* 单段),避免引入 minimatch 依赖 */
|
|
4010
|
-
function globWalk(dir, segments) {
|
|
4011
|
-
const [head, ...rest] = segments;
|
|
4012
|
-
if (!head) return [dir];
|
|
4013
|
-
if (!fs.existsSync(dir)) return [];
|
|
4014
|
-
if (head === "**") {
|
|
4015
|
-
const out = [];
|
|
4016
|
-
const walkAll = (d) => {
|
|
4017
|
-
out.push(...globWalk(d, rest));
|
|
4018
|
-
for (const name of fs.readdirSync(d, { withFileTypes: true })) {
|
|
4019
|
-
if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules") continue;
|
|
4020
|
-
walkAll(path.join(d, name.name));
|
|
4021
|
-
}
|
|
4022
|
-
};
|
|
4023
|
-
walkAll(dir);
|
|
4024
|
-
return [...new Set(out)];
|
|
4025
|
-
}
|
|
4026
|
-
if (head.includes("*")) {
|
|
4027
|
-
const re = new RegExp(`^${head.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`);
|
|
4028
|
-
const out = [];
|
|
4029
|
-
for (const name of fs.readdirSync(dir)) if (re.test(name)) out.push(...globWalk(path.join(dir, name), rest));
|
|
4030
|
-
return out;
|
|
4031
|
-
}
|
|
4032
|
-
return globWalk(path.join(dir, head), rest);
|
|
4033
|
-
}
|
|
4034
|
-
//#endregion
|
|
4035
4025
|
//#region src/history.ts
|
|
4036
4026
|
/**
|
|
4037
4027
|
* 发版历史:~/.applift/history.jsonl,每行一条 JSON(HistoryEntry)。
|
|
@@ -4073,438 +4063,26 @@ function readEntries(query = {}, filePath = HISTORY_PATH) {
|
|
|
4073
4063
|
//#region src/channels/shared/error-codes.ts
|
|
4074
4064
|
/**
|
|
4075
4065
|
* 错误码人话化:消费各渠道自带 errorHints(平台内聚——表与渠道实现同文件)。
|
|
4066
|
+
* 网络类故障(AbortSignal 超时/断连/DNS)渠道层不转译,统一在此兜底提示。
|
|
4076
4067
|
*/
|
|
4068
|
+
/** 网络故障特征 → 人话提示(命中即返回,按序优先:文件可读性 > 超时 > 网络故障) */
|
|
4069
|
+
const NETWORK_HINTS = [
|
|
4070
|
+
[/NotReadableError/i, "产物/图标文件在上传期间被修改或不可读(惰性 Blob 读盘中途失败):确认构建已完成、无并发进程改动产物,重跑"],
|
|
4071
|
+
[/aborted due to timeout|TimeoutError/i, "请求超时(服务端无响应或网络不稳):可重跑;大包上传建议在稳定网络环境执行"],
|
|
4072
|
+
[/\b(?:ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|EPIPE)\b|socket hang up|fetch failed/i, "网络故障(连接被重置/DNS 解析失败等):检查网络或代理后重跑"]
|
|
4073
|
+
];
|
|
4074
|
+
/** 码匹配:纯数字码加数字边界(防 1003 误中 11003/10031);其余按包含匹配 */
|
|
4075
|
+
function matchesCode(message, code) {
|
|
4076
|
+
if (/^\d+$/.test(code)) return new RegExp(`(?<!\\d)${code}(?!\\d)`).test(message);
|
|
4077
|
+
return message.includes(code);
|
|
4078
|
+
}
|
|
4077
4079
|
function humanizeError(hints, message) {
|
|
4080
|
+
for (const [re, hint] of NETWORK_HINTS) if (re.test(message)) return `${message}\n 提示: ${hint}`;
|
|
4078
4081
|
if (!hints) return message;
|
|
4079
|
-
for (const [code, hint] of Object.entries(hints)) if (message
|
|
4082
|
+
for (const [code, hint] of Object.entries(hints)) if (matchesCode(message, code)) return `${message}\n 提示: ${hint}`;
|
|
4080
4083
|
return message;
|
|
4081
4084
|
}
|
|
4082
4085
|
//#endregion
|
|
4083
|
-
//#region src/version/project.ts
|
|
4084
|
-
/**
|
|
4085
|
-
* 工程文件版本读写(可接管模式:版本事实源永远是工程文件,applift 只代读代写)。
|
|
4086
|
-
* 只认标准模板形态(AGP/Xcode/DevEco/pubspec/package.json/tauri.conf);行级改写、
|
|
4087
|
-
* 幂等(字段定位 + 写死目标值,不依赖旧值,跑 N 次 diff 不变——qy-driver sync-app-identity 同款模式)。
|
|
4088
|
-
* version catalog 引用形态(libs.versions.xxx.get())读侧识别并标注 problem,不代写。
|
|
4089
|
-
*/
|
|
4090
|
-
function readText(p) {
|
|
4091
|
-
try {
|
|
4092
|
-
return fs.readFileSync(p, "utf8");
|
|
4093
|
-
} catch {
|
|
4094
|
-
return null;
|
|
4095
|
-
}
|
|
4096
|
-
}
|
|
4097
|
-
function firstExisting(...paths) {
|
|
4098
|
-
return paths.find((p) => fs.existsSync(p));
|
|
4099
|
-
}
|
|
4100
|
-
function locateIosProject(dir) {
|
|
4101
|
-
const find = (d) => {
|
|
4102
|
-
try {
|
|
4103
|
-
return fs.readdirSync(d, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.endsWith(".xcodeproj") && !e.name.startsWith(".") && e.name !== "Pods.xcodeproj");
|
|
4104
|
-
} catch {
|
|
4105
|
-
return;
|
|
4106
|
-
}
|
|
4107
|
-
};
|
|
4108
|
-
let xcodeproj = find(dir);
|
|
4109
|
-
let base = dir;
|
|
4110
|
-
if (!xcodeproj) try {
|
|
4111
|
-
const subs = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "Pods");
|
|
4112
|
-
for (const s of subs) {
|
|
4113
|
-
xcodeproj = find(path.join(dir, s.name));
|
|
4114
|
-
if (xcodeproj) {
|
|
4115
|
-
base = path.join(dir, s.name);
|
|
4116
|
-
break;
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
} catch {}
|
|
4120
|
-
const pbxproj = xcodeproj ? path.join(base, xcodeproj.name, "project.pbxproj") : void 0;
|
|
4121
|
-
const projDir = xcodeproj?.name.replace(/\.xcodeproj$/, "");
|
|
4122
|
-
const plist = projDir && fs.existsSync(path.join(base, projDir, "Info.plist")) ? path.join(base, projDir, "Info.plist") : void 0;
|
|
4123
|
-
return {
|
|
4124
|
-
pbxproj: pbxproj && fs.existsSync(pbxproj) ? pbxproj : void 0,
|
|
4125
|
-
plist
|
|
4126
|
-
};
|
|
4127
|
-
}
|
|
4128
|
-
function locateVersionFiles(cwd, framework) {
|
|
4129
|
-
const loc = {};
|
|
4130
|
-
const id = framework?.id ?? "";
|
|
4131
|
-
const tryAll = framework === null;
|
|
4132
|
-
if (tryAll || id === "rn" || id === "native-android" || id === "flutter") loc.android = firstExisting(path.join(cwd, "android", "app", "build.gradle.kts"), path.join(cwd, "android", "app", "build.gradle"), path.join(cwd, "app", "build.gradle.kts"), path.join(cwd, "app", "build.gradle"));
|
|
4133
|
-
if (tryAll || id === "rn" || id === "native-ios" || id === "flutter") {
|
|
4134
|
-
const sub = locateIosProject(path.join(cwd, "ios"));
|
|
4135
|
-
const root = sub.pbxproj ? sub : locateIosProject(cwd);
|
|
4136
|
-
loc.ios = root.pbxproj;
|
|
4137
|
-
loc.plist = root.plist;
|
|
4138
|
-
}
|
|
4139
|
-
if (tryAll || id === "rn" || id === "native-harmony" || id === "flutter") loc.harmony = firstExisting(path.join(cwd, "harmony", "AppScope", "app.json5"), path.join(cwd, "AppScope", "app.json5"));
|
|
4140
|
-
if (tryAll || id === "flutter") loc.pubspec = fs.existsSync(path.join(cwd, "pubspec.yaml")) ? path.join(cwd, "pubspec.yaml") : void 0;
|
|
4141
|
-
if (tryAll || id === "rn" || id === "electron") loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
|
|
4142
|
-
if (tryAll || id === "tauri") {
|
|
4143
|
-
loc.tauri = firstExisting(path.join(cwd, "src-tauri", "tauri.conf.json"), path.join(cwd, "src-tauri", "tauri.conf.json5"));
|
|
4144
|
-
loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
|
|
4145
|
-
}
|
|
4146
|
-
if (id === "rn") {
|
|
4147
|
-
const appJson = readText(path.join(cwd, "app.json"));
|
|
4148
|
-
if (appJson && /"expo"\s*:/.test(appJson)) loc.expoAppJson = path.join(cwd, "app.json");
|
|
4149
|
-
}
|
|
4150
|
-
return loc;
|
|
4151
|
-
}
|
|
4152
|
-
function readGradle(file) {
|
|
4153
|
-
const text = readText(file);
|
|
4154
|
-
if (!text) return {};
|
|
4155
|
-
const out = {};
|
|
4156
|
-
const codeM = /(versionCode(?!Suffix)\s*=?\s*)(\d+)/.exec(text);
|
|
4157
|
-
const nameM = /(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/.exec(text);
|
|
4158
|
-
if (codeM) out.versionCode = codeM[2];
|
|
4159
|
-
if (nameM) {
|
|
4160
|
-
const nameVal = nameM[2].replace(/^["']|["']$/g, "");
|
|
4161
|
-
if (nameVal.startsWith("libs.versions.")) out.problem = "versionName 走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
|
|
4162
|
-
else out.versionName = nameVal;
|
|
4163
|
-
}
|
|
4164
|
-
if (/(versionCode|versionName)\s*=?\s*libs\.versions\./.test(text) && !out.problem) out.problem = "版本字段走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
|
|
4165
|
-
return out;
|
|
4166
|
-
}
|
|
4167
|
-
function readPbxproj(file) {
|
|
4168
|
-
const text = readText(file);
|
|
4169
|
-
if (!text) return {};
|
|
4170
|
-
const names = [...text.matchAll(/MARKETING_VERSION\s*=\s*([^;]+);/g)].map((m) => m[1].trim());
|
|
4171
|
-
return {
|
|
4172
|
-
versionName: names[0],
|
|
4173
|
-
versionCode: /CURRENT_PROJECT_VERSION\s*=\s*([^;]+);/.exec(text)?.[1]?.trim(),
|
|
4174
|
-
problem: new Set(names).size > 1 ? `多 target MARKETING_VERSION 不一致(${[...new Set(names)].join(" / ")}):读第一个,bump 时将全部同步` : void 0
|
|
4175
|
-
};
|
|
4176
|
-
}
|
|
4177
|
-
function readPlist(file) {
|
|
4178
|
-
const text = readText(file);
|
|
4179
|
-
if (!text) return {};
|
|
4180
|
-
const name = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
|
|
4181
|
-
const code = /<key>CFBundleVersion<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
|
|
4182
|
-
if (name && !name.startsWith("$")) return {
|
|
4183
|
-
versionName: name,
|
|
4184
|
-
versionCode: code
|
|
4185
|
-
};
|
|
4186
|
-
return {};
|
|
4187
|
-
}
|
|
4188
|
-
function readAppJson5(file) {
|
|
4189
|
-
const text = readText(file);
|
|
4190
|
-
if (!text) return {};
|
|
4191
|
-
return {
|
|
4192
|
-
versionName: /"versionName"\s*:\s*"([^"]+)"/.exec(text)?.[1],
|
|
4193
|
-
versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1]
|
|
4194
|
-
};
|
|
4195
|
-
}
|
|
4196
|
-
function readPubspec(file) {
|
|
4197
|
-
const text = readText(file);
|
|
4198
|
-
if (!text) return {};
|
|
4199
|
-
const m = /^version:\s*(\S+)(?:\s*#.*)?$/m.exec(text);
|
|
4200
|
-
if (!m) return {};
|
|
4201
|
-
const [name, code] = m[1].split("+");
|
|
4202
|
-
return {
|
|
4203
|
-
versionName: name,
|
|
4204
|
-
versionCode: code
|
|
4205
|
-
};
|
|
4206
|
-
}
|
|
4207
|
-
function readJsonVersionKey(file) {
|
|
4208
|
-
const text = readText(file);
|
|
4209
|
-
if (!text) return {};
|
|
4210
|
-
let best;
|
|
4211
|
-
for (const m of text.matchAll(/^(\s*)"version"\s*:\s*"([^"]+)"\s*,?\s*$/gm)) {
|
|
4212
|
-
const indent = m[1].length;
|
|
4213
|
-
if (!best || indent < best.indent) best = {
|
|
4214
|
-
indent,
|
|
4215
|
-
value: m[2]
|
|
4216
|
-
};
|
|
4217
|
-
}
|
|
4218
|
-
if (best) return { versionName: best.value };
|
|
4219
|
-
try {
|
|
4220
|
-
const obj = JSON.parse(text);
|
|
4221
|
-
if (typeof obj.version === "string") return { versionName: obj.version };
|
|
4222
|
-
} catch {}
|
|
4223
|
-
const flat = /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1];
|
|
4224
|
-
return flat ? { versionName: flat } : {};
|
|
4225
|
-
}
|
|
4226
|
-
function readExpoAppJson(file) {
|
|
4227
|
-
const text = readText(file);
|
|
4228
|
-
if (!text) return {};
|
|
4229
|
-
return {
|
|
4230
|
-
versionName: /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1],
|
|
4231
|
-
versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1] ?? /"buildNumber"\s*:\s*"([^"]+)"/.exec(text)?.[1]
|
|
4232
|
-
};
|
|
4233
|
-
}
|
|
4234
|
-
/** 各端版本实况(按框架布局;未识别框架时尝试全部标准位置) */
|
|
4235
|
-
function readProjectVersions(cwd, framework) {
|
|
4236
|
-
const loc = locateVersionFiles(cwd, framework);
|
|
4237
|
-
const out = [];
|
|
4238
|
-
if (loc.android) out.push({
|
|
4239
|
-
platform: "android",
|
|
4240
|
-
file: loc.android,
|
|
4241
|
-
kind: "gradle",
|
|
4242
|
-
...readGradle(loc.android)
|
|
4243
|
-
});
|
|
4244
|
-
if (loc.ios) out.push({
|
|
4245
|
-
platform: "ios",
|
|
4246
|
-
file: loc.ios,
|
|
4247
|
-
kind: "pbxproj",
|
|
4248
|
-
...readPbxproj(loc.ios)
|
|
4249
|
-
});
|
|
4250
|
-
else if (loc.plist) {
|
|
4251
|
-
const plist = readPlist(loc.plist);
|
|
4252
|
-
if (plist.versionName) out.push({
|
|
4253
|
-
platform: "ios",
|
|
4254
|
-
file: loc.plist,
|
|
4255
|
-
kind: "pbxproj",
|
|
4256
|
-
...plist,
|
|
4257
|
-
problem: "未找到 pbxproj,读 Info.plist 字面量"
|
|
4258
|
-
});
|
|
4259
|
-
}
|
|
4260
|
-
if (loc.harmony) out.push({
|
|
4261
|
-
platform: "harmony",
|
|
4262
|
-
file: loc.harmony,
|
|
4263
|
-
kind: "app-json5",
|
|
4264
|
-
...readAppJson5(loc.harmony)
|
|
4265
|
-
});
|
|
4266
|
-
if (loc.pubspec) out.push({
|
|
4267
|
-
platform: "flutter",
|
|
4268
|
-
file: loc.pubspec,
|
|
4269
|
-
kind: "pubspec",
|
|
4270
|
-
...readPubspec(loc.pubspec)
|
|
4271
|
-
});
|
|
4272
|
-
if (loc.packageJson) out.push({
|
|
4273
|
-
platform: "js",
|
|
4274
|
-
file: loc.packageJson,
|
|
4275
|
-
kind: "package-json",
|
|
4276
|
-
...readJsonVersionKey(loc.packageJson)
|
|
4277
|
-
});
|
|
4278
|
-
if (loc.tauri) out.push({
|
|
4279
|
-
platform: "tauri",
|
|
4280
|
-
file: loc.tauri,
|
|
4281
|
-
kind: "tauri",
|
|
4282
|
-
...readJsonVersionKey(loc.tauri)
|
|
4283
|
-
});
|
|
4284
|
-
if (loc.expoAppJson) out.push({
|
|
4285
|
-
platform: "expo",
|
|
4286
|
-
file: loc.expoAppJson,
|
|
4287
|
-
kind: "expo-app-json",
|
|
4288
|
-
...readExpoAppJson(loc.expoAppJson)
|
|
4289
|
-
});
|
|
4290
|
-
return out;
|
|
4291
|
-
}
|
|
4292
|
-
/** 指定捕获组替换为 value(其余组原样保留;约定各正则组外无字面量) */
|
|
4293
|
-
function replaceGroup(re, text, group, value) {
|
|
4294
|
-
let count = 0;
|
|
4295
|
-
return {
|
|
4296
|
-
text: text.replace(re, (_m, ...groups) => {
|
|
4297
|
-
count++;
|
|
4298
|
-
const g = groups.slice(0, -2);
|
|
4299
|
-
g[group - 1] = value;
|
|
4300
|
-
return g.join("");
|
|
4301
|
-
}),
|
|
4302
|
-
count
|
|
4303
|
-
};
|
|
4304
|
-
}
|
|
4305
|
-
function writeIfChanged(file, next) {
|
|
4306
|
-
if (readText(file) === next) return false;
|
|
4307
|
-
fs.writeFileSync(file, next, "utf8");
|
|
4308
|
-
return true;
|
|
4309
|
-
}
|
|
4310
|
-
function writeGradle(file, w) {
|
|
4311
|
-
const text = readText(file);
|
|
4312
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4313
|
-
if (/version(Code|Name)\s*=?\s*libs\.versions\./.test(text)) throw new Error(`${file} 的版本字段是 version catalog 引用形态:请先改造为字面量(applift 只写标准形态)`);
|
|
4314
|
-
let next = text;
|
|
4315
|
-
const code = replaceGroup(/(versionCode(?!Suffix)\s*=?\s*)(\d+)/g, next, 2, String(w.versionCode));
|
|
4316
|
-
next = code.text;
|
|
4317
|
-
const name = replaceGroup(/(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/g, next, 2, `"${w.versionName}"`);
|
|
4318
|
-
next = name.text;
|
|
4319
|
-
if (code.count === 0 && name.count === 0) throw new Error(`${file} 未找到 versionCode/versionName 字面量`);
|
|
4320
|
-
return writeIfChanged(file, next);
|
|
4321
|
-
}
|
|
4322
|
-
function writePbxproj(file, w) {
|
|
4323
|
-
const text = readText(file);
|
|
4324
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4325
|
-
let next = text;
|
|
4326
|
-
const name = replaceGroup(/(MARKETING_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, w.versionName);
|
|
4327
|
-
next = name.text;
|
|
4328
|
-
const code = replaceGroup(/(CURRENT_PROJECT_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, String(w.versionCode));
|
|
4329
|
-
next = code.text;
|
|
4330
|
-
if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 MARKETING_VERSION/CURRENT_PROJECT_VERSION`);
|
|
4331
|
-
return writeIfChanged(file, next);
|
|
4332
|
-
}
|
|
4333
|
-
function writeAppJson5(file, w) {
|
|
4334
|
-
const text = readText(file);
|
|
4335
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4336
|
-
let next = text;
|
|
4337
|
-
const name = replaceGroup(/("versionName"\s*:\s*")([^"]+)(")/g, next, 2, w.versionName);
|
|
4338
|
-
next = name.text;
|
|
4339
|
-
const code = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode));
|
|
4340
|
-
next = code.text;
|
|
4341
|
-
if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 versionName/versionCode`);
|
|
4342
|
-
return writeIfChanged(file, next);
|
|
4343
|
-
}
|
|
4344
|
-
function writePubspec(file, w) {
|
|
4345
|
-
const text = readText(file);
|
|
4346
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4347
|
-
if (!/^version:\s*\S+/m.test(text)) throw new Error(`${file} 未找到 version: 行`);
|
|
4348
|
-
return writeIfChanged(file, text.replace(/^version:[^\n#]*?(\s*)(#.*)?$/m, (_m, gap, comment) => `version: ${w.versionName}+${w.versionCode}${comment ? gap ?? " " : ""}${comment ?? ""}`));
|
|
4349
|
-
}
|
|
4350
|
-
/** JSON 顶层 version 键改写(两遍:先定位最小缩进行,再只改该行——单遍回调在嵌套键
|
|
4351
|
-
* 先出现时会把它一并改写,篡改依赖声明)。单行 JSON 回退首个匹配。 */
|
|
4352
|
-
function writeJsonVersionKey(file, versionName) {
|
|
4353
|
-
const text = readText(file);
|
|
4354
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4355
|
-
const lines = text.split("\n");
|
|
4356
|
-
const re = /^(\s*)"version"\s*:\s*"([^"]+)"(\s*,?\s*)$/;
|
|
4357
|
-
let target = -1;
|
|
4358
|
-
let bestIndent = Number.POSITIVE_INFINITY;
|
|
4359
|
-
for (let i = 0; i < lines.length; i++) {
|
|
4360
|
-
const m = re.exec(lines[i]);
|
|
4361
|
-
if (!m) continue;
|
|
4362
|
-
const ind = m[1].length;
|
|
4363
|
-
if (ind < bestIndent) {
|
|
4364
|
-
bestIndent = ind;
|
|
4365
|
-
target = i;
|
|
4366
|
-
}
|
|
4367
|
-
}
|
|
4368
|
-
let next;
|
|
4369
|
-
if (target >= 0) {
|
|
4370
|
-
const m = re.exec(lines[target]);
|
|
4371
|
-
lines[target] = `${m[1]}"version": "${versionName}"${m[3]}`;
|
|
4372
|
-
next = lines.join("\n");
|
|
4373
|
-
} else {
|
|
4374
|
-
let topVersion;
|
|
4375
|
-
try {
|
|
4376
|
-
const obj = JSON.parse(text);
|
|
4377
|
-
if (typeof obj.version === "string") topVersion = obj.version;
|
|
4378
|
-
} catch {}
|
|
4379
|
-
const esc = (topVersion ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4380
|
-
const re1 = topVersion ? new RegExp(`"version"\\s*:\\s*"${esc}"`) : /"version"\s*:\s*"([^"]+)"/;
|
|
4381
|
-
if (!re1.test(text)) throw new Error(`${file} 未找到 "version" 键`);
|
|
4382
|
-
next = text.replace(re1, `"version": "${versionName}"`);
|
|
4383
|
-
}
|
|
4384
|
-
return writeIfChanged(file, next);
|
|
4385
|
-
}
|
|
4386
|
-
function writeExpoAppJson(file, w) {
|
|
4387
|
-
const text = readText(file);
|
|
4388
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4389
|
-
const v = replaceGroup(/("version"\s*:\s*")([^"]+)(")/g, text, 2, w.versionName);
|
|
4390
|
-
if (v.count === 0) throw new Error(`${file} 未找到 expo "version" 键`);
|
|
4391
|
-
let next = v.text;
|
|
4392
|
-
next = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode)).text;
|
|
4393
|
-
next = replaceGroup(/("buildNumber"\s*:\s*")([^"]+)(")/g, next, 2, String(w.versionCode)).text;
|
|
4394
|
-
return writeIfChanged(file, next);
|
|
4395
|
-
}
|
|
4396
|
-
/**
|
|
4397
|
-
* 写回各端版本(多端 versionName 同步、versionCode 各端独立)。
|
|
4398
|
-
* 返回实际改动的文件列表(commit 范围)。problem 端跳过并在返回中列出(不阻断其他端)。
|
|
4399
|
-
*/
|
|
4400
|
-
function writeProjectVersions(cwd, framework, writes) {
|
|
4401
|
-
const loc = locateVersionFiles(cwd, framework);
|
|
4402
|
-
const changed = [];
|
|
4403
|
-
const skipped = [];
|
|
4404
|
-
const plat = new Map(writes.map((w) => [w.platform, w]));
|
|
4405
|
-
const apply = (file, fn, key) => {
|
|
4406
|
-
if (!file) {
|
|
4407
|
-
const w = plat.get(key);
|
|
4408
|
-
if (w) skipped.push({
|
|
4409
|
-
platform: w.platform,
|
|
4410
|
-
reason: `未找到版本文件(${key})`
|
|
4411
|
-
});
|
|
4412
|
-
return;
|
|
4413
|
-
}
|
|
4414
|
-
try {
|
|
4415
|
-
if (fn(file)) changed.push(file);
|
|
4416
|
-
} catch (e) {
|
|
4417
|
-
const code = e.code;
|
|
4418
|
-
if (code === "EACCES" || code === "EPERM") throw e;
|
|
4419
|
-
skipped.push({
|
|
4420
|
-
platform: key,
|
|
4421
|
-
reason: e.message
|
|
4422
|
-
});
|
|
4423
|
-
}
|
|
4424
|
-
};
|
|
4425
|
-
if (plat.has("android")) apply(loc.android, (f) => writeGradle(f, plat.get("android")), "android");
|
|
4426
|
-
if (plat.has("ios")) apply(loc.ios, (f) => writePbxproj(f, plat.get("ios")), "ios");
|
|
4427
|
-
if (plat.has("harmony")) apply(loc.harmony, (f) => writeAppJson5(f, plat.get("harmony")), "harmony");
|
|
4428
|
-
if (plat.has("flutter")) apply(loc.pubspec, (f) => writePubspec(f, plat.get("flutter")), "flutter");
|
|
4429
|
-
if (plat.has("js")) apply(loc.packageJson, (f) => writeJsonVersionKey(f, plat.get("js").versionName), "js");
|
|
4430
|
-
if (plat.has("tauri")) apply(loc.tauri, (f) => writeJsonVersionKey(f, plat.get("tauri").versionName), "tauri");
|
|
4431
|
-
if (plat.has("expo")) apply(loc.expoAppJson, (f) => writeExpoAppJson(f, plat.get("expo")), "expo");
|
|
4432
|
-
return {
|
|
4433
|
-
changed: [...new Set(changed)],
|
|
4434
|
-
skipped
|
|
4435
|
-
};
|
|
4436
|
-
}
|
|
4437
|
-
//#endregion
|
|
4438
|
-
//#region src/version/git.ts
|
|
4439
|
-
/**
|
|
4440
|
-
* git 只读/写小助手(bump 编排与推断共用)。
|
|
4441
|
-
* spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
|
|
4442
|
-
*/
|
|
4443
|
-
function git(cwd, args) {
|
|
4444
|
-
const r = spawnSync("git", args, {
|
|
4445
|
-
cwd,
|
|
4446
|
-
encoding: "utf8",
|
|
4447
|
-
timeout: 6e4
|
|
4448
|
-
});
|
|
4449
|
-
return {
|
|
4450
|
-
ok: !r.error && r.status === 0,
|
|
4451
|
-
stdout: (r.stdout ?? "").trim(),
|
|
4452
|
-
stderr: (r.stderr ?? "").trim()
|
|
4453
|
-
};
|
|
4454
|
-
}
|
|
4455
|
-
function isGitRepo(cwd) {
|
|
4456
|
-
return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
|
|
4457
|
-
}
|
|
4458
|
-
/** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
|
|
4459
|
-
function looseSemver(tag) {
|
|
4460
|
-
const direct = semver.valid(tag);
|
|
4461
|
-
if (direct) return direct;
|
|
4462
|
-
const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
|
|
4463
|
-
return m ? semver.valid(m[1]) ?? void 0 : void 0;
|
|
4464
|
-
}
|
|
4465
|
-
/**
|
|
4466
|
-
* 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
|
|
4467
|
-
* 无 tag / 非 git 仓库返回 undefined。
|
|
4468
|
-
*/
|
|
4469
|
-
function latestSemverTag(cwd) {
|
|
4470
|
-
const r = git(cwd, ["tag", "-l"]);
|
|
4471
|
-
if (!r.ok) return void 0;
|
|
4472
|
-
let best;
|
|
4473
|
-
for (const raw of r.stdout.split("\n")) {
|
|
4474
|
-
const t = raw.trim();
|
|
4475
|
-
if (!t) continue;
|
|
4476
|
-
const v = looseSemver(t);
|
|
4477
|
-
if (!v) continue;
|
|
4478
|
-
if (!best || semver.gt(v, best.v)) best = {
|
|
4479
|
-
raw: t,
|
|
4480
|
-
v
|
|
4481
|
-
};
|
|
4482
|
-
}
|
|
4483
|
-
return best?.raw;
|
|
4484
|
-
}
|
|
4485
|
-
/** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
|
|
4486
|
-
function commitsSince(cwd, ref) {
|
|
4487
|
-
const r = git(cwd, [
|
|
4488
|
-
"log",
|
|
4489
|
-
ref ? `${ref}..HEAD` : "HEAD",
|
|
4490
|
-
"--format=%B%x00",
|
|
4491
|
-
"--no-merges"
|
|
4492
|
-
]);
|
|
4493
|
-
if (!r.ok) return [];
|
|
4494
|
-
return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
|
|
4495
|
-
}
|
|
4496
|
-
/** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
|
|
4497
|
-
* 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
|
|
4498
|
-
function statusShort(cwd) {
|
|
4499
|
-
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
4500
|
-
cwd,
|
|
4501
|
-
encoding: "utf8",
|
|
4502
|
-
timeout: 3e4
|
|
4503
|
-
});
|
|
4504
|
-
if (r.error || r.status !== 0) return [];
|
|
4505
|
-
return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
4506
|
-
}
|
|
4507
|
-
//#endregion
|
|
4508
4086
|
//#region src/version/infer.ts
|
|
4509
4087
|
/**
|
|
4510
4088
|
* 版本推断与决定:锚点 tag..HEAD 的 conventional commits → bump level(纯函数),
|
|
@@ -5039,4 +4617,4 @@ function fromAapt(artifactPath) {
|
|
|
5039
4617
|
}
|
|
5040
4618
|
}
|
|
5041
4619
|
//#endregion
|
|
5042
|
-
export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema,
|
|
4620
|
+
export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertListingFiles, collectReleaseDiff, commitsSince, decideNextVersion, defineConfig, deps, detectBundleIds, electronAdapter, electronFramework, evalVersionCodeFormula, findArtifacts, findUp, flutterAdapter, flutterFramework, formPost, getAgcToken, getHarmonyAppId, git, hmacSign, humanizeError, inferBumpLevel, isGitRepo, knownFrameworkAdapters, latestSemverTag, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, looseSemver, md5File, md5String, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, nextVersionCode, planBump, projectCredentialsPath, readArtifactVersion, readEntries, readJson, readProjectVersions, redactText, registry, renderChangelogSection, resolveFramework, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, resolveProfileSource, rnAdapter, rnFramework, routeDelivery, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, statusShort, storeVersionName, subdirs, suggestFramework, tagReleaseVersion, tauriAdapter, tauriFramework, toChangelogEntries, tsMillis, tsSeconds, upsertChangelog, writeProjectVersions };
|