@applift/core 0.7.1 → 0.8.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.
@@ -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
- export { readJson as C, findArtifacts as S, subdirs as T, suggestFramework as _, electronFramework as a, deps as b, knownFrameworkAdapters as c, nativeHarmonyAdapter as d, nativeHarmonyFramework as f, rnFramework as g, rnAdapter as h, electronAdapter as i, nativeAndroidAdapter as l, nativeIosFramework as m, writeProjectVersions as n, flutterAdapter as o, nativeIosAdapter as p, redactText as r, flutterFramework as s, readProjectVersions as t, nativeAndroidFramework as u, tauriAdapter as v, resolveFramework as w, detectBundleIds as x, tauriFramework as y };
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
- /** beta 热更路径:构建热更包并发到测试通道(测试设备验证用) */
355
- publishToTest(ctx: PublishContext): Promise<{
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
- /** release 热更路径:测试通道最新已验证热更 promote 到正式(重绑正式原生版本 + 灰度) */
360
- promoteToProd(ctx: PublishContext): Promise<{
361
+ /** beta 热更路径(可选):热更包发测试通道(测试设备验证用);无内测流程的 provider 不实现
362
+ * (beta 判定 ota 时报错引导直接 release) */
363
+ publishToBeta?(ctx: PublishContext): Promise<{
361
364
  versionName: string;
362
- rollout?: number;
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-D83HVg8t.mjs";
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 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
+ 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 };
@@ -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-D83HVg8t.mjs";
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,14 @@ interface DingtalkNotifyOptions {
8
8
  }
9
9
  export declare function dingtalkNotify(options: DingtalkNotifyOptions): AppliftPlugin;
10
10
  //#endregion
11
- //#region src/plugins/pushy-cli.d.ts
12
- interface CliResult {
13
- code: number;
14
- stdout: string;
15
- stderr: string;
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
- /** 构建热更包,返回 ppk 路径 */
26
- bundle(platform: string, cwd?: string): Promise<string>;
27
- /** 发布热更版本(publish),返回版本 id */
18
+ /** 发布热更版本(version/create + 绑定目标原生版本),返回版本 id */
28
19
  publish(input: {
29
20
  appId: number;
30
21
  ppk: string;
@@ -36,8 +27,8 @@ interface PushyCli {
36
27
  }): Promise<{
37
28
  versionId: number;
38
29
  }>;
39
- /** 上传原生基线(基线版本号由服务端从包内读取) */
40
- uploadApp(appId: number, appFile: string): Promise<{
30
+ /** 上传原生基线(versionName 由调用方从产物读取后显式传入) */
31
+ uploadApp(appId: number, appFile: string, versionName: string): Promise<{
41
32
  packageId: number;
42
33
  version: string;
43
34
  }>;
@@ -55,7 +46,21 @@ interface PushyCli {
55
46
  name: string;
56
47
  }>>;
57
48
  }
58
- export declare function createPushyCli(runner?: CliRunner): PushyCli;
49
+ /** credentials.json 中的 pushy token 键名 */
50
+ export declare const PUSHY_TOKEN_KEY = "PUSHY_API_TOKEN";
51
+ /** pushy 官方 API 根(官方 CLI 默认端点簇的主端点) */
52
+ export declare const PUSHY_DEFAULT_BASE_URL = "https://update.reactnative.cn/api";
53
+ type FetchImpl = (url: string, init?: RequestInit) => Promise<Response>;
54
+ interface PushyApiOptions {
55
+ /** 服务端 API 根;缺省 pushy 官方,cresc 自建传如 https://cresc.example.com/api */
56
+ baseUrl?: string;
57
+ /** x-api-token(credentials.json 的 PUSHY_API_TOKEN) */
58
+ token: string;
59
+ /** 注入点:测试 mock(缺省 globalThis.fetch) */
60
+ fetchImpl?: FetchImpl;
61
+ }
62
+ /** OpenAPI 实现 PushyCli 的服务端操作(ppk 由调用方产出,见 PushyCli 契约注释) */
63
+ export declare function createPushyApi(opts: PushyApiOptions): PushyCli;
59
64
  //#endregion
60
65
  //#region src/plugins/rn-update.d.ts
61
66
  interface RnUpdateOptions {
@@ -65,18 +70,24 @@ interface RnUpdateOptions {
65
70
  ios?: number;
66
71
  /** 补丁目录(patch-package 约定默认 patches/,项目可覆盖) */
67
72
  patchDir?: string;
68
- /** 正式发布默认灰度比例 */
73
+ /** 正式发布默认灰度比例(缺省 100 全量——promote 语义是"验证后发正式",灰度是显式选择;
74
+ * 优先级:--rollout 旗标 > 此配置 > 内置默认) */
69
75
  defaultRollout?: number;
76
+ /** pushy/cresc 服务端 API 根(自建 cresc 传入,如 https://cresc.example.com/api);缺省 pushy 官方 */
77
+ serverUrl?: string;
78
+ /** 各平台 ppk 产物探测 pattern(相对项目根,glob);缺省 pushy 官方输出约定
79
+ * .pushy/output/{platform}.*.ppk。探测取 mtime 最新;--artifact 显式指定时跳过探测与护栏 */
80
+ ppkPatterns?: Partial<Record<"harmony" | "android" | "ios", string[]>>;
70
81
  }
71
82
  /**
72
83
  * 判定引擎:diff 能否走热更。
73
84
  * 五形态完备性:新原生实体必现于 原生目录/依赖(原生包)/补丁/构建配置/JS——前四判原生,纯 JS 判热更,其余灰区。
74
85
  */
75
86
  export declare function judgeDelivery(diff: string[], ctx: FrameworkContext, opts: RnUpdateOptions): DeliveryJudgement;
76
- /** provider 构造(cli 注入点:测试 mock / 将来直连 API) */
87
+ /** provider 构造(cli 注入点见 resolvePushyClient) */
77
88
  export declare function makeRnUpdateProvider(opts: RnUpdateOptions, cli?: PushyCli): UpdateProvider;
78
- /** rnUpdate 插件工厂:注册完整 provider(判定 + 测试发布 + promote)+ 原生发版自动基线。
79
- * cli 注入点:测试 mock / 将来进程内直连 API。 */
89
+ /** rnUpdate 插件工厂:注册完整 provider(判定 + beta 发布 + promote)+ 原生发版自动基线。
90
+ * cli 注入点:测试 mock / 特殊 transport;缺省 OpenAPI 客户端(token 来自 ctx.credentials)。 */
80
91
  export declare function rnUpdate(opts?: RnUpdateOptions, cli?: PushyCli): AppliftPlugin;
81
92
  //#endregion
82
- export { type CliResult, type CliRunner, type DingtalkNotifyOptions, type PushyCli, type RnUpdateOptions, electronAdapter, electronFramework, flutterAdapter, flutterFramework, knownFrameworkAdapters, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, suggestFramework, tauriAdapter, tauriFramework };
93
+ export { type DingtalkNotifyOptions, type FetchImpl, type PushyApiOptions, type PushyCli, type RnUpdateOptions, electronAdapter, electronFramework, flutterAdapter, flutterFramework, knownFrameworkAdapters, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, suggestFramework, tauriAdapter, tauriFramework };
@@ -1,8 +1,8 @@
1
- import { _ as suggestFramework, a as electronFramework, c as knownFrameworkAdapters, d as nativeHarmonyAdapter, f as nativeHarmonyFramework, g as rnFramework, h as rnAdapter, i as electronAdapter, l as nativeAndroidAdapter, m as nativeIosFramework, o as flutterAdapter, p as nativeIosAdapter, r as redactText, s as flutterFramework, t as readProjectVersions, u as nativeAndroidFramework, v as tauriAdapter, y as tauriFramework } from "../project-4_orxby_.mjs";
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 { spawn, spawnSync } from "node:child_process";
5
+ import { spawnSync } from "node:child_process";
6
6
  //#region src/plugins/dingtalk-notify.ts
7
7
  /**
8
8
  * 官方示例插件 —— 钉钉机器人通知。
@@ -45,119 +45,192 @@ function dingtalkNotify(options) {
45
45
  };
46
46
  }
47
47
  //#endregion
48
- //#region src/plugins/pushy-cli.ts
48
+ //#region src/plugins/pushy-api.ts
49
49
  /**
50
- * 不 require 其内部 lib(无公开契约);cresc/自建服务的差异在服务端 URL,CLI 参数面一致。
51
- * 输出解析正则按 react-native-update-cli 2.22.9 实测文案编写(源码 i18n 硬编码:pushy 恒中文、cresc 恒英文,
52
- * 不随系统 locale);CLI 升级改文案会显式抛错而非静默,升级后请跑 pushy-cli.test.ts 的真实输出回归。
53
- * cresc 二进制输出为英文,本封装不适用——换 cresc 应另写 provider 插件而非复用本封装。
50
+ * pushy OpenAPI HTTP 客户端(x-api-token 鉴权),替代原 spawn pushy CLI 的服务端操作。
51
+ * 端点与载荷对照 react-native-update-cli 2.29.3 源码(官方参考客户端)反推:
52
+ * publish POST /app/:id/version/create {name, hash, description} + POST /app/:id/binding
53
+ * uploadApp POST /upload {ext, appId, size} → 对象存储直传 → POST /app/:id/package/create {name, hash, buildTime}
54
+ * rebind GET /app/:id/package/list → POST /app/:id/binding {versionId, rollout, packageId}
55
+ * versions GET /app/:id/version/list?offset&limit(服务端封顶 limit=100,逐页取尽)
56
+ * 上传指令 POST /upload → {url, formData}(multipart 直传,formData.key 即 hash)
57
+ * 或自托管节点 {method:'PUT', key}(预签名 PUT 直传用户对象存储)
58
+ * ppk 不在此层:热更包是构建产物,由调用方先行产出(--artifact 显式或 ppkPatterns 探测,
59
+ * 见 rn-update.ts)——applift 不 spawn 构建命令(架构不变量 1)。
60
+ * cresc/自建服务差异仅在 baseUrl(同一套服务端代码),serverUrl 由 rnUpdate 配置注入。
61
+ * uploadApp 相对官方 CLI 的有意降级(记录在案):传原始产物而非 slim 包、不带 bundleHash/
62
+ * bytecodeVersion/bundleLocation(服务端无法将该基线用作 hermes-base 或 package-origin diff)、
63
+ * buildTime 为上传时刻而非包内提取、不校验包内 appId 与目标 app 一致(配错 appId 会传到别的 app——
64
+ * 由配置审查保证)、不做 maxSize 预检(超限撞对象存储错误)。需要 hermes-base 能力时在此补齐。
54
65
  */
55
- /** 默认 runner:spawn 全局 pushy,超时 10 分钟(bundle/upload 含网络大文件传输) */
56
- function spawnPushy(args, opts) {
57
- return new Promise((resolve, reject) => {
58
- const child = spawn("pushy", args, { cwd: opts?.cwd });
59
- let stdout = "";
60
- let stderr = "";
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}`));
66
+ /** credentials.json 中的 pushy token 键名 */
67
+ const PUSHY_TOKEN_KEY = "PUSHY_API_TOKEN";
68
+ /** pushy 官方 API 根(官方 CLI 默认端点簇的主端点) */
69
+ const PUSHY_DEFAULT_BASE_URL = "https://update.reactnative.cn/api";
70
+ const UA = "applift/pushy-openapi";
71
+ async function query(f, baseUrl, token, api, init) {
72
+ let res;
73
+ try {
74
+ res = await f(`${baseUrl}${api}`, {
75
+ method: init?.method ?? "GET",
76
+ headers: {
77
+ "User-Agent": UA,
78
+ "x-api-token": token,
79
+ ...init?.body ? { "Content-Type": "application/json" } : {}
80
+ },
81
+ ...init?.body ? { body: init.body } : {}
67
82
  });
68
- child.on("close", (code) => {
69
- clearTimeout(timer);
70
- resolve({
71
- code: code ?? 1,
72
- stdout,
73
- stderr
74
- });
83
+ } catch (e) {
84
+ throw new Error(`pushy API 网络错误:${api}——${e.message}`);
85
+ }
86
+ const text = await res.text();
87
+ let json;
88
+ try {
89
+ json = text ? JSON.parse(text) : void 0;
90
+ } catch {
91
+ if (res.ok) throw new Error(`pushy API ${api} 返回 200 但响应体非 JSON(${text.length} 字节,疑似网关劫持)`);
92
+ }
93
+ if (!res.ok) {
94
+ const msg = json?.message || res.statusText || `HTTP ${res.status}`;
95
+ throw new Error(res.status === 401 ? `pushy API 鉴权失败(token 无效或过期):请更新 credentials.json 的 ${PUSHY_TOKEN_KEY}` : `pushy API ${api} 失败(HTTP ${res.status}):${msg}`);
96
+ }
97
+ return json;
98
+ }
99
+ /** 文件 part 的 Content-Type(对照官方 fileContentType 的扩展名表;未列出走 octet-stream) */
100
+ function fileContentType(fileName) {
101
+ switch (path.extname(fileName).toLowerCase()) {
102
+ case ".apk": return "application/vnd.android.package-archive";
103
+ case ".zip": return "application/zip";
104
+ case ".json":
105
+ case ".map": return "application/json";
106
+ default: return "application/octet-stream";
107
+ }
108
+ }
109
+ /**
110
+ * 对象存储直传,返回服务端侧 hash/key。
111
+ * Buffer 整体读入:ppk/原生基线在发版机场景为几十 MB 量级,换取 content-length 精确与实现简单
112
+ * (旧 CLI 流式是为进度条与代理改写,非容量需求)。不申请 chunked(响应分支少一半,
113
+ * ppk 增量优化留待 diffStatus 端点,见设计说明)。
114
+ */
115
+ async function uploadFile(f, baseUrl, token, appId, file) {
116
+ if (!fs.existsSync(file)) throw new Error(`pushy 上传文件不存在:${file}`);
117
+ const size = fs.statSync(file).size;
118
+ const instr = await query(f, baseUrl, token, "/upload", {
119
+ method: "POST",
120
+ body: JSON.stringify({
121
+ ext: path.extname(file),
122
+ appId,
123
+ size
124
+ })
125
+ });
126
+ if (instr.method === "PUT") {
127
+ const res = await f(instr.url, {
128
+ method: "PUT",
129
+ body: fs.readFileSync(file),
130
+ headers: {
131
+ "content-length": String(size),
132
+ ...instr.headers ?? {}
133
+ }
75
134
  });
135
+ if (!res.ok) throw new Error(`pushy 基线直传失败(HTTP ${res.status}):${res.statusText || instr.url}`);
136
+ return instr.key;
137
+ }
138
+ const fields = Object.entries(instr.formData ?? {});
139
+ const boundary = `--------------------------${Array.from({ length: 24 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
140
+ let head = "";
141
+ for (const [name, value] of fields) head += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
142
+ head += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${path.basename(file)}"\r\nContent-Type: ${fileContentType(file)}\r\n\r\n`;
143
+ const body = Buffer.concat([
144
+ Buffer.from(head),
145
+ fs.readFileSync(file),
146
+ Buffer.from(`\r\n--${boundary}--\r\n`)
147
+ ]);
148
+ const res = await f(instr.url, {
149
+ method: "POST",
150
+ body,
151
+ headers: { "content-type": `multipart/form-data;boundary=${boundary}` }
152
+ });
153
+ if (!res.ok) throw new Error(`pushy 文件直传失败(HTTP ${res.status}):${res.statusText || instr.url}`);
154
+ const key = fields.find(([name]) => name === "key")?.[1];
155
+ if (!key) throw new Error(`pushy /upload 指令缺 formData.key(服务端响应异常):${instr.url}`);
156
+ return key;
157
+ }
158
+ /** packageVersion → packageId 精确匹配(对照 CLI update:pkg.name === packageVersion) */
159
+ async function bind(q, appId, versionId, packageVersion, rollout) {
160
+ const pkgs = await listAllPackages(q, appId);
161
+ const pkg = pkgs.find((p) => p.name === packageVersion);
162
+ if (!pkg) throw new Error(`pushy 未找到原生包版本 ${packageVersion}(appId=${appId},已有 ${pkgs.length} 个基线)——原生包尚未上传?先发一次原生版(stage:after 会自动上传基线)`);
163
+ await q(`/app/${appId}/binding`, {
164
+ method: "POST",
165
+ body: JSON.stringify({
166
+ versionId,
167
+ rollout,
168
+ packageId: pkg.id
169
+ })
76
170
  });
77
171
  }
78
- function fail(cmd, r) {
79
- throw new Error(`pushy ${cmd} 失败(exit ${r.code}):${(r.stderr || r.stdout).trim()}`);
172
+ async function listAllPackages(q, appId) {
173
+ const out = [];
174
+ const limit = 100;
175
+ for (let offset = 0; offset < 1e4; offset += limit) {
176
+ const { data } = await q(`/app/${appId}/package/list?offset=${offset}&limit=${limit}`);
177
+ if (!data?.length) break;
178
+ out.push(...data);
179
+ if (data.length < limit) break;
180
+ }
181
+ return out;
80
182
  }
81
- function createPushyCli(runner = spawnPushy) {
183
+ /** OpenAPI 实现 PushyCli 的服务端操作(ppk 由调用方产出,见 PushyCli 契约注释) */
184
+ function createPushyApi(opts) {
185
+ const baseUrl = opts.baseUrl ?? "https://update.reactnative.cn/api";
186
+ const f = opts.fetchImpl ?? fetch.bind(globalThis);
187
+ const q = (api, init) => query(f, baseUrl, opts.token, api, init);
82
188
  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
189
  async publish(input) {
96
- const args = [
97
- "publish",
98
- input.ppk,
99
- "--platform",
100
- input.platform,
101
- "--appId",
102
- String(input.appId),
103
- "--name",
104
- input.name,
105
- "--packageVersion",
106
- input.packageVersion
107
- ];
108
- if (input.description) args.push("--description", input.description);
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]) };
190
+ const hash = await uploadFile(f, baseUrl, opts.token, input.appId, input.ppk);
191
+ const { id } = await q(`/app/${input.appId}/version/create`, {
192
+ method: "POST",
193
+ body: JSON.stringify({
194
+ name: input.name,
195
+ hash,
196
+ description: input.description ?? "",
197
+ metaInfo: ""
198
+ })
199
+ });
200
+ await bind(q, input.appId, id, input.packageVersion, input.rollout);
201
+ return { versionId: id };
116
202
  },
117
- async uploadApp(appId, appFile) {
118
- const r = await runner([
119
- "uploadApp",
120
- appFile,
121
- "--appId",
122
- String(appId)
123
- ]);
124
- if (r.code !== 0) fail("uploadApp", r);
125
- const m = (r.stdout + r.stderr).match(/已成功上传app原生包(id:\s*(\d+),\s*version:\s*([\d.]+)[^)]*)/);
126
- if (!m) throw new Error(`pushy uploadApp 输出未含原生包信息:${(r.stdout + r.stderr).trim()}`);
203
+ async uploadApp(appId, appFile, versionName) {
204
+ const hash = await uploadFile(f, baseUrl, opts.token, appId, appFile);
205
+ const { id } = await q(`/app/${appId}/package/create`, {
206
+ method: "POST",
207
+ body: JSON.stringify({
208
+ name: versionName,
209
+ hash,
210
+ buildTime: String(Date.now())
211
+ })
212
+ });
127
213
  return {
128
- packageId: Number(m[1]),
129
- version: m[2]
214
+ packageId: id,
215
+ version: versionName
130
216
  };
131
217
  },
132
218
  async rebind(input) {
133
- const args = [
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);
219
+ await bind(q, input.appId, input.versionId, input.packageVersion, input.rollout);
146
220
  },
147
221
  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
222
  const out = [];
156
- for (const m of r.stdout.matchAll(/^(\d+)\)\s+(\S+)\s+(\S+)/gm)) out.push({
157
- id: Number(m[1]),
158
- hash: m[2],
159
- name: m[3]
160
- });
223
+ const limit = 100;
224
+ for (let offset = 0; offset < 1e4; offset += limit) {
225
+ const { data } = await q(`/app/${appId}/version/list?offset=${offset}&limit=${limit}`);
226
+ if (!data?.length) break;
227
+ for (const v of data) out.push({
228
+ id: v.id,
229
+ hash: v.hash ?? "",
230
+ name: v.name
231
+ });
232
+ if (data.length < limit) break;
233
+ }
161
234
  return out;
162
235
  }
163
236
  };
@@ -166,8 +239,17 @@ function createPushyCli(runner = spawnPushy) {
166
239
  //#region src/plugins/rn-update.ts
167
240
  /**
168
241
  * rn-update 插件:react-native-update(pushy/cresc/自建服务端)的 applift UpdateProvider 实现。
169
- * 判定引擎为纯函数(五形态完备性见测试头注释);发布链路 = ppk 构建 / 测试通道 / promote / 原生基线。
242
+ * 判定引擎为纯函数(五形态完备性见测试头注释);发布链路 = ppk 产物入口 / 测试通道 / promote / 原生基线。
243
+ * ppk 是构建产物(pushy bundle = metro + hermesc,完整构建链):由项目先行产出
244
+ * (npm script / CI),applift 只探测/接收——不 spawn 构建命令(架构不变量 1)。
170
245
  */
246
+ /** ppk 探测内置默认:pushy CLI 官方输出约定(tempDir .pushy + output/{platform}.${time}.ppk;
247
+ * cresc 同构,仅目录 .cresc.temp 不同——用 cresc 的项目在 ppkPatterns 覆盖) */
248
+ const DEFAULT_PPK_PATTERNS = {
249
+ harmony: [".pushy/output/harmony.*.ppk"],
250
+ android: [".pushy/output/android.*.ppk"],
251
+ ios: [".pushy/output/ios.*.ppk"]
252
+ };
171
253
  /** 项目原生目录:任一命中即必须原生发版 */
172
254
  const NATIVE_DIRS = [
173
255
  "harmony/",
@@ -366,12 +448,15 @@ function judgeDelivery(diff, ctx, opts) {
366
448
  };
367
449
  }
368
450
  /** pushy 官方测试包约定:与正式同源码、仅版本号偏移(major+1000,如 1.6.0→1001.6.0) */
369
- const TEST_VERSION_OFFSET = 1e3;
451
+ const BETA_VERSION_OFFSET = 1e3;
452
+ /** 正式 promote 的内置默认灰度比例(100 = 全量)。promote 语义是"测试通道已验证、发正式",
453
+ * 全量是常态、灰度是显式选择(--rollout 或 rnUpdate({ defaultRollout })),不藏在配置后面 */
454
+ const DEFAULT_ROLLOUT = 100;
370
455
  /** 生成测试包偏移版本号(major+1000);非数字开头版本号无法偏移,显式报错 */
371
- function testPackageVersion(versionName) {
456
+ function betaPackageVersion(versionName) {
372
457
  const m = /^(\d+)(\..+)?$/.exec(versionName);
373
- if (!m) throw new Error(`versionName「${versionName}」非 X.Y.Z 形态,无法按 major+${TEST_VERSION_OFFSET} 生成测试包偏移版本`);
374
- return `${Number(m[1]) + TEST_VERSION_OFFSET}${m[2] ?? ""}`;
458
+ if (!m) throw new Error(`versionName「${versionName}」非 X.Y.Z 形态,无法按 major+${BETA_VERSION_OFFSET} 生成测试包偏移版本`);
459
+ return `${Number(m[1]) + BETA_VERSION_OFFSET}${m[2] ?? ""}`;
375
460
  }
376
461
  function platformAppId(opts, platform) {
377
462
  const id = opts[platform];
@@ -394,8 +479,68 @@ function nextPatchNumber(names, versionName) {
394
479
  }
395
480
  return max + 1;
396
481
  }
397
- /** provider 构造(cli 注入点:测试 mock / 将来直连 API) */
398
- function makeRnUpdateProvider(opts, cli = createPushyCli()) {
482
+ /**
483
+ * 客户端解析:注入了 cli(测试 mock / 特殊 transport)则直用;缺省按凭据构造 OpenAPI 客户端
484
+ * (token 来自 PUSHY_API_TOKEN,bundle 始终走本地 pushy CLI)。
485
+ * fail-fast:token 缺失在本地 bundle 之前报错(不打无谓的 metro 包)。
486
+ */
487
+ function resolvePushyClient(opts, cli, credentials) {
488
+ if (cli) return cli;
489
+ const token = credentials?.[PUSHY_TOKEN_KEY];
490
+ if (!token) throw new Error(`pushy 热更缺少凭据 ${PUSHY_TOKEN_KEY}:在 ~/.applift/credentials.json 对应 profile 配置(pushy 网页控制台 → API Token 页创建;CI 可用同名环境变量覆盖)`);
491
+ return createPushyApi({
492
+ baseUrl: opts.serverUrl,
493
+ token
494
+ });
495
+ }
496
+ /**
497
+ * ppk 产物入口:--artifact 显式 > ppkPatterns 探测(mtime 最新)。
498
+ * 探测路径强制新鲜度护栏;显式 --artifact 是用户声明「我知道我在发什么」,跳过护栏
499
+ * (与 --delivery ota 的显式覆盖语义一致,同原生 --artifact 不校验产物新旧)。
500
+ */
501
+ async function resolvePpk(opts, ctx) {
502
+ if (ctx.artifact) return ctx.artifact;
503
+ const platform = ctx.platform;
504
+ const patterns = opts.ppkPatterns?.[platform] ?? DEFAULT_PPK_PATTERNS[platform];
505
+ const ppk = findArtifacts(ctx.cwd, patterns)[0];
506
+ if (!ppk) throw new Error(`未探测到 ${platform} 的 ppk 热更包(${patterns.join(", ")})——ppk 是构建产物,请先产出(如 npm run ppk:${platform} = pushy bundle --platform ${platform} --no-interactive)再 applift beta,或用 --artifact 显式指定 ppk 路径`);
507
+ await assertPpkFresh(ctx.cwd, ppk);
508
+ return ppk;
509
+ }
510
+ /**
511
+ * 新鲜度护栏:工作区未发布改动(锚点..当前,与判定同口径 collectReleaseDiff)的最新文件
512
+ * mtime 晚于 ppk → 报错——防止忘重新 bundle 把旧 JS 发给测试设备。
513
+ * 无锚点/无 diff/文件已删除(无 mtime 可比)不拦。
514
+ */
515
+ async function assertPpkFresh(cwd, ppk) {
516
+ const diff = await collectReleaseDiff(cwd);
517
+ if (!diff) return;
518
+ let newest = 0;
519
+ let newestFile = "";
520
+ for (const f of diff) try {
521
+ const m = fs.statSync(path.join(cwd, f)).mtimeMs;
522
+ if (m > newest) {
523
+ newest = m;
524
+ newestFile = f;
525
+ }
526
+ } catch {}
527
+ if (newest > 0 && fs.statSync(ppk).mtimeMs < newest) throw new Error(`ppk(${path.basename(ppk)})早于最新代码改动(${newestFile})——疑似忘重新 bundle,请重新产出 ppk 后再发;确认无误可 --artifact 显式指定跳过此护栏`);
528
+ }
529
+ /** 发布新热更的公共骨架:客户端/appId/版本号/ppk 探测 + 服务端取号(publishToBeta/publishToProd 共用) */
530
+ async function preparePublish(opts, cli, ctx) {
531
+ const client = resolvePushyClient(opts, cli, ctx.credentials);
532
+ const appId = platformAppId(opts, ctx.platform);
533
+ const versionName = currentVersionName(ctx);
534
+ return {
535
+ client,
536
+ appId,
537
+ versionName,
538
+ ppk: await resolvePpk(opts, ctx),
539
+ name: `${versionName}-patch.${nextPatchNumber(await client.versions(appId), versionName)}`
540
+ };
541
+ }
542
+ /** provider 构造(cli 注入点见 resolvePushyClient) */
543
+ function makeRnUpdateProvider(opts, cli) {
399
544
  return {
400
545
  id: "rn-update",
401
546
  frameworks: ["rn"],
@@ -405,40 +550,51 @@ function makeRnUpdateProvider(opts, cli = createPushyCli()) {
405
550
  "ios"
406
551
  ].filter((k) => typeof opts[k] === "number"),
407
552
  judge: (diff, ctx) => judgeDelivery(diff, ctx, opts),
408
- async publishToTest(ctx) {
409
- const appId = platformAppId(opts, ctx.platform);
410
- const versionName = currentVersionName(ctx);
411
- const name = `${versionName}-patch.${nextPatchNumber(await cli.versions(appId), versionName)}`;
412
- const ppk = await cli.bundle(ctx.platform, ctx.cwd);
553
+ async publishToBeta(ctx) {
554
+ const { client, appId, versionName, ppk, name } = await preparePublish(opts, cli, ctx);
413
555
  return {
414
556
  versionName: name,
415
- versionId: (await cli.publish({
557
+ versionId: (await client.publish({
416
558
  appId,
417
559
  ppk,
418
560
  platform: ctx.platform,
419
561
  name,
420
562
  description: ctx.note,
421
- packageVersion: testPackageVersion(versionName)
563
+ packageVersion: betaPackageVersion(versionName)
422
564
  })).versionId
423
565
  };
424
566
  },
567
+ async publishToProd(ctx) {
568
+ const { client, appId, versionName, ppk, name } = await preparePublish(opts, cli, ctx);
569
+ const rollout = ctx.rollout ?? opts.defaultRollout ?? DEFAULT_ROLLOUT;
570
+ return {
571
+ versionName: name,
572
+ versionId: (await client.publish({
573
+ appId,
574
+ ppk,
575
+ platform: ctx.platform,
576
+ name,
577
+ description: ctx.note,
578
+ packageVersion: versionName,
579
+ rollout
580
+ })).versionId,
581
+ ...rollout !== void 0 ? { rollout } : {}
582
+ };
583
+ },
425
584
  async promoteToProd(ctx) {
585
+ const client = resolvePushyClient(opts, cli, ctx.credentials);
426
586
  const appId = platformAppId(opts, ctx.platform);
427
587
  const versionName = currentVersionName(ctx);
428
- const existing = await cli.versions(appId);
429
- const latest = existing.map((v) => {
588
+ const latest = (await client.versions(appId)).map((v) => {
430
589
  const m = v.name.match(new RegExp(`^${versionName.replace(/\./g, "\\.")}-patch\\.(\\d+)$`));
431
590
  return m ? {
432
591
  ...v,
433
592
  n: Number(m[1])
434
593
  } : null;
435
594
  }).filter((x) => x !== null).sort((a, b) => b.n - a.n)[0];
436
- if (!latest) {
437
- const patchNames = existing.map((v) => v.name).filter((n) => /-patch\.\d+$/.test(n)).slice(0, 5);
438
- throw new Error(`测试通道没有 ${versionName}-patch.N 热更版本可供 promote${patchNames.length > 0 ? `(现存:${patchNames.join(", ")}${existing.length > 5 ? " …" : ""})` : ""}:先 applift beta 发布测试热更并验证`);
439
- }
440
- const rollout = ctx.rollout ?? opts.defaultRollout;
441
- await cli.rebind({
595
+ if (!latest) return null;
596
+ const rollout = ctx.rollout ?? opts.defaultRollout ?? DEFAULT_ROLLOUT;
597
+ await client.rebind({
442
598
  appId,
443
599
  versionId: latest.id,
444
600
  packageVersion: versionName,
@@ -453,22 +609,24 @@ function makeRnUpdateProvider(opts, cli = createPushyCli()) {
453
609
  }
454
610
  /** 渠道 → 平台映射(热更已接入的平台;基线上传按此路由) */
455
611
  const CHANNEL_PLATFORM = { agc: "harmony" };
456
- /** rnUpdate 插件工厂:注册完整 provider(判定 + 测试发布 + promote)+ 原生发版自动基线。
457
- * cli 注入点:测试 mock / 将来进程内直连 API。 */
458
- function rnUpdate(opts = {}, cli = createPushyCli()) {
612
+ /** rnUpdate 插件工厂:注册完整 provider(判定 + beta 发布 + promote)+ 原生发版自动基线。
613
+ * cli 注入点:测试 mock / 特殊 transport;缺省 OpenAPI 客户端(token 来自 ctx.credentials)。 */
614
+ function rnUpdate(opts = {}, cli) {
459
615
  const provider = makeRnUpdateProvider(opts, cli);
460
616
  return {
461
617
  name: "rn-update",
462
618
  "update:register"(providers) {
463
619
  providers.push(provider);
464
620
  },
465
- async "stage:after"(result) {
621
+ async "stage:after"(result, hookCtx) {
466
622
  const platform = CHANNEL_PLATFORM[result.channel];
467
623
  if (!result.ok || !platform || !result.artifactPath) return;
468
624
  const appId = opts[platform];
469
625
  if (typeof appId !== "number") return;
470
626
  try {
471
- await cli.uploadApp(appId, result.artifactPath);
627
+ const versionName = readArtifactVersion(result.artifactPath)?.versionName;
628
+ if (!versionName) throw new Error(`未能从产物读出 versionName:${result.artifactPath}(readArtifactVersion 不支持该格式?)`);
629
+ await resolvePushyClient(opts, cli, hookCtx.credentials).uploadApp(appId, result.artifactPath, versionName);
472
630
  } catch (e) {
473
631
  process.stderr.write(`⚠ pushy 基线上传失败(请人工补传):${e.message}\n`);
474
632
  }
@@ -476,4 +634,4 @@ function rnUpdate(opts = {}, cli = createPushyCli()) {
476
634
  };
477
635
  }
478
636
  //#endregion
479
- export { createPushyCli, dingtalkNotify, electronAdapter, electronFramework, flutterAdapter, flutterFramework, judgeDelivery, knownFrameworkAdapters, makeRnUpdateProvider, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, rnUpdate, spawnPushy, suggestFramework, tauriAdapter, tauriFramework };
637
+ export { PUSHY_DEFAULT_BASE_URL, PUSHY_TOKEN_KEY, createPushyApi, dingtalkNotify, electronAdapter, electronFramework, flutterAdapter, flutterFramework, judgeDelivery, knownFrameworkAdapters, makeRnUpdateProvider, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, rnAdapter, rnFramework, rnUpdate, suggestFramework, tauriAdapter, tauriFramework };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applift/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/hcg1023/applift.git",