@applift/core 0.7.2 → 0.8.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.
- package/dist/{project-4_orxby_.mjs → artifact-B2RCmWw9.mjs} +240 -1
- package/dist/{frameworks-D83HVg8t.d.mts → frameworks-DJIt293o.d.mts} +20 -5
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +6 -240
- package/dist/plugins/index.d.mts +51 -24
- package/dist/plugins/index.mjs +325 -124
- package/package.json +1 -1
|
@@ -1,5 +1,168 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import semver from "semver";
|
|
6
|
+
//#region src/version/git.ts
|
|
7
|
+
/**
|
|
8
|
+
* git 只读/写小助手(bump 编排与推断共用)。
|
|
9
|
+
* spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
|
|
10
|
+
*/
|
|
11
|
+
function git(cwd, args) {
|
|
12
|
+
const r = spawnSync("git", args, {
|
|
13
|
+
cwd,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
timeout: 6e4
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
ok: !r.error && r.status === 0,
|
|
19
|
+
stdout: (r.stdout ?? "").trim(),
|
|
20
|
+
stderr: (r.stderr ?? "").trim()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isGitRepo(cwd) {
|
|
24
|
+
return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
|
|
25
|
+
}
|
|
26
|
+
/** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
|
|
27
|
+
function looseSemver(tag) {
|
|
28
|
+
const direct = semver.valid(tag);
|
|
29
|
+
if (direct) return direct;
|
|
30
|
+
const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
|
|
31
|
+
return m ? semver.valid(m[1]) ?? void 0 : void 0;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
|
|
35
|
+
* 无 tag / 非 git 仓库返回 undefined。
|
|
36
|
+
*/
|
|
37
|
+
function latestSemverTag(cwd) {
|
|
38
|
+
const r = git(cwd, ["tag", "-l"]);
|
|
39
|
+
if (!r.ok) return void 0;
|
|
40
|
+
let best;
|
|
41
|
+
for (const raw of r.stdout.split("\n")) {
|
|
42
|
+
const t = raw.trim();
|
|
43
|
+
if (!t) continue;
|
|
44
|
+
const v = looseSemver(t);
|
|
45
|
+
if (!v) continue;
|
|
46
|
+
if (!best || semver.gt(v, best.v)) best = {
|
|
47
|
+
raw: t,
|
|
48
|
+
v
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return best?.raw;
|
|
52
|
+
}
|
|
53
|
+
/** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
|
|
54
|
+
function commitsSince(cwd, ref) {
|
|
55
|
+
const r = git(cwd, [
|
|
56
|
+
"log",
|
|
57
|
+
ref ? `${ref}..HEAD` : "HEAD",
|
|
58
|
+
"--format=%B%x00",
|
|
59
|
+
"--no-merges"
|
|
60
|
+
]);
|
|
61
|
+
if (!r.ok) return [];
|
|
62
|
+
return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
/** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
|
|
65
|
+
* 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
|
|
66
|
+
function statusShort(cwd) {
|
|
67
|
+
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
68
|
+
cwd,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
timeout: 3e4
|
|
71
|
+
});
|
|
72
|
+
if (r.error || r.status !== 0) return [];
|
|
73
|
+
return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
|
|
77
|
+
* - tag 已存在(多端发布,前一端已打)→ 跳过
|
|
78
|
+
* - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
|
|
79
|
+
*/
|
|
80
|
+
function tagReleaseVersion(cwd, version, opts = {}) {
|
|
81
|
+
if (!version) return {
|
|
82
|
+
tagged: false,
|
|
83
|
+
reason: "无版本"
|
|
84
|
+
};
|
|
85
|
+
if (opts.dryRun) return {
|
|
86
|
+
tagged: false,
|
|
87
|
+
reason: "dry-run"
|
|
88
|
+
};
|
|
89
|
+
const tag = `v${version}`;
|
|
90
|
+
if (spawnSync("git", [
|
|
91
|
+
"rev-parse",
|
|
92
|
+
"-q",
|
|
93
|
+
"--verify",
|
|
94
|
+
`refs/tags/${tag}`
|
|
95
|
+
], {
|
|
96
|
+
cwd,
|
|
97
|
+
encoding: "utf8"
|
|
98
|
+
}).status === 0) return {
|
|
99
|
+
tagged: false,
|
|
100
|
+
reason: `tag ${tag} 已存在(前一端已发布)`
|
|
101
|
+
};
|
|
102
|
+
const r = spawnSync("git", ["tag", tag], {
|
|
103
|
+
cwd,
|
|
104
|
+
encoding: "utf8"
|
|
105
|
+
});
|
|
106
|
+
if (r.status !== 0) return {
|
|
107
|
+
tagged: false,
|
|
108
|
+
reason: `tag ${tag} 创建失败:${r.stderr.trim()}`
|
|
109
|
+
};
|
|
110
|
+
return { tagged: true };
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/update.ts
|
|
114
|
+
/**
|
|
115
|
+
* 热更能力契约:applift 的第三维扩展(framework × channel 之外的 update 维度)。
|
|
116
|
+
*
|
|
117
|
+
* provider 与 framework 绑定(如 react-native-update 只服务 RN 工程),由插件经
|
|
118
|
+
* 'update:register' 注册;beta/release 管线的投递路由(热更 vs 原生包)消费 judge 结果,
|
|
119
|
+
* 只做推荐与证据展示,最终投递方式由用户在确认界面拍板(CI 非交互时 uncertain 即中止)。
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* 投递路由:按当前 framework 匹配 provider 并执行判定。
|
|
123
|
+
* - 无 provider / 无匹配 → null(走原生包路径,原始行为不受影响)
|
|
124
|
+
* - 多个 provider 匹配同一 framework → 配置错误(一个框架一个热更实现)
|
|
125
|
+
* 结论是推荐值:最终投递方式由用户在确认界面拍板,CI 非交互时 uncertain 中止
|
|
126
|
+
*/
|
|
127
|
+
async function routeDelivery(providers, ctx, diff, platform) {
|
|
128
|
+
const matched = providers.filter((p) => p.frameworks.includes(ctx.framework.id) && (!platform || !p.platforms || p.platforms.includes(platform)));
|
|
129
|
+
if (matched.length === 0) return null;
|
|
130
|
+
if (matched.length > 1) throw new Error(`多个热更 provider 匹配 framework「${ctx.framework.id}」:${matched.map((p) => p.id).join(", ")}——一个框架只应有一个热更实现`);
|
|
131
|
+
const provider = matched[0];
|
|
132
|
+
return {
|
|
133
|
+
provider,
|
|
134
|
+
judgement: await provider.judge(diff, ctx)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
|
|
139
|
+
* 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
|
|
140
|
+
* 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
|
|
141
|
+
*/
|
|
142
|
+
async function collectReleaseDiff(cwd) {
|
|
143
|
+
const anchor = latestSemverTag(cwd);
|
|
144
|
+
if (!anchor) return null;
|
|
145
|
+
const r = spawnSync("git", [
|
|
146
|
+
"diff",
|
|
147
|
+
"--name-only",
|
|
148
|
+
anchor
|
|
149
|
+
], {
|
|
150
|
+
cwd,
|
|
151
|
+
encoding: "utf8"
|
|
152
|
+
});
|
|
153
|
+
const untracked = spawnSync("git", [
|
|
154
|
+
"ls-files",
|
|
155
|
+
"--others",
|
|
156
|
+
"--exclude-standard"
|
|
157
|
+
], {
|
|
158
|
+
cwd,
|
|
159
|
+
encoding: "utf8"
|
|
160
|
+
});
|
|
161
|
+
if (r.status !== 0 || untracked.status !== 0) return null;
|
|
162
|
+
const files = [...r.stdout.split("\n"), ...untracked.stdout.split("\n")].map((l) => l.trim());
|
|
163
|
+
return [...new Set(files)].filter(Boolean);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
3
166
|
//#region src/framework.ts
|
|
4
167
|
/**
|
|
5
168
|
* 框架适配器:让 applift 框架无关(RN / Flutter / 原生安卓 / 原生 iOS / 原生鸿蒙)。
|
|
@@ -803,4 +966,80 @@ function writeProjectVersions(cwd, framework, writes) {
|
|
|
803
966
|
};
|
|
804
967
|
}
|
|
805
968
|
//#endregion
|
|
806
|
-
|
|
969
|
+
//#region src/version/artifact.ts
|
|
970
|
+
/**
|
|
971
|
+
* 产物版本读取(只读不写——版本号变更属开发提交,发版工具只负责读版本防回退)。
|
|
972
|
+
* .hap/.app:pack.info 是纯 JSON,零依赖直接读;
|
|
973
|
+
* .apk:aapt2/aapt dump badging(ANDROID_HOME/SDK 探测,同 oppo 渠道逻辑);
|
|
974
|
+
* .ipa:二进制 plist 解析成本高,不支持自动读(返回 undefined,doctor 提示用 --version/配置)。
|
|
975
|
+
*/
|
|
976
|
+
function readArtifactVersion(artifactPath) {
|
|
977
|
+
const ext = path.extname(artifactPath).toLowerCase();
|
|
978
|
+
if (ext === ".hap" || ext === ".app") return fromPackInfo(artifactPath);
|
|
979
|
+
if (ext === ".apk") return fromAapt(artifactPath);
|
|
980
|
+
}
|
|
981
|
+
/** 鸿蒙包版本读取:优先包内 pack.info(zip 容器内的权威数据),回退同目录 pack.info(标注可能过期) */
|
|
982
|
+
function fromPackInfo(artifactPath) {
|
|
983
|
+
const r = spawnSync("unzip", [
|
|
984
|
+
"-p",
|
|
985
|
+
artifactPath,
|
|
986
|
+
"pack.info"
|
|
987
|
+
], {
|
|
988
|
+
timeout: 1e4,
|
|
989
|
+
encoding: "utf8"
|
|
990
|
+
});
|
|
991
|
+
if (!r.error && r.status === 0 && r.stdout) {
|
|
992
|
+
const v = parsePackInfo(r.stdout, "pack.info(包内)");
|
|
993
|
+
if (v) return v;
|
|
994
|
+
}
|
|
995
|
+
const side = path.join(path.dirname(artifactPath), "pack.info");
|
|
996
|
+
if (fs.existsSync(side)) try {
|
|
997
|
+
const v = parsePackInfo(fs.readFileSync(side, "utf8"), "pack.info(同目录,可能过期)");
|
|
998
|
+
if (v) return v;
|
|
999
|
+
} catch {}
|
|
1000
|
+
}
|
|
1001
|
+
/** 真实鸿蒙 pack.info schema(qy-driver 实测样本):summary.app.version.{name,code,build};
|
|
1002
|
+
* 顶层 versionName/versionCode 为历史兼容形态。 */
|
|
1003
|
+
function parsePackInfo(text, source) {
|
|
1004
|
+
try {
|
|
1005
|
+
const info = JSON.parse(text);
|
|
1006
|
+
const nested = info.summary?.app?.version;
|
|
1007
|
+
const versionName = nested?.name ?? info.versionName;
|
|
1008
|
+
const versionCode = nested?.code ?? info.versionCode;
|
|
1009
|
+
if (versionName || versionCode !== void 0) return {
|
|
1010
|
+
versionName,
|
|
1011
|
+
versionCode: String(versionCode ?? ""),
|
|
1012
|
+
source
|
|
1013
|
+
};
|
|
1014
|
+
} catch {}
|
|
1015
|
+
}
|
|
1016
|
+
/** APK:aapt2/aapt dump badging(versionCode/versionName) */
|
|
1017
|
+
function fromAapt(artifactPath) {
|
|
1018
|
+
const tools = [];
|
|
1019
|
+
const sdkDir = process.env.ANDROID_HOME ?? path.join(os.homedir(), "Library/Android/sdk");
|
|
1020
|
+
const btDir = path.join(sdkDir, "build-tools");
|
|
1021
|
+
if (fs.existsSync(btDir)) for (const bt of fs.readdirSync(btDir).sort().reverse()) for (const tool of ["aapt2", "aapt"]) {
|
|
1022
|
+
const p = path.join(btDir, bt, tool);
|
|
1023
|
+
if (fs.existsSync(p)) tools.push(p);
|
|
1024
|
+
}
|
|
1025
|
+
for (const tool of tools) {
|
|
1026
|
+
const r = spawnSync(tool, [
|
|
1027
|
+
"dump",
|
|
1028
|
+
"badging",
|
|
1029
|
+
artifactPath
|
|
1030
|
+
], {
|
|
1031
|
+
timeout: 1e4,
|
|
1032
|
+
encoding: "utf8"
|
|
1033
|
+
});
|
|
1034
|
+
if (r.error || r.status !== 0) continue;
|
|
1035
|
+
const name = /versionName='([^']*)'/.exec(r.stdout ?? "")?.[1];
|
|
1036
|
+
const code = /versionCode='(\d+)'/.exec(r.stdout ?? "")?.[1];
|
|
1037
|
+
if (name || code) return {
|
|
1038
|
+
versionName: name,
|
|
1039
|
+
versionCode: code,
|
|
1040
|
+
source: path.basename(tool)
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
//#endregion
|
|
1045
|
+
export { git as A, findArtifacts as C, collectReleaseDiff as D, subdirs as E, tagReleaseVersion as F, latestSemverTag as M, looseSemver as N, routeDelivery as O, statusShort as P, detectBundleIds as S, resolveFramework as T, rnFramework as _, electronAdapter as a, tauriFramework as b, flutterFramework as c, nativeAndroidFramework as d, nativeHarmonyAdapter as f, rnAdapter as g, nativeIosFramework as h, redactText as i, isGitRepo as j, commitsSince as k, knownFrameworkAdapters as l, nativeIosAdapter as m, readProjectVersions as n, electronFramework as o, nativeHarmonyFramework as p, writeProjectVersions as r, flutterAdapter as s, readArtifactVersion as t, nativeAndroidAdapter as u, suggestFramework as v, readJson as w, deps as x, tauriAdapter as y };
|
|
@@ -351,16 +351,25 @@ interface UpdateProvider {
|
|
|
351
351
|
platforms?: string[];
|
|
352
352
|
/** 判定本次 diff 能否走热更(diff = 上个发版锚点到工作区的文件列表,含未提交与未跟踪) */
|
|
353
353
|
judge(diff: string[], ctx: FrameworkContext): Promise<DeliveryJudgement> | DeliveryJudgement;
|
|
354
|
-
/**
|
|
355
|
-
|
|
354
|
+
/** release 热更路径(必选):直接发热更正式——ppk 产物入口(--artifact/探测)→ 发布绑正式原生版本 + 灰度。
|
|
355
|
+
* 无内测流程的 provider 只实现此方法即可独立完成正式热更发布(热更不依赖 beta) */
|
|
356
|
+
publishToProd(ctx: PublishContext): Promise<{
|
|
356
357
|
versionName: string;
|
|
357
358
|
versionId: number;
|
|
359
|
+
rollout?: number;
|
|
358
360
|
}>;
|
|
359
|
-
/**
|
|
360
|
-
|
|
361
|
+
/** beta 热更路径(可选):热更包发测试通道(测试设备验证用);无内测流程的 provider 不实现
|
|
362
|
+
* (beta 判定 ota 时报错引导直接 release) */
|
|
363
|
+
publishToBeta?(ctx: PublishContext): Promise<{
|
|
361
364
|
versionName: string;
|
|
362
|
-
|
|
365
|
+
versionId: number;
|
|
363
366
|
}>;
|
|
367
|
+
/** release 热更路径(可选):测试通道最新已验证热更 promote 到正式(重绑正式原生版本 + 灰度)。
|
|
368
|
+
* 返回 null = 测试通道无可 promote(调用方转 publishToProd 直接发布) */
|
|
369
|
+
promoteToProd?(ctx: PublishContext): Promise<{
|
|
370
|
+
versionName: string;
|
|
371
|
+
rollout?: number;
|
|
372
|
+
} | null>;
|
|
364
373
|
}
|
|
365
374
|
interface DeliveryRoute {
|
|
366
375
|
provider: UpdateProvider;
|
|
@@ -373,6 +382,10 @@ interface PublishContext extends FrameworkContext {
|
|
|
373
382
|
note?: string;
|
|
374
383
|
/** release 热更的灰度比例(CLI 确认时收集;缺省用 provider 默认) */
|
|
375
384
|
rollout?: number;
|
|
385
|
+
/** 已解析 profile 凭据(热更 provider 读服务端 token,如 PUSHY_API_TOKEN;dry-run 路径缺省) */
|
|
386
|
+
credentials?: Readonly<Record<string, string>>;
|
|
387
|
+
/** 显式指定的热更 ppk(--artifact 透传;热更产物入口,与原生 --artifact 对称) */
|
|
388
|
+
artifact?: string;
|
|
376
389
|
}
|
|
377
390
|
/**
|
|
378
391
|
* 投递路由:按当前 framework 匹配 provider 并执行判定。
|
|
@@ -394,6 +407,8 @@ interface HookContext {
|
|
|
394
407
|
channel?: string;
|
|
395
408
|
/** 项目根目录 */
|
|
396
409
|
cwd: string;
|
|
410
|
+
/** 该渠道已解析的 profile 凭据(stage/submit 级钩子才有;供基线上传类插件读服务端 token) */
|
|
411
|
+
credentials?: Readonly<Record<string, string>>;
|
|
397
412
|
}
|
|
398
413
|
interface AppliftPlugin {
|
|
399
414
|
name: string;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as StoreConfigSchema, A as routeDelivery, B as appendEntry, C as resolvePlugins, Ct as subdirs, D as PublishContext, E as FrameworkContext, F as runSubmit, G as CredentialsFileSchema, H as BetaConfig, I as HISTORY_PATH, J as GlobalConfig, K as CredentialsProfile, L as HistoryEntry, M as PipelineSummary, N as PipelineSummaryEntry, O as UpdateProvider, P as runStage, Q as StoreConfig, R as HistoryEvent, S as defineConfig, St as resolveFramework, T as DeliveryRoute, U as BetaConfigSchema, V as readEntries, W as CredentialsFile, X as ProjectConfig, Y as GlobalConfigSchema, Z as ProjectConfigSchema, _ as AppliftPlugin, _t as Platform, a as knownFrameworkAdapters, at as VersionConfigSchema, b as PluginHookName, bt as findArtifacts, c as nativeHarmonyAdapter, ct as ChannelContext, d as nativeIosFramework, dt as ReleasePlanEntry, et as Stores, f as rnAdapter, ft as ReviewStatus, g as tauriFramework, gt as FrameworkAdapter, h as tauriAdapter, ht as BundleIds, i as flutterFramework, it as VersionConfig, j as PipelineInput, k as collectReleaseDiff, l as nativeHarmonyFramework, lt as ChannelEvent, m as suggestFramework, mt as SubmitResult, n as electronFramework, nt as VersionCodeConfig, o as nativeAndroidAdapter, ot as Channel, p as rnFramework, pt as StageResult, q as CredentialsProfileSchema, r as flutterAdapter, rt as VersionCodeConfigSchema, s as nativeAndroidFramework, st as ChannelCapabilities, t as electronAdapter, tt as StoresSchema, u as nativeIosAdapter, ut as ChannelRegistry, v as AppliftUserConfig, vt as deps, w as DeliveryJudgement, x as PluginHost, xt as readJson, y as HookContext, yt as detectBundleIds, z as HistoryQuery } from "./frameworks-
|
|
1
|
+
import { $ as StoreConfigSchema, A as routeDelivery, B as appendEntry, C as resolvePlugins, Ct as subdirs, D as PublishContext, E as FrameworkContext, F as runSubmit, G as CredentialsFileSchema, H as BetaConfig, I as HISTORY_PATH, J as GlobalConfig, K as CredentialsProfile, L as HistoryEntry, M as PipelineSummary, N as PipelineSummaryEntry, O as UpdateProvider, P as runStage, Q as StoreConfig, R as HistoryEvent, S as defineConfig, St as resolveFramework, T as DeliveryRoute, U as BetaConfigSchema, V as readEntries, W as CredentialsFile, X as ProjectConfig, Y as GlobalConfigSchema, Z as ProjectConfigSchema, _ as AppliftPlugin, _t as Platform, a as knownFrameworkAdapters, at as VersionConfigSchema, b as PluginHookName, bt as findArtifacts, c as nativeHarmonyAdapter, ct as ChannelContext, d as nativeIosFramework, dt as ReleasePlanEntry, et as Stores, f as rnAdapter, ft as ReviewStatus, g as tauriFramework, gt as FrameworkAdapter, h as tauriAdapter, ht as BundleIds, i as flutterFramework, it as VersionConfig, j as PipelineInput, k as collectReleaseDiff, l as nativeHarmonyFramework, lt as ChannelEvent, m as suggestFramework, mt as SubmitResult, n as electronFramework, nt as VersionCodeConfig, o as nativeAndroidAdapter, ot as Channel, p as rnFramework, pt as StageResult, q as CredentialsProfileSchema, r as flutterAdapter, rt as VersionCodeConfigSchema, s as nativeAndroidFramework, st as ChannelCapabilities, t as electronAdapter, tt as StoresSchema, u as nativeIosAdapter, ut as ChannelRegistry, v as AppliftUserConfig, vt as deps, w as DeliveryJudgement, x as PluginHost, xt as readJson, y as HookContext, yt as detectBundleIds, z as HistoryQuery } from "./frameworks-DJIt293o.mjs";
|
|
2
2
|
//#region src/registry.d.ts
|
|
3
3
|
export declare class DefaultChannelRegistry implements ChannelRegistry {
|
|
4
4
|
private readonly channels;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as git, C as findArtifacts, D as collectReleaseDiff, E as subdirs, F as tagReleaseVersion, M as latestSemverTag, N as looseSemver, O as routeDelivery, P as statusShort, S as detectBundleIds, T as resolveFramework, _ as rnFramework, a as electronAdapter, b as tauriFramework, c as flutterFramework, d as nativeAndroidFramework, f as nativeHarmonyAdapter, g as rnAdapter, h as nativeIosFramework, i as redactText, j as isGitRepo, k as commitsSince, l as knownFrameworkAdapters, m as nativeIosAdapter, n as readProjectVersions, o as electronFramework, p as nativeHarmonyFramework, r as writeProjectVersions, s as flutterAdapter, t as readArtifactVersion, u as nativeAndroidAdapter, v as suggestFramework, w as readJson, x as deps, y as tauriAdapter } from "./artifact-B2RCmWw9.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -567,7 +567,7 @@ var PgyerChannel = class {
|
|
|
567
567
|
});
|
|
568
568
|
}
|
|
569
569
|
if (!token?.data) throw lastErr instanceof Error ? lastErr : /* @__PURE__ */ new Error("蒲公英凭证获取失败");
|
|
570
|
-
const { endpoint, params, buildKey } = token.data;
|
|
570
|
+
const { endpoint, params, key: buildKey } = token.data;
|
|
571
571
|
ctx.emit({
|
|
572
572
|
type: "phase",
|
|
573
573
|
phase: "upload",
|
|
@@ -3617,7 +3617,8 @@ async function runStage(input) {
|
|
|
3617
3617
|
};
|
|
3618
3618
|
if (plugins) await plugins.broadcast("stage:after", {
|
|
3619
3619
|
channel: channel.id,
|
|
3620
|
-
cwd: input.cwd
|
|
3620
|
+
cwd: input.cwd,
|
|
3621
|
+
credentials: effectiveCtx.credentials
|
|
3621
3622
|
}, {
|
|
3622
3623
|
channel: channel.id,
|
|
3623
3624
|
ok: r.ok,
|
|
@@ -3740,7 +3741,8 @@ async function runSubmit(input, options) {
|
|
|
3740
3741
|
});
|
|
3741
3742
|
if (plugins) await plugins.broadcast("stage:after", {
|
|
3742
3743
|
channel: channel.id,
|
|
3743
|
-
cwd: input.cwd
|
|
3744
|
+
cwd: input.cwd,
|
|
3745
|
+
credentials: effectiveCtx.credentials
|
|
3744
3746
|
}, {
|
|
3745
3747
|
channel: channel.id,
|
|
3746
3748
|
ok: true,
|
|
@@ -3817,166 +3819,6 @@ function planFor(plans, base, id, artifactPath) {
|
|
|
3817
3819
|
return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id, artifactPath);
|
|
3818
3820
|
}
|
|
3819
3821
|
//#endregion
|
|
3820
|
-
//#region src/version/git.ts
|
|
3821
|
-
/**
|
|
3822
|
-
* git 只读/写小助手(bump 编排与推断共用)。
|
|
3823
|
-
* spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
|
|
3824
|
-
*/
|
|
3825
|
-
function git(cwd, args) {
|
|
3826
|
-
const r = spawnSync("git", args, {
|
|
3827
|
-
cwd,
|
|
3828
|
-
encoding: "utf8",
|
|
3829
|
-
timeout: 6e4
|
|
3830
|
-
});
|
|
3831
|
-
return {
|
|
3832
|
-
ok: !r.error && r.status === 0,
|
|
3833
|
-
stdout: (r.stdout ?? "").trim(),
|
|
3834
|
-
stderr: (r.stderr ?? "").trim()
|
|
3835
|
-
};
|
|
3836
|
-
}
|
|
3837
|
-
function isGitRepo(cwd) {
|
|
3838
|
-
return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
|
|
3839
|
-
}
|
|
3840
|
-
/** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
|
|
3841
|
-
function looseSemver(tag) {
|
|
3842
|
-
const direct = semver.valid(tag);
|
|
3843
|
-
if (direct) return direct;
|
|
3844
|
-
const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
|
|
3845
|
-
return m ? semver.valid(m[1]) ?? void 0 : void 0;
|
|
3846
|
-
}
|
|
3847
|
-
/**
|
|
3848
|
-
* 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
|
|
3849
|
-
* 无 tag / 非 git 仓库返回 undefined。
|
|
3850
|
-
*/
|
|
3851
|
-
function latestSemverTag(cwd) {
|
|
3852
|
-
const r = git(cwd, ["tag", "-l"]);
|
|
3853
|
-
if (!r.ok) return void 0;
|
|
3854
|
-
let best;
|
|
3855
|
-
for (const raw of r.stdout.split("\n")) {
|
|
3856
|
-
const t = raw.trim();
|
|
3857
|
-
if (!t) continue;
|
|
3858
|
-
const v = looseSemver(t);
|
|
3859
|
-
if (!v) continue;
|
|
3860
|
-
if (!best || semver.gt(v, best.v)) best = {
|
|
3861
|
-
raw: t,
|
|
3862
|
-
v
|
|
3863
|
-
};
|
|
3864
|
-
}
|
|
3865
|
-
return best?.raw;
|
|
3866
|
-
}
|
|
3867
|
-
/** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
|
|
3868
|
-
function commitsSince(cwd, ref) {
|
|
3869
|
-
const r = git(cwd, [
|
|
3870
|
-
"log",
|
|
3871
|
-
ref ? `${ref}..HEAD` : "HEAD",
|
|
3872
|
-
"--format=%B%x00",
|
|
3873
|
-
"--no-merges"
|
|
3874
|
-
]);
|
|
3875
|
-
if (!r.ok) return [];
|
|
3876
|
-
return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
|
|
3877
|
-
}
|
|
3878
|
-
/** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
|
|
3879
|
-
* 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
|
|
3880
|
-
function statusShort(cwd) {
|
|
3881
|
-
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
3882
|
-
cwd,
|
|
3883
|
-
encoding: "utf8",
|
|
3884
|
-
timeout: 3e4
|
|
3885
|
-
});
|
|
3886
|
-
if (r.error || r.status !== 0) return [];
|
|
3887
|
-
return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
3888
|
-
}
|
|
3889
|
-
/**
|
|
3890
|
-
* release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
|
|
3891
|
-
* - tag 已存在(多端发布,前一端已打)→ 跳过
|
|
3892
|
-
* - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
|
|
3893
|
-
*/
|
|
3894
|
-
function tagReleaseVersion(cwd, version, opts = {}) {
|
|
3895
|
-
if (!version) return {
|
|
3896
|
-
tagged: false,
|
|
3897
|
-
reason: "无版本"
|
|
3898
|
-
};
|
|
3899
|
-
if (opts.dryRun) return {
|
|
3900
|
-
tagged: false,
|
|
3901
|
-
reason: "dry-run"
|
|
3902
|
-
};
|
|
3903
|
-
const tag = `v${version}`;
|
|
3904
|
-
if (spawnSync("git", [
|
|
3905
|
-
"rev-parse",
|
|
3906
|
-
"-q",
|
|
3907
|
-
"--verify",
|
|
3908
|
-
`refs/tags/${tag}`
|
|
3909
|
-
], {
|
|
3910
|
-
cwd,
|
|
3911
|
-
encoding: "utf8"
|
|
3912
|
-
}).status === 0) return {
|
|
3913
|
-
tagged: false,
|
|
3914
|
-
reason: `tag ${tag} 已存在(前一端已发布)`
|
|
3915
|
-
};
|
|
3916
|
-
const r = spawnSync("git", ["tag", tag], {
|
|
3917
|
-
cwd,
|
|
3918
|
-
encoding: "utf8"
|
|
3919
|
-
});
|
|
3920
|
-
if (r.status !== 0) return {
|
|
3921
|
-
tagged: false,
|
|
3922
|
-
reason: `tag ${tag} 创建失败:${r.stderr.trim()}`
|
|
3923
|
-
};
|
|
3924
|
-
return { tagged: true };
|
|
3925
|
-
}
|
|
3926
|
-
//#endregion
|
|
3927
|
-
//#region src/update.ts
|
|
3928
|
-
/**
|
|
3929
|
-
* 热更能力契约:applift 的第三维扩展(framework × channel 之外的 update 维度)。
|
|
3930
|
-
*
|
|
3931
|
-
* provider 与 framework 绑定(如 react-native-update 只服务 RN 工程),由插件经
|
|
3932
|
-
* 'update:register' 注册;beta/release 管线的投递路由(热更 vs 原生包)消费 judge 结果,
|
|
3933
|
-
* 只做推荐与证据展示,最终投递方式由用户在确认界面拍板(CI 非交互时 uncertain 即中止)。
|
|
3934
|
-
*/
|
|
3935
|
-
/**
|
|
3936
|
-
* 投递路由:按当前 framework 匹配 provider 并执行判定。
|
|
3937
|
-
* - 无 provider / 无匹配 → null(走原生包路径,原始行为不受影响)
|
|
3938
|
-
* - 多个 provider 匹配同一 framework → 配置错误(一个框架一个热更实现)
|
|
3939
|
-
* 结论是推荐值:最终投递方式由用户在确认界面拍板,CI 非交互时 uncertain 中止
|
|
3940
|
-
*/
|
|
3941
|
-
async function routeDelivery(providers, ctx, diff, platform) {
|
|
3942
|
-
const matched = providers.filter((p) => p.frameworks.includes(ctx.framework.id) && (!platform || !p.platforms || p.platforms.includes(platform)));
|
|
3943
|
-
if (matched.length === 0) return null;
|
|
3944
|
-
if (matched.length > 1) throw new Error(`多个热更 provider 匹配 framework「${ctx.framework.id}」:${matched.map((p) => p.id).join(", ")}——一个框架只应有一个热更实现`);
|
|
3945
|
-
const provider = matched[0];
|
|
3946
|
-
return {
|
|
3947
|
-
provider,
|
|
3948
|
-
judgement: await provider.judge(diff, ctx)
|
|
3949
|
-
};
|
|
3950
|
-
}
|
|
3951
|
-
/**
|
|
3952
|
-
* 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
|
|
3953
|
-
* 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
|
|
3954
|
-
* 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
|
|
3955
|
-
*/
|
|
3956
|
-
async function collectReleaseDiff(cwd) {
|
|
3957
|
-
const anchor = latestSemverTag(cwd);
|
|
3958
|
-
if (!anchor) return null;
|
|
3959
|
-
const r = spawnSync("git", [
|
|
3960
|
-
"diff",
|
|
3961
|
-
"--name-only",
|
|
3962
|
-
anchor
|
|
3963
|
-
], {
|
|
3964
|
-
cwd,
|
|
3965
|
-
encoding: "utf8"
|
|
3966
|
-
});
|
|
3967
|
-
const untracked = spawnSync("git", [
|
|
3968
|
-
"ls-files",
|
|
3969
|
-
"--others",
|
|
3970
|
-
"--exclude-standard"
|
|
3971
|
-
], {
|
|
3972
|
-
cwd,
|
|
3973
|
-
encoding: "utf8"
|
|
3974
|
-
});
|
|
3975
|
-
if (r.status !== 0 || untracked.status !== 0) return null;
|
|
3976
|
-
const files = [...r.stdout.split("\n"), ...untracked.stdout.split("\n")].map((l) => l.trim());
|
|
3977
|
-
return [...new Set(files)].filter(Boolean);
|
|
3978
|
-
}
|
|
3979
|
-
//#endregion
|
|
3980
3822
|
//#region src/plugin.ts
|
|
3981
3823
|
var PluginHost = class {
|
|
3982
3824
|
plugins;
|
|
@@ -4542,80 +4384,4 @@ function applyBump(cwd, plan, framework, opts) {
|
|
|
4542
4384
|
}
|
|
4543
4385
|
}
|
|
4544
4386
|
//#endregion
|
|
4545
|
-
//#region src/version/artifact.ts
|
|
4546
|
-
/**
|
|
4547
|
-
* 产物版本读取(只读不写——版本号变更属开发提交,发版工具只负责读版本防回退)。
|
|
4548
|
-
* .hap/.app:pack.info 是纯 JSON,零依赖直接读;
|
|
4549
|
-
* .apk:aapt2/aapt dump badging(ANDROID_HOME/SDK 探测,同 oppo 渠道逻辑);
|
|
4550
|
-
* .ipa:二进制 plist 解析成本高,不支持自动读(返回 undefined,doctor 提示用 --version/配置)。
|
|
4551
|
-
*/
|
|
4552
|
-
function readArtifactVersion(artifactPath) {
|
|
4553
|
-
const ext = path.extname(artifactPath).toLowerCase();
|
|
4554
|
-
if (ext === ".hap" || ext === ".app") return fromPackInfo(artifactPath);
|
|
4555
|
-
if (ext === ".apk") return fromAapt(artifactPath);
|
|
4556
|
-
}
|
|
4557
|
-
/** 鸿蒙包版本读取:优先包内 pack.info(zip 容器内的权威数据),回退同目录 pack.info(标注可能过期) */
|
|
4558
|
-
function fromPackInfo(artifactPath) {
|
|
4559
|
-
const r = spawnSync("unzip", [
|
|
4560
|
-
"-p",
|
|
4561
|
-
artifactPath,
|
|
4562
|
-
"pack.info"
|
|
4563
|
-
], {
|
|
4564
|
-
timeout: 1e4,
|
|
4565
|
-
encoding: "utf8"
|
|
4566
|
-
});
|
|
4567
|
-
if (!r.error && r.status === 0 && r.stdout) {
|
|
4568
|
-
const v = parsePackInfo(r.stdout, "pack.info(包内)");
|
|
4569
|
-
if (v) return v;
|
|
4570
|
-
}
|
|
4571
|
-
const side = path.join(path.dirname(artifactPath), "pack.info");
|
|
4572
|
-
if (fs.existsSync(side)) try {
|
|
4573
|
-
const v = parsePackInfo(fs.readFileSync(side, "utf8"), "pack.info(同目录,可能过期)");
|
|
4574
|
-
if (v) return v;
|
|
4575
|
-
} catch {}
|
|
4576
|
-
}
|
|
4577
|
-
/** 真实鸿蒙 pack.info schema(qy-driver 实测样本):summary.app.version.{name,code,build};
|
|
4578
|
-
* 顶层 versionName/versionCode 为历史兼容形态。 */
|
|
4579
|
-
function parsePackInfo(text, source) {
|
|
4580
|
-
try {
|
|
4581
|
-
const info = JSON.parse(text);
|
|
4582
|
-
const nested = info.summary?.app?.version;
|
|
4583
|
-
const versionName = nested?.name ?? info.versionName;
|
|
4584
|
-
const versionCode = nested?.code ?? info.versionCode;
|
|
4585
|
-
if (versionName || versionCode !== void 0) return {
|
|
4586
|
-
versionName,
|
|
4587
|
-
versionCode: String(versionCode ?? ""),
|
|
4588
|
-
source
|
|
4589
|
-
};
|
|
4590
|
-
} catch {}
|
|
4591
|
-
}
|
|
4592
|
-
/** APK:aapt2/aapt dump badging(versionCode/versionName) */
|
|
4593
|
-
function fromAapt(artifactPath) {
|
|
4594
|
-
const tools = [];
|
|
4595
|
-
const sdkDir = process.env.ANDROID_HOME ?? path.join(os.homedir(), "Library/Android/sdk");
|
|
4596
|
-
const btDir = path.join(sdkDir, "build-tools");
|
|
4597
|
-
if (fs.existsSync(btDir)) for (const bt of fs.readdirSync(btDir).sort().reverse()) for (const tool of ["aapt2", "aapt"]) {
|
|
4598
|
-
const p = path.join(btDir, bt, tool);
|
|
4599
|
-
if (fs.existsSync(p)) tools.push(p);
|
|
4600
|
-
}
|
|
4601
|
-
for (const tool of tools) {
|
|
4602
|
-
const r = spawnSync(tool, [
|
|
4603
|
-
"dump",
|
|
4604
|
-
"badging",
|
|
4605
|
-
artifactPath
|
|
4606
|
-
], {
|
|
4607
|
-
timeout: 1e4,
|
|
4608
|
-
encoding: "utf8"
|
|
4609
|
-
});
|
|
4610
|
-
if (r.error || r.status !== 0) continue;
|
|
4611
|
-
const name = /versionName='([^']*)'/.exec(r.stdout ?? "")?.[1];
|
|
4612
|
-
const code = /versionCode='(\d+)'/.exec(r.stdout ?? "")?.[1];
|
|
4613
|
-
if (name || code) return {
|
|
4614
|
-
versionName: name,
|
|
4615
|
-
versionCode: code,
|
|
4616
|
-
source: path.basename(tool)
|
|
4617
|
-
};
|
|
4618
|
-
}
|
|
4619
|
-
}
|
|
4620
|
-
//#endregion
|
|
4621
4387
|
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 };
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as FrameworkContext, O as UpdateProvider, _ as AppliftPlugin, a as knownFrameworkAdapters, c as nativeHarmonyAdapter, d as nativeIosFramework, f as rnAdapter, g as tauriFramework, h as tauriAdapter, i as flutterFramework, l as nativeHarmonyFramework, m as suggestFramework, n as electronFramework, o as nativeAndroidAdapter, p as rnFramework, r as flutterAdapter, s as nativeAndroidFramework, t as electronAdapter, u as nativeIosAdapter, w as DeliveryJudgement } from "../frameworks-
|
|
1
|
+
import { E as FrameworkContext, O as UpdateProvider, _ as AppliftPlugin, a as knownFrameworkAdapters, c as nativeHarmonyAdapter, d as nativeIosFramework, f as rnAdapter, g as tauriFramework, h as tauriAdapter, i as flutterFramework, l as nativeHarmonyFramework, m as suggestFramework, n as electronFramework, o as nativeAndroidAdapter, p as rnFramework, r as flutterAdapter, s as nativeAndroidFramework, t as electronAdapter, u as nativeIosAdapter, w as DeliveryJudgement } from "../frameworks-DJIt293o.mjs";
|
|
2
2
|
//#region src/plugins/dingtalk-notify.d.ts
|
|
3
3
|
interface DingtalkNotifyOptions {
|
|
4
4
|
webhook: string;
|
|
@@ -8,23 +8,18 @@ interface DingtalkNotifyOptions {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function dingtalkNotify(options: DingtalkNotifyOptions): AppliftPlugin;
|
|
10
10
|
//#endregion
|
|
11
|
-
//#region src/plugins/pushy-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type CliRunner = (args: string[], opts?: {
|
|
18
|
-
cwd?: string;
|
|
19
|
-
}) => Promise<CliResult>;
|
|
20
|
-
/** 默认 runner:spawn 全局 pushy,超时 10 分钟(bundle/upload 含网络大文件传输) */
|
|
21
|
-
export declare function spawnPushy(args: string[], opts?: {
|
|
22
|
-
cwd?: string;
|
|
23
|
-
}): Promise<CliResult>;
|
|
11
|
+
//#region src/plugins/pushy-api.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* pushy 服务端操作契约(全 OpenAPI 实现;注入点:测试 mock)。
|
|
14
|
+
* ppk 不在此层:热更包是构建产物,由调用方先行产出(--artifact 显式或 ppkPatterns 探测,
|
|
15
|
+
* 见 rn-update.ts)——applift 不 spawn 构建命令(架构不变量 1)。
|
|
16
|
+
*/
|
|
24
17
|
interface PushyCli {
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
/** 发布热更版本(version/create + 绑定目标原生版本),返回版本 id。
|
|
19
|
+
* deps 为服务端强校验所需(2026-09-17 后创建的应用要求 deps 里有 react-native-update
|
|
20
|
+
* >= 10.53.1——实测缺省被 400 拒绝),由调用方用 readDepVersions 从工程读取。
|
|
21
|
+
* 不上报 commit:实测带 commit(官方 getCommitInfo 同构形态)被 version/create 以
|
|
22
|
+
* 「上传凭据无效」拒绝(2026-10-21 实证,去 commit 即过);溯源走本地 history.jsonl */
|
|
28
23
|
publish(input: {
|
|
29
24
|
appId: number;
|
|
30
25
|
ppk: string;
|
|
@@ -33,11 +28,12 @@ interface PushyCli {
|
|
|
33
28
|
description?: string;
|
|
34
29
|
packageVersion: string;
|
|
35
30
|
rollout?: number;
|
|
31
|
+
deps: Record<string, string>;
|
|
36
32
|
}): Promise<{
|
|
37
33
|
versionId: number;
|
|
38
34
|
}>;
|
|
39
|
-
/**
|
|
40
|
-
uploadApp(appId: number, appFile: string): Promise<{
|
|
35
|
+
/** 上传原生基线(versionName 由调用方从产物读取后显式传入;deps 同 publish) */
|
|
36
|
+
uploadApp(appId: number, appFile: string, versionName: string, deps: Record<string, string>): Promise<{
|
|
41
37
|
packageId: number;
|
|
42
38
|
version: string;
|
|
43
39
|
}>;
|
|
@@ -55,7 +51,33 @@ interface PushyCli {
|
|
|
55
51
|
name: string;
|
|
56
52
|
}>>;
|
|
57
53
|
}
|
|
58
|
-
|
|
54
|
+
/** credentials.json 中的 pushy token 键名 */
|
|
55
|
+
export declare const PUSHY_TOKEN_KEY = "PUSHY_API_TOKEN";
|
|
56
|
+
/** pushy 官方 API 根(官方 CLI 默认端点簇的主端点) */
|
|
57
|
+
export declare const PUSHY_DEFAULT_BASE_URL = "https://update.reactnative.cn/api";
|
|
58
|
+
type FetchImpl = (url: string, init?: RequestInit) => Promise<Response>;
|
|
59
|
+
interface PushyApiOptions {
|
|
60
|
+
/** 服务端 API 根;缺省 pushy 官方,cresc 自建传如 https://cresc.example.com/api */
|
|
61
|
+
baseUrl?: string;
|
|
62
|
+
/** x-api-token(credentials.json 的 PUSHY_API_TOKEN) */
|
|
63
|
+
token: string;
|
|
64
|
+
/** 注入点:测试 mock(缺省 globalThis.fetch) */
|
|
65
|
+
fetchImpl?: FetchImpl;
|
|
66
|
+
/** 直传实现注入(缺省 node:http(s) 原生;测试 mock) */
|
|
67
|
+
httpUpload?: HttpUploadImpl;
|
|
68
|
+
}
|
|
69
|
+
/** 直传实现(node:http(s) 原生;测试注入 mock) */
|
|
70
|
+
type HttpUploadImpl = (method: "POST" | "PUT", url: string, headers: Record<string, string>, body: Buffer) => Promise<{
|
|
71
|
+
status: number;
|
|
72
|
+
}>;
|
|
73
|
+
export declare function nodeHttpUpload(method: "POST" | "PUT", url: string, headers: Record<string, string>, body: Buffer): Promise<{
|
|
74
|
+
status: number;
|
|
75
|
+
}>;
|
|
76
|
+
/** 项目直接依赖(dependencies+devDependencies)的实装版本映射(逐级向上找 node_modules,键排序)。
|
|
77
|
+
* 对照官方 dep-versions.js 同构:服务端按 deps["react-native-update"] 做热更兼容门禁与变更提示 */
|
|
78
|
+
export declare function readDepVersions(cwd: string): Record<string, string>;
|
|
79
|
+
/** OpenAPI 实现 PushyCli 的服务端操作(ppk 由调用方产出,见 PushyCli 契约注释) */
|
|
80
|
+
export declare function createPushyApi(opts: PushyApiOptions): PushyCli;
|
|
59
81
|
//#endregion
|
|
60
82
|
//#region src/plugins/rn-update.d.ts
|
|
61
83
|
interface RnUpdateOptions {
|
|
@@ -68,16 +90,21 @@ interface RnUpdateOptions {
|
|
|
68
90
|
/** 正式发布默认灰度比例(缺省 100 全量——promote 语义是"验证后发正式",灰度是显式选择;
|
|
69
91
|
* 优先级:--rollout 旗标 > 此配置 > 内置默认) */
|
|
70
92
|
defaultRollout?: number;
|
|
93
|
+
/** pushy/cresc 服务端 API 根(自建 cresc 传入,如 https://cresc.example.com/api);缺省 pushy 官方 */
|
|
94
|
+
serverUrl?: string;
|
|
95
|
+
/** 各平台 ppk 产物探测 pattern(相对项目根,glob);缺省 pushy 官方输出约定
|
|
96
|
+
* .pushy/output/{platform}.*.ppk。探测取 mtime 最新;--artifact 显式指定时跳过探测与护栏 */
|
|
97
|
+
ppkPatterns?: Partial<Record<"harmony" | "android" | "ios", string[]>>;
|
|
71
98
|
}
|
|
72
99
|
/**
|
|
73
100
|
* 判定引擎:diff 能否走热更。
|
|
74
101
|
* 五形态完备性:新原生实体必现于 原生目录/依赖(原生包)/补丁/构建配置/JS——前四判原生,纯 JS 判热更,其余灰区。
|
|
75
102
|
*/
|
|
76
103
|
export declare function judgeDelivery(diff: string[], ctx: FrameworkContext, opts: RnUpdateOptions): DeliveryJudgement;
|
|
77
|
-
/** provider 构造(cli
|
|
104
|
+
/** provider 构造(cli 注入点见 resolvePushyClient) */
|
|
78
105
|
export declare function makeRnUpdateProvider(opts: RnUpdateOptions, cli?: PushyCli): UpdateProvider;
|
|
79
|
-
/** rnUpdate 插件工厂:注册完整 provider(判定 +
|
|
80
|
-
* cli 注入点:测试 mock /
|
|
106
|
+
/** rnUpdate 插件工厂:注册完整 provider(判定 + beta 发布 + promote)+ 原生发版自动基线。
|
|
107
|
+
* cli 注入点:测试 mock / 特殊 transport;缺省 OpenAPI 客户端(token 来自 ctx.credentials)。 */
|
|
81
108
|
export declare function rnUpdate(opts?: RnUpdateOptions, cli?: PushyCli): AppliftPlugin;
|
|
82
109
|
//#endregion
|
|
83
|
-
export { type
|
|
110
|
+
export { type DingtalkNotifyOptions, type FetchImpl, type HttpUploadImpl, type PushyApiOptions, type PushyCli, type RnUpdateOptions, electronAdapter, electronFramework, flutterAdapter, flutterFramework, knownFrameworkAdapters, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, suggestFramework, tauriAdapter, tauriFramework };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { C as findArtifacts, D as collectReleaseDiff, _ as rnFramework, a as electronAdapter, b as tauriFramework, c as flutterFramework, d as nativeAndroidFramework, f as nativeHarmonyAdapter, g as rnAdapter, h as nativeIosFramework, i as redactText, l as knownFrameworkAdapters, m as nativeIosAdapter, n as readProjectVersions, o as electronFramework, p as nativeHarmonyFramework, s as flutterAdapter, t as readArtifactVersion, u as nativeAndroidAdapter, v as suggestFramework, y as tauriAdapter } from "../artifact-B2RCmWw9.mjs";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import nodeHttp from "node:http";
|
|
7
|
+
import nodeHttps from "node:https";
|
|
6
8
|
//#region src/plugins/dingtalk-notify.ts
|
|
7
9
|
/**
|
|
8
10
|
* 官方示例插件 —— 钉钉机器人通知。
|
|
@@ -45,119 +47,234 @@ function dingtalkNotify(options) {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
//#endregion
|
|
48
|
-
//#region src/plugins/pushy-
|
|
50
|
+
//#region src/plugins/pushy-api.ts
|
|
49
51
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* pushy OpenAPI HTTP 客户端(x-api-token 鉴权),替代原 spawn pushy CLI 的服务端操作。
|
|
53
|
+
* 端点与载荷对照 react-native-update-cli 2.29.3 源码(官方参考客户端)反推:
|
|
54
|
+
* publish POST /app/:id/version/create {name, hash, description} + POST /app/:id/binding
|
|
55
|
+
* uploadApp POST /upload {ext, appId, size} → 对象存储直传 → POST /app/:id/package/create {name, hash, buildTime}
|
|
56
|
+
* rebind GET /app/:id/package/list → POST /app/:id/binding {versionId, rollout, packageId}
|
|
57
|
+
* versions GET /app/:id/version/list?offset&limit(服务端封顶 limit=100,逐页取尽)
|
|
58
|
+
* 上传指令 POST /upload → {url, formData}(multipart 直传,formData.key 即 hash)
|
|
59
|
+
* 或自托管节点 {method:'PUT', key}(预签名 PUT 直传用户对象存储)
|
|
60
|
+
* ppk 不在此层:热更包是构建产物,由调用方先行产出(--artifact 显式或 ppkPatterns 探测,
|
|
61
|
+
* 见 rn-update.ts)——applift 不 spawn 构建命令(架构不变量 1)。
|
|
62
|
+
* cresc/自建服务差异仅在 baseUrl(同一套服务端代码),serverUrl 由 rnUpdate 配置注入。
|
|
63
|
+
* uploadApp 相对官方 CLI 的有意降级(记录在案):传原始产物而非 slim 包、不带 bundleHash/
|
|
64
|
+
* bytecodeVersion/bundleLocation(服务端无法将该基线用作 hermes-base 或 package-origin diff)、
|
|
65
|
+
* buildTime 为上传时刻而非包内提取、不校验包内 appId 与目标 app 一致(配错 appId 会传到别的 app——
|
|
66
|
+
* 由配置审查保证)、不做 maxSize 预检(超限撞对象存储错误)。需要 hermes-base 能力时在此补齐。
|
|
54
67
|
*/
|
|
55
|
-
/**
|
|
56
|
-
|
|
68
|
+
/** credentials.json 中的 pushy token 键名 */
|
|
69
|
+
const PUSHY_TOKEN_KEY = "PUSHY_API_TOKEN";
|
|
70
|
+
/** pushy 官方 API 根(官方 CLI 默认端点簇的主端点) */
|
|
71
|
+
const PUSHY_DEFAULT_BASE_URL = "https://update.reactnative.cn/api";
|
|
72
|
+
const UA = "react-native-update-cli/2.29.3";
|
|
73
|
+
async function query(f, baseUrl, token, api, init) {
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = await f(`${baseUrl}${api}`, {
|
|
77
|
+
method: init?.method ?? "GET",
|
|
78
|
+
headers: {
|
|
79
|
+
"User-Agent": UA,
|
|
80
|
+
"x-api-token": token,
|
|
81
|
+
...init?.body ? { "Content-Type": "application/json" } : {}
|
|
82
|
+
},
|
|
83
|
+
...init?.body ? { body: init.body } : {}
|
|
84
|
+
});
|
|
85
|
+
} catch (e) {
|
|
86
|
+
throw new Error(`pushy API 网络错误:${api}——${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
const text = await res.text();
|
|
89
|
+
let json;
|
|
90
|
+
try {
|
|
91
|
+
json = text ? JSON.parse(text) : void 0;
|
|
92
|
+
} catch {
|
|
93
|
+
if (res.ok) throw new Error(`pushy API ${api} 返回 200 但响应体非 JSON(${text.length} 字节,疑似网关劫持)`);
|
|
94
|
+
}
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const msg = json?.message || res.statusText || `HTTP ${res.status}`;
|
|
97
|
+
throw new Error(res.status === 401 ? `pushy API 鉴权失败(token 无效或过期):请更新 credentials.json 的 ${PUSHY_TOKEN_KEY}` : `pushy API ${api} 失败(HTTP ${res.status}):${msg}`);
|
|
98
|
+
}
|
|
99
|
+
return json ?? {};
|
|
100
|
+
}
|
|
101
|
+
/** 文件 part 的 Content-Type(对照官方 fileContentType 的扩展名表;未列出走 octet-stream) */
|
|
102
|
+
function fileContentType(fileName) {
|
|
103
|
+
switch (path.extname(fileName).toLowerCase()) {
|
|
104
|
+
case ".apk": return "application/vnd.android.package-archive";
|
|
105
|
+
case ".zip": return "application/zip";
|
|
106
|
+
case ".json":
|
|
107
|
+
case ".map": return "application/json";
|
|
108
|
+
default: return "application/octet-stream";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 对象存储直传,返回服务端侧 hash/key。
|
|
113
|
+
* Buffer 整体读入:ppk/原生基线在发版机场景为几十 MB 量级,换取 content-length 精确与实现简单
|
|
114
|
+
* (旧 CLI 流式是为进度条与代理改写,非容量需求)。不申请 chunked(响应分支少一半,
|
|
115
|
+
* ppk 增量优化留待 diffStatus 端点,见设计说明)。
|
|
116
|
+
*/
|
|
117
|
+
async function uploadFile(f, baseUrl, token, appId, file, upload) {
|
|
118
|
+
if (!fs.existsSync(file)) throw new Error(`pushy 上传文件不存在:${file}`);
|
|
119
|
+
const size = fs.statSync(file).size;
|
|
120
|
+
const instr = await query(f, baseUrl, token, "/upload", {
|
|
121
|
+
method: "POST",
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
ext: path.extname(file),
|
|
124
|
+
appId,
|
|
125
|
+
chunked: true,
|
|
126
|
+
size
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
const httpUpload = upload ?? nodeHttpUpload;
|
|
130
|
+
if (instr.method === "PUT") {
|
|
131
|
+
const putBody = fs.readFileSync(file);
|
|
132
|
+
const res = await httpUpload("PUT", instr.url, {
|
|
133
|
+
"content-length": String(size),
|
|
134
|
+
...instr.headers ?? {}
|
|
135
|
+
}, putBody);
|
|
136
|
+
if (res.status > 299) throw new Error(`pushy 基线直传失败(HTTP ${res.status}):${instr.url}`);
|
|
137
|
+
return instr.key;
|
|
138
|
+
}
|
|
139
|
+
const fields = Object.entries(instr.formData ?? {});
|
|
140
|
+
const boundary = `--------------------------${Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
|
|
141
|
+
let head = "";
|
|
142
|
+
for (const [name, value] of fields) head += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
|
|
143
|
+
head += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${path.basename(file)}"\r\nContent-Type: ${fileContentType(file)}\r\n\r\n`;
|
|
144
|
+
const body = Buffer.concat([
|
|
145
|
+
Buffer.from(head),
|
|
146
|
+
fs.readFileSync(file),
|
|
147
|
+
Buffer.from(`\r\n--${boundary}--\r\n`)
|
|
148
|
+
]);
|
|
149
|
+
const res = await httpUpload("POST", instr.url, {
|
|
150
|
+
accept: "*/*",
|
|
151
|
+
"user-agent": UA,
|
|
152
|
+
"content-type": `multipart/form-data;boundary=${boundary}`,
|
|
153
|
+
"content-length": String(body.length)
|
|
154
|
+
}, body);
|
|
155
|
+
if (res.status > 299) throw new Error(`pushy 文件直传失败(HTTP ${res.status}):${instr.url}`);
|
|
156
|
+
const key = fields.find(([name]) => name === "key")?.[1];
|
|
157
|
+
if (!key) throw new Error(`pushy /upload 指令缺 formData.key(服务端响应异常):${instr.url}`);
|
|
158
|
+
return key;
|
|
159
|
+
}
|
|
160
|
+
async function nodeHttpUpload(method, url, headers, body) {
|
|
161
|
+
const mod = url.startsWith("https:") ? nodeHttps : nodeHttp;
|
|
57
162
|
return new Promise((resolve, reject) => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), 6e5);
|
|
62
|
-
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
63
|
-
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
64
|
-
child.on("error", (e) => {
|
|
65
|
-
clearTimeout(timer);
|
|
66
|
-
reject(/* @__PURE__ */ new Error(`pushy 命令不可用(react-native-update-cli 未全局安装?):${e.message}`));
|
|
163
|
+
const req = mod.request(url, {
|
|
164
|
+
method,
|
|
165
|
+
headers
|
|
67
166
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
resolve({
|
|
71
|
-
code: code ?? 1,
|
|
72
|
-
stdout,
|
|
73
|
-
stderr
|
|
74
|
-
});
|
|
167
|
+
req.on("response", (res) => {
|
|
168
|
+
res.resume();
|
|
169
|
+
resolve({ status: res.statusCode ?? 0 });
|
|
75
170
|
});
|
|
171
|
+
req.on("error", (e) => reject(/* @__PURE__ */ new Error(`pushy 直传网络错误:${url}——${e.message}`)));
|
|
172
|
+
req.end(body);
|
|
76
173
|
});
|
|
77
174
|
}
|
|
78
|
-
|
|
79
|
-
|
|
175
|
+
/** packageVersion → packageId 精确匹配(对照 CLI update:pkg.name === packageVersion) */
|
|
176
|
+
async function bind(q, appId, versionId, packageVersion, rollout) {
|
|
177
|
+
const pkgs = await listAllPackages(q, appId);
|
|
178
|
+
const pkg = pkgs.find((p) => p.name === packageVersion);
|
|
179
|
+
if (!pkg) throw new Error(`pushy 未找到原生包版本 ${packageVersion}(appId=${appId},已有 ${pkgs.length} 个基线)——原生包尚未上传?先发一次原生版(stage:after 会自动上传基线)`);
|
|
180
|
+
await q(`/app/${appId}/binding`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
versionId,
|
|
184
|
+
rollout,
|
|
185
|
+
packageId: pkg.id
|
|
186
|
+
})
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function listAllPackages(q, appId) {
|
|
190
|
+
const out = [];
|
|
191
|
+
const limit = 100;
|
|
192
|
+
for (let offset = 0; offset < 1e4; offset += limit) {
|
|
193
|
+
const { data } = await q(`/app/${appId}/package/list?offset=${offset}&limit=${limit}`);
|
|
194
|
+
if (!data?.length) break;
|
|
195
|
+
out.push(...data);
|
|
196
|
+
if (data.length < limit) break;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
/** 项目直接依赖(dependencies+devDependencies)的实装版本映射(逐级向上找 node_modules,键排序)。
|
|
201
|
+
* 对照官方 dep-versions.js 同构:服务端按 deps["react-native-update"] 做热更兼容门禁与变更提示 */
|
|
202
|
+
function readDepVersions(cwd) {
|
|
203
|
+
let pkg;
|
|
204
|
+
try {
|
|
205
|
+
pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
206
|
+
} catch {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
const versions = {};
|
|
210
|
+
for (const dep of /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})])) {
|
|
211
|
+
let dir = path.resolve(cwd);
|
|
212
|
+
for (;;) {
|
|
213
|
+
try {
|
|
214
|
+
const installed = JSON.parse(fs.readFileSync(path.join(dir, "node_modules", dep, "package.json"), "utf8"));
|
|
215
|
+
if (typeof installed.version === "string") versions[dep] = installed.version;
|
|
216
|
+
break;
|
|
217
|
+
} catch {}
|
|
218
|
+
const parent = path.dirname(dir);
|
|
219
|
+
if (parent === dir) break;
|
|
220
|
+
dir = parent;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return Object.fromEntries(Object.keys(versions).sort().map((k) => [k, versions[k]]));
|
|
80
224
|
}
|
|
81
|
-
|
|
225
|
+
/** OpenAPI 实现 PushyCli 的服务端操作(ppk 由调用方产出,见 PushyCli 契约注释) */
|
|
226
|
+
function createPushyApi(opts) {
|
|
227
|
+
const baseUrl = opts.baseUrl ?? "https://update.reactnative.cn/api";
|
|
228
|
+
const f = opts.fetchImpl ?? fetch.bind(globalThis);
|
|
229
|
+
const q = (api, init) => query(f, baseUrl, opts.token, api, init);
|
|
82
230
|
return {
|
|
83
|
-
async bundle(platform, cwd) {
|
|
84
|
-
const r = await runner([
|
|
85
|
-
"bundle",
|
|
86
|
-
"--platform",
|
|
87
|
-
platform,
|
|
88
|
-
"--no-interactive"
|
|
89
|
-
], { cwd });
|
|
90
|
-
if (r.code !== 0) fail("bundle", r);
|
|
91
|
-
const m = r.stdout.match(/已生成\s+(\S+\.ppk)/);
|
|
92
|
-
if (!m) throw new Error(`pushy bundle 输出未含 ppk 路径:${r.stdout.trim()}`);
|
|
93
|
-
return m[1];
|
|
94
|
-
},
|
|
95
231
|
async publish(input) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (input.rollout !== void 0) args.push("--rollout", String(input.rollout));
|
|
110
|
-
args.push("--no-interactive");
|
|
111
|
-
const r = await runner(args);
|
|
112
|
-
if (r.code !== 0) fail("publish", r);
|
|
113
|
-
const m = (r.stdout + r.stderr).match(/已成功上传新热更包(id:\s*(\d+))/);
|
|
114
|
-
if (!m) throw new Error(`pushy publish 输出未含版本 id:${r.stdout.trim()}`);
|
|
115
|
-
return { versionId: Number(m[1]) };
|
|
232
|
+
const hash = await uploadFile(f, baseUrl, opts.token, input.appId, input.ppk, opts.httpUpload);
|
|
233
|
+
const { id } = await q(`/app/${input.appId}/version/create`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
name: input.name,
|
|
237
|
+
hash,
|
|
238
|
+
description: input.description ?? "",
|
|
239
|
+
metaInfo: "",
|
|
240
|
+
deps: input.deps
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
await bind(q, input.appId, id, input.packageVersion, input.rollout);
|
|
244
|
+
return { versionId: id };
|
|
116
245
|
},
|
|
117
|
-
async uploadApp(appId, appFile) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
246
|
+
async uploadApp(appId, appFile, versionName, deps) {
|
|
247
|
+
const hash = await uploadFile(f, baseUrl, opts.token, appId, appFile, opts.httpUpload);
|
|
248
|
+
const { id } = await q(`/app/${appId}/package/create`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
name: versionName,
|
|
252
|
+
hash,
|
|
253
|
+
buildTime: String(Date.now()),
|
|
254
|
+
deps
|
|
255
|
+
})
|
|
256
|
+
});
|
|
127
257
|
return {
|
|
128
|
-
packageId:
|
|
129
|
-
version:
|
|
258
|
+
packageId: id,
|
|
259
|
+
version: versionName
|
|
130
260
|
};
|
|
131
261
|
},
|
|
132
262
|
async rebind(input) {
|
|
133
|
-
|
|
134
|
-
"update",
|
|
135
|
-
"--versionId",
|
|
136
|
-
String(input.versionId),
|
|
137
|
-
"--appId",
|
|
138
|
-
String(input.appId),
|
|
139
|
-
"--packageVersion",
|
|
140
|
-
input.packageVersion
|
|
141
|
-
];
|
|
142
|
-
if (input.rollout !== void 0) args.push("--rollout", String(input.rollout));
|
|
143
|
-
args.push("--no-interactive");
|
|
144
|
-
const r = await runner(args);
|
|
145
|
-
if (r.code !== 0) fail("update", r);
|
|
263
|
+
await bind(q, input.appId, input.versionId, input.packageVersion, input.rollout);
|
|
146
264
|
},
|
|
147
265
|
async versions(appId) {
|
|
148
|
-
const r = await runner([
|
|
149
|
-
"versions",
|
|
150
|
-
"--appId",
|
|
151
|
-
String(appId),
|
|
152
|
-
"--no-interactive"
|
|
153
|
-
]);
|
|
154
|
-
if (r.code !== 0) fail("versions", r);
|
|
155
266
|
const out = [];
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
267
|
+
const limit = 100;
|
|
268
|
+
for (let offset = 0; offset < 1e4; offset += limit) {
|
|
269
|
+
const { data } = await q(`/app/${appId}/version/list?offset=${offset}&limit=${limit}`);
|
|
270
|
+
if (!data?.length) break;
|
|
271
|
+
for (const v of data) out.push({
|
|
272
|
+
id: v.id,
|
|
273
|
+
hash: v.hash ?? "",
|
|
274
|
+
name: v.name
|
|
275
|
+
});
|
|
276
|
+
if (data.length < limit) break;
|
|
277
|
+
}
|
|
161
278
|
return out;
|
|
162
279
|
}
|
|
163
280
|
};
|
|
@@ -166,8 +283,17 @@ function createPushyCli(runner = spawnPushy) {
|
|
|
166
283
|
//#region src/plugins/rn-update.ts
|
|
167
284
|
/**
|
|
168
285
|
* rn-update 插件:react-native-update(pushy/cresc/自建服务端)的 applift UpdateProvider 实现。
|
|
169
|
-
* 判定引擎为纯函数(五形态完备性见测试头注释);发布链路 = ppk
|
|
286
|
+
* 判定引擎为纯函数(五形态完备性见测试头注释);发布链路 = ppk 产物入口 / 测试通道 / promote / 原生基线。
|
|
287
|
+
* ppk 是构建产物(pushy bundle = metro + hermesc,完整构建链):由项目先行产出
|
|
288
|
+
* (npm script / CI),applift 只探测/接收——不 spawn 构建命令(架构不变量 1)。
|
|
170
289
|
*/
|
|
290
|
+
/** ppk 探测内置默认:pushy CLI 官方输出约定(tempDir .pushy + output/{platform}.${time}.ppk;
|
|
291
|
+
* cresc 同构,仅目录 .cresc.temp 不同——用 cresc 的项目在 ppkPatterns 覆盖) */
|
|
292
|
+
const DEFAULT_PPK_PATTERNS = {
|
|
293
|
+
harmony: [".pushy/output/harmony.*.ppk"],
|
|
294
|
+
android: [".pushy/output/android.*.ppk"],
|
|
295
|
+
ios: [".pushy/output/ios.*.ppk"]
|
|
296
|
+
};
|
|
171
297
|
/** 项目原生目录:任一命中即必须原生发版 */
|
|
172
298
|
const NATIVE_DIRS = [
|
|
173
299
|
"harmony/",
|
|
@@ -366,15 +492,15 @@ function judgeDelivery(diff, ctx, opts) {
|
|
|
366
492
|
};
|
|
367
493
|
}
|
|
368
494
|
/** pushy 官方测试包约定:与正式同源码、仅版本号偏移(major+1000,如 1.6.0→1001.6.0) */
|
|
369
|
-
const
|
|
495
|
+
const BETA_VERSION_OFFSET = 1e3;
|
|
370
496
|
/** 正式 promote 的内置默认灰度比例(100 = 全量)。promote 语义是"测试通道已验证、发正式",
|
|
371
497
|
* 全量是常态、灰度是显式选择(--rollout 或 rnUpdate({ defaultRollout })),不藏在配置后面 */
|
|
372
498
|
const DEFAULT_ROLLOUT = 100;
|
|
373
499
|
/** 生成测试包偏移版本号(major+1000);非数字开头版本号无法偏移,显式报错 */
|
|
374
|
-
function
|
|
500
|
+
function betaPackageVersion(versionName) {
|
|
375
501
|
const m = /^(\d+)(\..+)?$/.exec(versionName);
|
|
376
|
-
if (!m) throw new Error(`versionName「${versionName}」非 X.Y.Z 形态,无法按 major+${
|
|
377
|
-
return `${Number(m[1]) +
|
|
502
|
+
if (!m) throw new Error(`versionName「${versionName}」非 X.Y.Z 形态,无法按 major+${BETA_VERSION_OFFSET} 生成测试包偏移版本`);
|
|
503
|
+
return `${Number(m[1]) + BETA_VERSION_OFFSET}${m[2] ?? ""}`;
|
|
378
504
|
}
|
|
379
505
|
function platformAppId(opts, platform) {
|
|
380
506
|
const id = opts[platform];
|
|
@@ -397,8 +523,68 @@ function nextPatchNumber(names, versionName) {
|
|
|
397
523
|
}
|
|
398
524
|
return max + 1;
|
|
399
525
|
}
|
|
400
|
-
/**
|
|
401
|
-
|
|
526
|
+
/**
|
|
527
|
+
* 客户端解析:注入了 cli(测试 mock / 特殊 transport)则直用;缺省按凭据构造 OpenAPI 客户端
|
|
528
|
+
* (token 来自 PUSHY_API_TOKEN,bundle 始终走本地 pushy CLI)。
|
|
529
|
+
* fail-fast:token 缺失在本地 bundle 之前报错(不打无谓的 metro 包)。
|
|
530
|
+
*/
|
|
531
|
+
function resolvePushyClient(opts, cli, credentials) {
|
|
532
|
+
if (cli) return cli;
|
|
533
|
+
const token = credentials?.[PUSHY_TOKEN_KEY];
|
|
534
|
+
if (!token) throw new Error(`pushy 热更缺少凭据 ${PUSHY_TOKEN_KEY}:在 ~/.applift/credentials.json 对应 profile 配置(pushy 网页控制台 → API Token 页创建;CI 可用同名环境变量覆盖)`);
|
|
535
|
+
return createPushyApi({
|
|
536
|
+
baseUrl: opts.serverUrl,
|
|
537
|
+
token
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* ppk 产物入口:--artifact 显式 > ppkPatterns 探测(mtime 最新)。
|
|
542
|
+
* 探测路径强制新鲜度护栏;显式 --artifact 是用户声明「我知道我在发什么」,跳过护栏
|
|
543
|
+
* (与 --delivery ota 的显式覆盖语义一致,同原生 --artifact 不校验产物新旧)。
|
|
544
|
+
*/
|
|
545
|
+
async function resolvePpk(opts, ctx) {
|
|
546
|
+
if (ctx.artifact) return ctx.artifact;
|
|
547
|
+
const platform = ctx.platform;
|
|
548
|
+
const patterns = opts.ppkPatterns?.[platform] ?? DEFAULT_PPK_PATTERNS[platform];
|
|
549
|
+
const ppk = findArtifacts(ctx.cwd, patterns)[0];
|
|
550
|
+
if (!ppk) throw new Error(`未探测到 ${platform} 的 ppk 热更包(${patterns.join(", ")})——ppk 是构建产物,请先产出(如 npm run ppk:${platform} = pushy bundle --platform ${platform} --no-interactive)再 applift beta,或用 --artifact 显式指定 ppk 路径`);
|
|
551
|
+
await assertPpkFresh(ctx.cwd, ppk);
|
|
552
|
+
return ppk;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* 新鲜度护栏:工作区未发布改动(锚点..当前,与判定同口径 collectReleaseDiff)的最新文件
|
|
556
|
+
* mtime 晚于 ppk → 报错——防止忘重新 bundle 把旧 JS 发给测试设备。
|
|
557
|
+
* 无锚点/无 diff/文件已删除(无 mtime 可比)不拦。
|
|
558
|
+
*/
|
|
559
|
+
async function assertPpkFresh(cwd, ppk) {
|
|
560
|
+
const diff = await collectReleaseDiff(cwd);
|
|
561
|
+
if (!diff) return;
|
|
562
|
+
let newest = 0;
|
|
563
|
+
let newestFile = "";
|
|
564
|
+
for (const f of diff) try {
|
|
565
|
+
const m = fs.statSync(path.join(cwd, f)).mtimeMs;
|
|
566
|
+
if (m > newest) {
|
|
567
|
+
newest = m;
|
|
568
|
+
newestFile = f;
|
|
569
|
+
}
|
|
570
|
+
} catch {}
|
|
571
|
+
if (newest > 0 && fs.statSync(ppk).mtimeMs < newest) throw new Error(`ppk(${path.basename(ppk)})早于最新代码改动(${newestFile})——疑似忘重新 bundle,请重新产出 ppk 后再发;确认无误可 --artifact 显式指定跳过此护栏`);
|
|
572
|
+
}
|
|
573
|
+
/** 发布新热更的公共骨架:客户端/appId/版本号/ppk 探测 + 服务端取号(publishToBeta/publishToProd 共用) */
|
|
574
|
+
async function preparePublish(opts, cli, ctx) {
|
|
575
|
+
const client = resolvePushyClient(opts, cli, ctx.credentials);
|
|
576
|
+
const appId = platformAppId(opts, ctx.platform);
|
|
577
|
+
const versionName = currentVersionName(ctx);
|
|
578
|
+
return {
|
|
579
|
+
client,
|
|
580
|
+
appId,
|
|
581
|
+
versionName,
|
|
582
|
+
ppk: await resolvePpk(opts, ctx),
|
|
583
|
+
name: `${versionName}-patch.${nextPatchNumber(await client.versions(appId), versionName)}`
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/** provider 构造(cli 注入点见 resolvePushyClient) */
|
|
587
|
+
function makeRnUpdateProvider(opts, cli) {
|
|
402
588
|
return {
|
|
403
589
|
id: "rn-update",
|
|
404
590
|
frameworks: ["rn"],
|
|
@@ -408,40 +594,53 @@ function makeRnUpdateProvider(opts, cli = createPushyCli()) {
|
|
|
408
594
|
"ios"
|
|
409
595
|
].filter((k) => typeof opts[k] === "number"),
|
|
410
596
|
judge: (diff, ctx) => judgeDelivery(diff, ctx, opts),
|
|
411
|
-
async
|
|
412
|
-
const appId =
|
|
413
|
-
const versionName = currentVersionName(ctx);
|
|
414
|
-
const name = `${versionName}-patch.${nextPatchNumber(await cli.versions(appId), versionName)}`;
|
|
415
|
-
const ppk = await cli.bundle(ctx.platform, ctx.cwd);
|
|
597
|
+
async publishToBeta(ctx) {
|
|
598
|
+
const { client, appId, versionName, ppk, name } = await preparePublish(opts, cli, ctx);
|
|
416
599
|
return {
|
|
417
600
|
versionName: name,
|
|
418
|
-
versionId: (await
|
|
601
|
+
versionId: (await client.publish({
|
|
419
602
|
appId,
|
|
420
603
|
ppk,
|
|
421
604
|
platform: ctx.platform,
|
|
422
605
|
name,
|
|
423
606
|
description: ctx.note,
|
|
424
|
-
packageVersion:
|
|
607
|
+
packageVersion: betaPackageVersion(versionName),
|
|
608
|
+
deps: readDepVersions(ctx.cwd)
|
|
425
609
|
})).versionId
|
|
426
610
|
};
|
|
427
611
|
},
|
|
612
|
+
async publishToProd(ctx) {
|
|
613
|
+
const { client, appId, versionName, ppk, name } = await preparePublish(opts, cli, ctx);
|
|
614
|
+
const rollout = ctx.rollout ?? opts.defaultRollout ?? DEFAULT_ROLLOUT;
|
|
615
|
+
return {
|
|
616
|
+
versionName: name,
|
|
617
|
+
versionId: (await client.publish({
|
|
618
|
+
appId,
|
|
619
|
+
ppk,
|
|
620
|
+
platform: ctx.platform,
|
|
621
|
+
name,
|
|
622
|
+
description: ctx.note,
|
|
623
|
+
packageVersion: versionName,
|
|
624
|
+
rollout,
|
|
625
|
+
deps: readDepVersions(ctx.cwd)
|
|
626
|
+
})).versionId,
|
|
627
|
+
...rollout !== void 0 ? { rollout } : {}
|
|
628
|
+
};
|
|
629
|
+
},
|
|
428
630
|
async promoteToProd(ctx) {
|
|
631
|
+
const client = resolvePushyClient(opts, cli, ctx.credentials);
|
|
429
632
|
const appId = platformAppId(opts, ctx.platform);
|
|
430
633
|
const versionName = currentVersionName(ctx);
|
|
431
|
-
const
|
|
432
|
-
const latest = existing.map((v) => {
|
|
634
|
+
const latest = (await client.versions(appId)).map((v) => {
|
|
433
635
|
const m = v.name.match(new RegExp(`^${versionName.replace(/\./g, "\\.")}-patch\\.(\\d+)$`));
|
|
434
636
|
return m ? {
|
|
435
637
|
...v,
|
|
436
638
|
n: Number(m[1])
|
|
437
639
|
} : null;
|
|
438
640
|
}).filter((x) => x !== null).sort((a, b) => b.n - a.n)[0];
|
|
439
|
-
if (!latest)
|
|
440
|
-
const patchNames = existing.map((v) => v.name).filter((n) => /-patch\.\d+$/.test(n)).slice(0, 5);
|
|
441
|
-
throw new Error(`测试通道没有 ${versionName}-patch.N 热更版本可供 promote${patchNames.length > 0 ? `(现存:${patchNames.join(", ")}${existing.length > 5 ? " …" : ""})` : ""}:先 applift beta 发布测试热更并验证`);
|
|
442
|
-
}
|
|
641
|
+
if (!latest) return null;
|
|
443
642
|
const rollout = ctx.rollout ?? opts.defaultRollout ?? DEFAULT_ROLLOUT;
|
|
444
|
-
await
|
|
643
|
+
await client.rebind({
|
|
445
644
|
appId,
|
|
446
645
|
versionId: latest.id,
|
|
447
646
|
packageVersion: versionName,
|
|
@@ -456,22 +655,24 @@ function makeRnUpdateProvider(opts, cli = createPushyCli()) {
|
|
|
456
655
|
}
|
|
457
656
|
/** 渠道 → 平台映射(热更已接入的平台;基线上传按此路由) */
|
|
458
657
|
const CHANNEL_PLATFORM = { agc: "harmony" };
|
|
459
|
-
/** rnUpdate 插件工厂:注册完整 provider(判定 +
|
|
460
|
-
* cli 注入点:测试 mock /
|
|
461
|
-
function rnUpdate(opts = {}, cli
|
|
658
|
+
/** rnUpdate 插件工厂:注册完整 provider(判定 + beta 发布 + promote)+ 原生发版自动基线。
|
|
659
|
+
* cli 注入点:测试 mock / 特殊 transport;缺省 OpenAPI 客户端(token 来自 ctx.credentials)。 */
|
|
660
|
+
function rnUpdate(opts = {}, cli) {
|
|
462
661
|
const provider = makeRnUpdateProvider(opts, cli);
|
|
463
662
|
return {
|
|
464
663
|
name: "rn-update",
|
|
465
664
|
"update:register"(providers) {
|
|
466
665
|
providers.push(provider);
|
|
467
666
|
},
|
|
468
|
-
async "stage:after"(result) {
|
|
667
|
+
async "stage:after"(result, hookCtx) {
|
|
469
668
|
const platform = CHANNEL_PLATFORM[result.channel];
|
|
470
669
|
if (!result.ok || !platform || !result.artifactPath) return;
|
|
471
670
|
const appId = opts[platform];
|
|
472
671
|
if (typeof appId !== "number") return;
|
|
473
672
|
try {
|
|
474
|
-
|
|
673
|
+
const versionName = readArtifactVersion(result.artifactPath)?.versionName;
|
|
674
|
+
if (!versionName) throw new Error(`未能从产物读出 versionName:${result.artifactPath}(readArtifactVersion 不支持该格式?)`);
|
|
675
|
+
await resolvePushyClient(opts, cli, hookCtx.credentials).uploadApp(appId, result.artifactPath, versionName, readDepVersions(hookCtx.cwd));
|
|
475
676
|
} catch (e) {
|
|
476
677
|
process.stderr.write(`⚠ pushy 基线上传失败(请人工补传):${e.message}\n`);
|
|
477
678
|
}
|
|
@@ -479,4 +680,4 @@ function rnUpdate(opts = {}, cli = createPushyCli()) {
|
|
|
479
680
|
};
|
|
480
681
|
}
|
|
481
682
|
//#endregion
|
|
482
|
-
export {
|
|
683
|
+
export { PUSHY_DEFAULT_BASE_URL, PUSHY_TOKEN_KEY, createPushyApi, dingtalkNotify, electronAdapter, electronFramework, flutterAdapter, flutterFramework, judgeDelivery, knownFrameworkAdapters, makeRnUpdateProvider, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, nodeHttpUpload, readDepVersions, rnAdapter, rnFramework, rnUpdate, suggestFramework, tauriAdapter, tauriFramework };
|