@huoye/app-ship 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/dist/cli.js +733 -75
- package/dist/index.cjs +655 -67
- package/dist/index.d.cts +137 -14
- package/dist/index.d.ts +137 -14
- package/dist/index.js +629 -51
- package/package.json +11 -7
- package/dist/cli.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/utils/upload-manager.ts
|
|
10
|
-
import
|
|
10
|
+
import path12 from "path";
|
|
11
11
|
|
|
12
12
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
13
13
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -965,6 +965,280 @@ async function parseHap(filePath) {
|
|
|
965
965
|
return info;
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
// src/utils/release-receipt.ts
|
|
969
|
+
import { createHash, randomUUID } from "crypto";
|
|
970
|
+
import { createReadStream } from "fs";
|
|
971
|
+
import { chmod, mkdir, readFile, realpath, rename, stat, writeFile } from "fs/promises";
|
|
972
|
+
import { homedir } from "os";
|
|
973
|
+
import path2 from "path";
|
|
974
|
+
var RELEASE_RECEIPT_SCHEMA_VERSION = 1;
|
|
975
|
+
var DEFAULT_RELEASE_RECEIPT_LIMIT = 100;
|
|
976
|
+
var REMOTE_IDENTIFIER_KEYS = [
|
|
977
|
+
"id",
|
|
978
|
+
"appId",
|
|
979
|
+
"buildId",
|
|
980
|
+
"versionId",
|
|
981
|
+
"releaseId",
|
|
982
|
+
"editId",
|
|
983
|
+
"versionCode",
|
|
984
|
+
"track",
|
|
985
|
+
"apiTrack",
|
|
986
|
+
"status",
|
|
987
|
+
"committed"
|
|
988
|
+
];
|
|
989
|
+
function resolveReleaseProjectRoot(projectRoot, configPath) {
|
|
990
|
+
return path2.resolve(projectRoot || path2.dirname(path2.resolve(configPath)));
|
|
991
|
+
}
|
|
992
|
+
function sanitizeReleaseMessage(value, maxLength = 500) {
|
|
993
|
+
let message = typeof value === "string" ? value : String(value ?? "");
|
|
994
|
+
message = message.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[\u5DF2\u8131\u654F]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [\u5DF2\u8131\u654F]").replace(
|
|
995
|
+
/(["']?(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|api[_-]?key|app[_-]?password|authorization|credential|service[_-]?account(?:[_-]?json)?[_-]?content)["']?\s*[:=]\s*)["']?[^\s,"'}]+["']?/gi,
|
|
996
|
+
"$1[\u5DF2\u8131\u654F]"
|
|
997
|
+
).replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
998
|
+
return message.length > maxLength ? `${message.slice(0, maxLength - 1)}\u2026` : message;
|
|
999
|
+
}
|
|
1000
|
+
function createUploadTargets(results) {
|
|
1001
|
+
return results.map((result) => {
|
|
1002
|
+
const platform = sanitizeReleaseMessage(result.platform, 80) || "unknown";
|
|
1003
|
+
const data = isRecord(result.data) ? result.data : void 0;
|
|
1004
|
+
const isDraft = result.success && platform === "google_play" && String(data?.status || "").toLowerCase() === "draft";
|
|
1005
|
+
const remote = createRemoteReceipt(result, data);
|
|
1006
|
+
return {
|
|
1007
|
+
platform,
|
|
1008
|
+
state: result.success ? isDraft ? "draft_created" : "uploaded" : "failed",
|
|
1009
|
+
outcome: result.success ? "success" : "failed",
|
|
1010
|
+
message: sanitizeReleaseMessage(result.message) || (result.success ? "\u4E0A\u4F20\u6210\u529F" : "\u4E0A\u4F20\u5931\u8D25"),
|
|
1011
|
+
...remote ? { remote } : {}
|
|
1012
|
+
};
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
async function createReleaseReceipt(input) {
|
|
1016
|
+
const projectRoot = await canonicalizeProjectRoot(input.projectRoot);
|
|
1017
|
+
const startedAt = toDate(input.startedAt ?? Date.now());
|
|
1018
|
+
const finishedAt = toDate(input.finishedAt ?? Date.now());
|
|
1019
|
+
const targets = input.targets.map(sanitizeTarget);
|
|
1020
|
+
return {
|
|
1021
|
+
schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
|
|
1022
|
+
id: randomUUID(),
|
|
1023
|
+
projectKey: createProjectKey(projectRoot),
|
|
1024
|
+
projectRoot,
|
|
1025
|
+
kind: input.kind,
|
|
1026
|
+
...cleanOptionalText(input.appName, 160) ? { appName: cleanOptionalText(input.appName, 160) } : {},
|
|
1027
|
+
...cleanOptionalText(input.packageName, 240) ? { packageName: cleanOptionalText(input.packageName, 240) } : {},
|
|
1028
|
+
...cleanOptionalText(input.versionName, 80) ? { versionName: cleanOptionalText(input.versionName, 80) } : {},
|
|
1029
|
+
...input.buildNumber !== void 0 ? { buildNumber: cleanOptionalText(String(input.buildNumber), 80) } : {},
|
|
1030
|
+
...cleanOptionalText(input.changelog, 4e3) ? { changelog: cleanOptionalText(input.changelog, 4e3) } : {},
|
|
1031
|
+
artifacts: await inspectArtifacts(input.artifactPaths),
|
|
1032
|
+
targets,
|
|
1033
|
+
outcome: input.outcome ?? inferOutcome(targets),
|
|
1034
|
+
startedAt: startedAt.toISOString(),
|
|
1035
|
+
finishedAt: finishedAt.toISOString(),
|
|
1036
|
+
durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime())
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function formatReleaseReceipt(receipt) {
|
|
1040
|
+
const lines = [
|
|
1041
|
+
`${receipt.appName || receipt.packageName || "\u5E94\u7528"} \xB7 ${receipt.kind === "build" ? "\u6784\u5EFA\u56DE\u6267" : "\u4E0A\u4F20\u56DE\u6267"}`,
|
|
1042
|
+
`\u7ED3\u679C\uFF1A${receipt.outcome}`,
|
|
1043
|
+
`\u7248\u672C\uFF1A${receipt.versionName || "-"} (${receipt.buildNumber || "-"})`,
|
|
1044
|
+
`\u5B8C\u6210\u65F6\u95F4\uFF1A${receipt.finishedAt}`
|
|
1045
|
+
];
|
|
1046
|
+
if (receipt.packageName) lines.push(`\u5305\u540D\uFF1A${receipt.packageName}`);
|
|
1047
|
+
if (receipt.changelog) lines.push(`\u66F4\u65B0\u65E5\u5FD7\uFF1A${receipt.changelog}`);
|
|
1048
|
+
for (const artifact of receipt.artifacts) {
|
|
1049
|
+
lines.push(
|
|
1050
|
+
`\u4EA7\u7269\uFF1A${artifact.path}${artifact.sizeBytes !== void 0 ? ` \xB7 ${artifact.sizeBytes} bytes` : ""}${artifact.sha256 ? ` \xB7 SHA-256 ${artifact.sha256}` : ""}`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
for (const target of receipt.targets) {
|
|
1054
|
+
lines.push(`${target.platform}\uFF1A${target.state} \xB7 ${target.message}`);
|
|
1055
|
+
if (target.remote?.consoleUrl) lines.push(`\u540E\u53F0\uFF1A${target.remote.consoleUrl}`);
|
|
1056
|
+
}
|
|
1057
|
+
return lines.join("\n");
|
|
1058
|
+
}
|
|
1059
|
+
var ReleaseReceiptStore = class {
|
|
1060
|
+
storageRoot;
|
|
1061
|
+
maxEntries;
|
|
1062
|
+
constructor(options = {}) {
|
|
1063
|
+
this.storageRoot = path2.resolve(options.storageRoot || path2.join(homedir(), ".flu-cli", "release-receipts"));
|
|
1064
|
+
this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_RELEASE_RECEIPT_LIMIT);
|
|
1065
|
+
}
|
|
1066
|
+
async append(receipt) {
|
|
1067
|
+
await mkdir(this.storageRoot, { recursive: true, mode: 448 });
|
|
1068
|
+
await chmod(this.storageRoot, 448);
|
|
1069
|
+
const filePath = this.getFilePath(receipt.projectKey);
|
|
1070
|
+
const current = await this.readFile(filePath, receipt.projectKey);
|
|
1071
|
+
const receipts = [receipt, ...current.receipts.filter((item) => item.id !== receipt.id)].slice(
|
|
1072
|
+
0,
|
|
1073
|
+
this.maxEntries
|
|
1074
|
+
);
|
|
1075
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1076
|
+
await writeFile(
|
|
1077
|
+
tempPath,
|
|
1078
|
+
`${JSON.stringify({
|
|
1079
|
+
schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
|
|
1080
|
+
projectKey: receipt.projectKey,
|
|
1081
|
+
receipts
|
|
1082
|
+
}, null, 2)}
|
|
1083
|
+
`,
|
|
1084
|
+
{ encoding: "utf8", mode: 384 }
|
|
1085
|
+
);
|
|
1086
|
+
await rename(tempPath, filePath);
|
|
1087
|
+
await chmod(filePath, 384);
|
|
1088
|
+
}
|
|
1089
|
+
async list(projectRoot, limit = 20) {
|
|
1090
|
+
const canonicalRoot = await canonicalizeProjectRoot(projectRoot);
|
|
1091
|
+
const projectKey = createProjectKey(canonicalRoot);
|
|
1092
|
+
const data = await this.readFile(this.getFilePath(projectKey), projectKey);
|
|
1093
|
+
return data.receipts.slice(0, Math.max(0, limit));
|
|
1094
|
+
}
|
|
1095
|
+
async save(input) {
|
|
1096
|
+
const receipt = await createReleaseReceipt(input);
|
|
1097
|
+
await this.append(receipt);
|
|
1098
|
+
return receipt;
|
|
1099
|
+
}
|
|
1100
|
+
getFilePath(projectKey) {
|
|
1101
|
+
return path2.join(this.storageRoot, `${projectKey}.json`);
|
|
1102
|
+
}
|
|
1103
|
+
async readFile(filePath, projectKey) {
|
|
1104
|
+
try {
|
|
1105
|
+
const raw = JSON.parse(await readFile(filePath, "utf8"));
|
|
1106
|
+
return {
|
|
1107
|
+
schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
|
|
1108
|
+
projectKey,
|
|
1109
|
+
receipts: Array.isArray(raw.receipts) ? raw.receipts : []
|
|
1110
|
+
};
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
if (error?.code === "ENOENT") {
|
|
1113
|
+
return { schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION, projectKey, receipts: [] };
|
|
1114
|
+
}
|
|
1115
|
+
throw new Error(`\u8BFB\u53D6\u53D1\u5E03\u56DE\u6267\u5931\u8D25: ${sanitizeReleaseMessage(error?.message || error)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
function createProjectKey(projectRoot) {
|
|
1120
|
+
return createHash("sha256").update(path2.resolve(projectRoot)).digest("hex").slice(0, 24);
|
|
1121
|
+
}
|
|
1122
|
+
function createRemoteReceipt(result, data) {
|
|
1123
|
+
const identifiers = {};
|
|
1124
|
+
for (const key of REMOTE_IDENTIFIER_KEYS) {
|
|
1125
|
+
const value = data?.[key];
|
|
1126
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1127
|
+
identifiers[key] = typeof value === "string" ? sanitizeReleaseMessage(value, 240) : value;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const consoleUrl = cleanHttpUrl(result.consoleUrl);
|
|
1131
|
+
const downloadUrl = cleanHttpUrl(result.url || result.link);
|
|
1132
|
+
if (!consoleUrl && !downloadUrl && Object.keys(identifiers).length === 0) return void 0;
|
|
1133
|
+
return {
|
|
1134
|
+
...consoleUrl ? { consoleUrl } : {},
|
|
1135
|
+
...downloadUrl ? { downloadUrl } : {},
|
|
1136
|
+
...Object.keys(identifiers).length > 0 ? { identifiers } : {}
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function sanitizeTarget(target) {
|
|
1140
|
+
return {
|
|
1141
|
+
platform: sanitizeReleaseMessage(target.platform, 80) || "unknown",
|
|
1142
|
+
state: target.state,
|
|
1143
|
+
outcome: target.outcome,
|
|
1144
|
+
message: sanitizeReleaseMessage(target.message),
|
|
1145
|
+
...target.remote ? { remote: sanitizeRemote(target.remote) } : {},
|
|
1146
|
+
...cleanOptionalText(target.lastKnownRemoteStatus, 120) ? { lastKnownRemoteStatus: cleanOptionalText(target.lastKnownRemoteStatus, 120) } : {},
|
|
1147
|
+
...cleanOptionalText(target.checkedAt, 80) ? { checkedAt: cleanOptionalText(target.checkedAt, 80) } : {}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function sanitizeRemote(remote) {
|
|
1151
|
+
const identifiers = {};
|
|
1152
|
+
for (const [key, value] of Object.entries(remote.identifiers || {})) {
|
|
1153
|
+
if (!REMOTE_IDENTIFIER_KEYS.includes(key)) continue;
|
|
1154
|
+
if (typeof value === "string") identifiers[key] = sanitizeReleaseMessage(value, 240);
|
|
1155
|
+
if (typeof value === "number" || typeof value === "boolean") identifiers[key] = value;
|
|
1156
|
+
}
|
|
1157
|
+
const consoleUrl = cleanHttpUrl(remote.consoleUrl);
|
|
1158
|
+
const downloadUrl = cleanHttpUrl(remote.downloadUrl);
|
|
1159
|
+
return {
|
|
1160
|
+
...consoleUrl ? { consoleUrl } : {},
|
|
1161
|
+
...downloadUrl ? { downloadUrl } : {},
|
|
1162
|
+
...Object.keys(identifiers).length > 0 ? { identifiers } : {}
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
async function inspectArtifacts(value) {
|
|
1166
|
+
const paths = Array.from(new Set(flattenArtifactPaths(value).map((item) => path2.resolve(item))));
|
|
1167
|
+
return Promise.all(paths.map(inspectArtifact));
|
|
1168
|
+
}
|
|
1169
|
+
async function inspectArtifact(filePath) {
|
|
1170
|
+
const base = { path: filePath, name: path2.basename(filePath) };
|
|
1171
|
+
try {
|
|
1172
|
+
const info = await stat(filePath);
|
|
1173
|
+
if (!info.isFile()) return { ...base, available: false };
|
|
1174
|
+
return {
|
|
1175
|
+
...base,
|
|
1176
|
+
sizeBytes: info.size,
|
|
1177
|
+
sha256: await hashFile(filePath),
|
|
1178
|
+
available: true
|
|
1179
|
+
};
|
|
1180
|
+
} catch {
|
|
1181
|
+
return { ...base, available: false };
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function hashFile(filePath) {
|
|
1185
|
+
return new Promise((resolve, reject) => {
|
|
1186
|
+
const hash = createHash("sha256");
|
|
1187
|
+
const stream = createReadStream(filePath);
|
|
1188
|
+
stream.on("error", reject);
|
|
1189
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1190
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function flattenArtifactPaths(value) {
|
|
1194
|
+
if (typeof value === "string" && value.trim()) return [value];
|
|
1195
|
+
if (Array.isArray(value)) return value.flatMap(flattenArtifactPaths);
|
|
1196
|
+
if (isRecord(value)) return Object.values(value).flatMap(flattenArtifactPaths);
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
function inferOutcome(targets) {
|
|
1200
|
+
if (targets.length === 0) return "failed";
|
|
1201
|
+
if (targets.every((target) => target.outcome === "cancelled")) return "cancelled";
|
|
1202
|
+
const successCount = targets.filter((target) => target.outcome === "success").length;
|
|
1203
|
+
if (successCount === targets.length) return "success";
|
|
1204
|
+
if (successCount > 0) return "partial";
|
|
1205
|
+
return "failed";
|
|
1206
|
+
}
|
|
1207
|
+
async function canonicalizeProjectRoot(projectRoot) {
|
|
1208
|
+
const resolved = path2.resolve(projectRoot);
|
|
1209
|
+
try {
|
|
1210
|
+
return await realpath(resolved);
|
|
1211
|
+
} catch {
|
|
1212
|
+
return resolved;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function toDate(value) {
|
|
1216
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
1217
|
+
return Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
|
|
1218
|
+
}
|
|
1219
|
+
function cleanOptionalText(value, maxLength) {
|
|
1220
|
+
const cleaned = sanitizeReleaseMessage(value, maxLength);
|
|
1221
|
+
return cleaned || void 0;
|
|
1222
|
+
}
|
|
1223
|
+
function cleanHttpUrl(value) {
|
|
1224
|
+
if (typeof value !== "string") return void 0;
|
|
1225
|
+
try {
|
|
1226
|
+
const url = new URL(value);
|
|
1227
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
|
|
1228
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
1229
|
+
if (/token|secret|password|signature|credential|key/i.test(key)) {
|
|
1230
|
+
url.searchParams.set(key, "[\u5DF2\u8131\u654F]");
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return url.toString();
|
|
1234
|
+
} catch {
|
|
1235
|
+
return void 0;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function isRecord(value) {
|
|
1239
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
968
1242
|
// src/adapters/pgyer_uploader.ts
|
|
969
1243
|
var PgyerUploader = class _PgyerUploader {
|
|
970
1244
|
name = "Pgyer";
|
|
@@ -1083,7 +1357,7 @@ var PgyerUploader = class _PgyerUploader {
|
|
|
1083
1357
|
|
|
1084
1358
|
// src/adapters/huawei_uploader.ts
|
|
1085
1359
|
import fs4 from "fs-extra";
|
|
1086
|
-
import
|
|
1360
|
+
import path3 from "path";
|
|
1087
1361
|
import { promisify } from "util";
|
|
1088
1362
|
import axios2 from "axios";
|
|
1089
1363
|
import FormData2 from "form-data";
|
|
@@ -1162,7 +1436,7 @@ var HuaweiUploader = class _HuaweiUploader {
|
|
|
1162
1436
|
const clientSecret = hwConfig.auth.clientSecret;
|
|
1163
1437
|
const appId = hwConfig.appId;
|
|
1164
1438
|
const releaseType = hwConfig.releaseType || "draft";
|
|
1165
|
-
const suffix =
|
|
1439
|
+
const suffix = path3.extname(filePath).substring(1);
|
|
1166
1440
|
const retryConfig = config.retry;
|
|
1167
1441
|
const tokenUrl = this.getTokenUrl(config);
|
|
1168
1442
|
const publishApiRoot = this.getApiRoot(config);
|
|
@@ -1188,8 +1462,8 @@ var HuaweiUploader = class _HuaweiUploader {
|
|
|
1188
1462
|
const uploadResults = [];
|
|
1189
1463
|
const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
|
|
1190
1464
|
if (!p || !fs4.existsSync(p)) return;
|
|
1191
|
-
const currentSuffix =
|
|
1192
|
-
logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${
|
|
1465
|
+
const currentSuffix = path3.extname(p).substring(1);
|
|
1466
|
+
logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${path3.basename(p)}`);
|
|
1193
1467
|
const uploadInfo = await retryStrategy.execute(
|
|
1194
1468
|
() => this.getUploadUrl(ctx, currentSuffix),
|
|
1195
1469
|
`Huawei get upload URL for ${abi}`
|
|
@@ -1205,7 +1479,7 @@ var HuaweiUploader = class _HuaweiUploader {
|
|
|
1205
1479
|
throw uploadFileRes.error || new Error(`File upload failed for ${abi}`);
|
|
1206
1480
|
}
|
|
1207
1481
|
uploadResults.push({
|
|
1208
|
-
fileName:
|
|
1482
|
+
fileName: path3.basename(p),
|
|
1209
1483
|
fileDestUrl: uploadFileRes.data[0].fileDestUrl
|
|
1210
1484
|
});
|
|
1211
1485
|
});
|
|
@@ -1377,7 +1651,7 @@ var HuaweiUploader = class _HuaweiUploader {
|
|
|
1377
1651
|
}
|
|
1378
1652
|
/** 通用文件上传(FormData 方式,无需 API 认证头) */
|
|
1379
1653
|
async executeUploadRequest(uploadUrl, authCode, filePath) {
|
|
1380
|
-
const fileName =
|
|
1654
|
+
const fileName = path3.basename(filePath);
|
|
1381
1655
|
const fileSize = fs4.statSync(filePath).size;
|
|
1382
1656
|
const form = new FormData2();
|
|
1383
1657
|
form.append("file", fs4.createReadStream(filePath), {
|
|
@@ -1544,7 +1818,7 @@ var HuaweiUploader = class _HuaweiUploader {
|
|
|
1544
1818
|
logger.warn(`Asset file not found, skipping: ${filePath}`);
|
|
1545
1819
|
continue;
|
|
1546
1820
|
}
|
|
1547
|
-
const suffix =
|
|
1821
|
+
const suffix = path3.extname(filePath).substring(1).toLowerCase();
|
|
1548
1822
|
if (!HUAWEI_SUPPORTED_SUFFIXES.image.includes(suffix)) {
|
|
1549
1823
|
logger.warn(`Unsupported file type ${suffix}, skipping: ${filePath}`);
|
|
1550
1824
|
continue;
|
|
@@ -1599,7 +1873,7 @@ var HuaweiUploader = class _HuaweiUploader {
|
|
|
1599
1873
|
|
|
1600
1874
|
// src/adapters/xiaomi_uploader.ts
|
|
1601
1875
|
import fs5 from "fs-extra";
|
|
1602
|
-
import
|
|
1876
|
+
import path4 from "path";
|
|
1603
1877
|
import axios3 from "axios";
|
|
1604
1878
|
import FormData3 from "form-data";
|
|
1605
1879
|
import crypto3 from "crypto";
|
|
@@ -1927,14 +2201,14 @@ ${lines.join("\n")}
|
|
|
1927
2201
|
if (arm64 && arm32) {
|
|
1928
2202
|
mainPath = arm64[1];
|
|
1929
2203
|
secondPath = arm32[1];
|
|
1930
|
-
logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u67B6\u6784\u5206\u5305: \u4E3B\u5305(64\u4F4D)=${
|
|
2204
|
+
logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u67B6\u6784\u5206\u5305: \u4E3B\u5305(64\u4F4D)=${path4.basename(mainPath)}, \u6B21\u5305(32\u4F4D)=${path4.basename(secondPath)}`);
|
|
1931
2205
|
} else {
|
|
1932
2206
|
mainPath = artifactEntries[0][1];
|
|
1933
2207
|
if (artifactEntries.length > 1) {
|
|
1934
2208
|
secondPath = artifactEntries[1][1];
|
|
1935
|
-
logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u4E2A\u5305\uFF08\u975E\u6807\u51C6\u540D\uFF09\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u987A\u5E8F: \u4E3B\u5305=${
|
|
2209
|
+
logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u4E2A\u5305\uFF08\u975E\u6807\u51C6\u540D\uFF09\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u987A\u5E8F: \u4E3B\u5305=${path4.basename(mainPath)}, \u6B21\u5305=${path4.basename(secondPath)}`);
|
|
1936
2210
|
} else {
|
|
1937
|
-
logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${
|
|
2211
|
+
logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${path4.basename(mainPath)}`);
|
|
1938
2212
|
}
|
|
1939
2213
|
}
|
|
1940
2214
|
const mainMD5 = await this.calculateFileMD5(mainPath);
|
|
@@ -2008,7 +2282,7 @@ ${lines.join("\n")}
|
|
|
2008
2282
|
|
|
2009
2283
|
// src/adapters/apple_store_uploader.ts
|
|
2010
2284
|
import fs6 from "fs-extra";
|
|
2011
|
-
import
|
|
2285
|
+
import path5 from "path";
|
|
2012
2286
|
import { exec as exec2 } from "child_process";
|
|
2013
2287
|
import { promisify as promisify2 } from "util";
|
|
2014
2288
|
import axios4 from "axios";
|
|
@@ -2174,7 +2448,7 @@ var AppleStoreUploader = class {
|
|
|
2174
2448
|
appPassword,
|
|
2175
2449
|
"-showTransferRate"
|
|
2176
2450
|
].join(" ");
|
|
2177
|
-
logger.info(`[Transporter] Uploading: ${
|
|
2451
|
+
logger.info(`[Transporter] Uploading: ${path5.basename(ipaPath)}`);
|
|
2178
2452
|
try {
|
|
2179
2453
|
const { stdout, stderr } = await execAsync(command, {
|
|
2180
2454
|
timeout: 30 * 60 * 1e3,
|
|
@@ -2225,7 +2499,7 @@ Stderr: ${error.stderr || ""}`);
|
|
|
2225
2499
|
});
|
|
2226
2500
|
command = innerArgs.join(" ");
|
|
2227
2501
|
}
|
|
2228
|
-
logger.info(`[altool] Uploading: ${
|
|
2502
|
+
logger.info(`[altool] Uploading: ${path5.basename(ipaPath)}`);
|
|
2229
2503
|
logger.info(`[altool] Command prefix: ${tool.command}`);
|
|
2230
2504
|
try {
|
|
2231
2505
|
const { stdout, stderr } = await execAsync(command, {
|
|
@@ -2809,7 +3083,7 @@ var TestFlightUploader = class _TestFlightUploader {
|
|
|
2809
3083
|
|
|
2810
3084
|
// src/adapters/adhoc_uploader.ts
|
|
2811
3085
|
import fs8 from "fs-extra";
|
|
2812
|
-
import
|
|
3086
|
+
import path6 from "path";
|
|
2813
3087
|
import { execSync } from "child_process";
|
|
2814
3088
|
var AdHocUploader = class {
|
|
2815
3089
|
name = "AdHoc";
|
|
@@ -2942,8 +3216,8 @@ var AdHocUploader = class {
|
|
|
2942
3216
|
* @param changelog 更新日志
|
|
2943
3217
|
*/
|
|
2944
3218
|
async createManifest(ipaPath, version, bundleId, changelog) {
|
|
2945
|
-
const ipaFileName =
|
|
2946
|
-
const appName = bundleId ||
|
|
3219
|
+
const ipaFileName = path6.basename(ipaPath);
|
|
3220
|
+
const appName = bundleId || path6.parse(ipaFileName).name;
|
|
2947
3221
|
const ipaUrl = `itms-services://?action=download-manifest&url=https://your-distribution-server.com/manifests/${ipaFileName}.plist`;
|
|
2948
3222
|
const manifestContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2949
3223
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -3002,14 +3276,14 @@ var AdHocUploader = class {
|
|
|
3002
3276
|
*/
|
|
3003
3277
|
async setupDistributionDirectory(ipaPath, manifestPath) {
|
|
3004
3278
|
const projectRoot = process.cwd();
|
|
3005
|
-
const distributionDir =
|
|
3279
|
+
const distributionDir = path6.join(projectRoot, "build", "adhoc-distribution");
|
|
3006
3280
|
await fs8.ensureDir(distributionDir);
|
|
3007
|
-
await fs8.ensureDir(
|
|
3008
|
-
await fs8.ensureDir(
|
|
3009
|
-
await fs8.ensureDir(
|
|
3010
|
-
const ipaFileName =
|
|
3011
|
-
await fs8.copy(ipaPath,
|
|
3012
|
-
await fs8.copy(manifestPath,
|
|
3281
|
+
await fs8.ensureDir(path6.join(distributionDir, "apps"));
|
|
3282
|
+
await fs8.ensureDir(path6.join(distributionDir, "manifests"));
|
|
3283
|
+
await fs8.ensureDir(path6.join(distributionDir, "icons"));
|
|
3284
|
+
const ipaFileName = path6.basename(ipaPath);
|
|
3285
|
+
await fs8.copy(ipaPath, path6.join(distributionDir, "apps", ipaFileName));
|
|
3286
|
+
await fs8.copy(manifestPath, path6.join(distributionDir, "manifests", `${ipaFileName}.plist`));
|
|
3013
3287
|
logger.info(`Distribution directory: ${distributionDir}`);
|
|
3014
3288
|
return distributionDir;
|
|
3015
3289
|
}
|
|
@@ -3019,7 +3293,7 @@ var AdHocUploader = class {
|
|
|
3019
3293
|
* @param bundleId Bundle ID
|
|
3020
3294
|
*/
|
|
3021
3295
|
generateInstallUrl(distributionPath, bundleId) {
|
|
3022
|
-
const manifestPath =
|
|
3296
|
+
const manifestPath = path6.join(distributionPath, "manifests", `*.plist`);
|
|
3023
3297
|
return `itms-services://?action=download-manifest&url=file://${manifestPath}`;
|
|
3024
3298
|
}
|
|
3025
3299
|
/**
|
|
@@ -3028,7 +3302,7 @@ var AdHocUploader = class {
|
|
|
3028
3302
|
* @param distributionPath 分发目录
|
|
3029
3303
|
*/
|
|
3030
3304
|
async generateQRCode(url, distributionPath) {
|
|
3031
|
-
const qrCodePath =
|
|
3305
|
+
const qrCodePath = path6.join(distributionPath, "install-qrcode.png");
|
|
3032
3306
|
try {
|
|
3033
3307
|
logger.info(`QR code would be saved to: ${qrCodePath}`);
|
|
3034
3308
|
logger.info(`Install URL: ${url}`);
|
|
@@ -3055,8 +3329,8 @@ var AdHocUploader = class {
|
|
|
3055
3329
|
logger.newLine();
|
|
3056
3330
|
logger.info("\u{1F4E6} Distribution Files:");
|
|
3057
3331
|
logger.info(` Directory: ${distributionPath}`);
|
|
3058
|
-
logger.info(` IPA: ${
|
|
3059
|
-
logger.info(` Manifest: ${
|
|
3332
|
+
logger.info(` IPA: ${path6.join(distributionPath, "apps", "*.ipa")}`);
|
|
3333
|
+
logger.info(` Manifest: ${path6.join(distributionPath, "manifests", "*.plist")}`);
|
|
3060
3334
|
logger.info(` QR Code: ${qrCodePath}`);
|
|
3061
3335
|
logger.newLine();
|
|
3062
3336
|
logger.info("\u{1F517} Installation Link:");
|
|
@@ -3115,7 +3389,7 @@ var AdHocUploader = class {
|
|
|
3115
3389
|
|
|
3116
3390
|
// src/adapters/harmony_uploader.ts
|
|
3117
3391
|
import fs9 from "fs-extra";
|
|
3118
|
-
import
|
|
3392
|
+
import path7 from "path";
|
|
3119
3393
|
import axios6 from "axios";
|
|
3120
3394
|
import FormData5 from "form-data";
|
|
3121
3395
|
var HarmonyUploader = class _HarmonyUploader {
|
|
@@ -3145,7 +3419,7 @@ var HarmonyUploader = class _HarmonyUploader {
|
|
|
3145
3419
|
const clientSecret = harmonyConfig.auth.clientSecret;
|
|
3146
3420
|
const appId = harmonyConfig.appId;
|
|
3147
3421
|
const releaseType = harmonyConfig.releaseType || "draft";
|
|
3148
|
-
const suffix =
|
|
3422
|
+
const suffix = path7.extname(filePath).substring(1);
|
|
3149
3423
|
const retryConfig = config.retry;
|
|
3150
3424
|
logger.info("Starting HarmonyOS AppGallery upload...");
|
|
3151
3425
|
try {
|
|
@@ -3262,7 +3536,7 @@ var HarmonyUploader = class _HarmonyUploader {
|
|
|
3262
3536
|
* @param filePath 文件路径
|
|
3263
3537
|
*/
|
|
3264
3538
|
async updateAppFileInfo(token, appId, authCode, filePath) {
|
|
3265
|
-
const fileName =
|
|
3539
|
+
const fileName = path7.basename(filePath);
|
|
3266
3540
|
const body = {
|
|
3267
3541
|
fileType: 7,
|
|
3268
3542
|
// 鸿蒙包的 fileType(需要根据华为最新文档确认)
|
|
@@ -3285,7 +3559,7 @@ var HarmonyUploader = class _HarmonyUploader {
|
|
|
3285
3559
|
|
|
3286
3560
|
// src/adapters/oppo_uploader.ts
|
|
3287
3561
|
import fs10 from "fs-extra";
|
|
3288
|
-
import
|
|
3562
|
+
import path8 from "path";
|
|
3289
3563
|
import axios7 from "axios";
|
|
3290
3564
|
import FormData6 from "form-data";
|
|
3291
3565
|
import crypto4 from "crypto";
|
|
@@ -3326,7 +3600,7 @@ var OppoUploader = class _OppoUploader {
|
|
|
3326
3600
|
* 尝试从文件名推断包名
|
|
3327
3601
|
*/
|
|
3328
3602
|
inferPackageName(filePath) {
|
|
3329
|
-
const fileName =
|
|
3603
|
+
const fileName = path8.basename(filePath);
|
|
3330
3604
|
const match = fileName.match(/^([\w.]+?)(?:-(?:release|debug|unsigned))?\.apk$/i);
|
|
3331
3605
|
if (match && match[1].includes(".")) {
|
|
3332
3606
|
return match[1];
|
|
@@ -3607,7 +3881,7 @@ var OppoUploader = class _OppoUploader {
|
|
|
3607
3881
|
|
|
3608
3882
|
// src/adapters/vivo_uploader.ts
|
|
3609
3883
|
import fs11 from "fs-extra";
|
|
3610
|
-
import
|
|
3884
|
+
import path9 from "path";
|
|
3611
3885
|
import axios8 from "axios";
|
|
3612
3886
|
import FormData7 from "form-data";
|
|
3613
3887
|
import crypto5 from "crypto";
|
|
@@ -3639,7 +3913,7 @@ var VivoUploader = class _VivoUploader {
|
|
|
3639
3913
|
const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
|
|
3640
3914
|
if (!p || !fs11.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
|
|
3641
3915
|
const md5 = await this.calculateFileMD5(p);
|
|
3642
|
-
logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${
|
|
3916
|
+
logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${path9.basename(p)}`);
|
|
3643
3917
|
const serialnumber = await this.uploadApk(accessKey, accessSecret, p, vivoConfig.packageName || "", md5);
|
|
3644
3918
|
const pkgInfo = await parsePackageInfo(p, { androidSdkPath: config.globalSettings?.androidSdkPath });
|
|
3645
3919
|
const nativeCodes = pkgInfo?.nativeCode || [];
|
|
@@ -3703,7 +3977,7 @@ var VivoUploader = class _VivoUploader {
|
|
|
3703
3977
|
const form = new FormData7();
|
|
3704
3978
|
Object.entries(params).forEach(([k, v]) => form.append(k, v));
|
|
3705
3979
|
form.append("sign", sign);
|
|
3706
|
-
form.append("file", fs11.createReadStream(filePath), { filename:
|
|
3980
|
+
form.append("file", fs11.createReadStream(filePath), { filename: path9.basename(filePath) });
|
|
3707
3981
|
const res = await axios8.post(_VivoUploader.API_BASE, form, { headers: form.getHeaders(), timeout: 3e5 });
|
|
3708
3982
|
if (res.data.code !== 0) throw new Error(`\u4E0A\u4F20\u5305\u5931\u8D25: ${res.data.msg}`);
|
|
3709
3983
|
return res.data.data?.serialnumber || res.data.data;
|
|
@@ -3743,7 +4017,7 @@ var VivoUploader = class _VivoUploader {
|
|
|
3743
4017
|
|
|
3744
4018
|
// src/adapters/tencent_uploader.ts
|
|
3745
4019
|
import fs12 from "fs-extra";
|
|
3746
|
-
import
|
|
4020
|
+
import path10 from "path";
|
|
3747
4021
|
import axios9 from "axios";
|
|
3748
4022
|
import crypto6 from "crypto";
|
|
3749
4023
|
var TencentUploader = class _TencentUploader {
|
|
@@ -3783,7 +4057,7 @@ var TencentUploader = class _TencentUploader {
|
|
|
3783
4057
|
const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
|
|
3784
4058
|
if (!p || !fs12.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
|
|
3785
4059
|
const stats = fs12.statSync(p);
|
|
3786
|
-
const fileName =
|
|
4060
|
+
const fileName = path10.basename(p);
|
|
3787
4061
|
logger.info(`[Tencent] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${fileName}`);
|
|
3788
4062
|
const uploadInfo = await this.getFileUploadInfo(auth, stats.size, fileName, pkgName);
|
|
3789
4063
|
await this.uploadToCos(uploadInfo.pre_sign_url, p, stats.size);
|
|
@@ -3987,6 +4261,225 @@ var TencentUploader = class _TencentUploader {
|
|
|
3987
4261
|
}
|
|
3988
4262
|
};
|
|
3989
4263
|
|
|
4264
|
+
// src/adapters/google_play_uploader.ts
|
|
4265
|
+
import fs13 from "fs-extra";
|
|
4266
|
+
import path11 from "path";
|
|
4267
|
+
import axios10 from "axios";
|
|
4268
|
+
import { GoogleAuth } from "google-auth-library";
|
|
4269
|
+
var ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
|
|
4270
|
+
var PUBLISH_API_ROOT = "https://androidpublisher.googleapis.com/androidpublisher/v3";
|
|
4271
|
+
var UPLOAD_API_ROOT = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3";
|
|
4272
|
+
var GOOGLE_PLAY_CONSOLE_URL = "https://play.google.com/console/developers";
|
|
4273
|
+
var GOOGLE_PLAY_INTERNAL_API_TRACK = "internal";
|
|
4274
|
+
var UPLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4275
|
+
var GooglePlayUploader = class {
|
|
4276
|
+
name = "Google Play";
|
|
4277
|
+
httpClient;
|
|
4278
|
+
getAccessToken;
|
|
4279
|
+
constructor(dependencies = {}) {
|
|
4280
|
+
this.httpClient = dependencies.httpClient || axios10;
|
|
4281
|
+
this.getAccessToken = dependencies.getAccessToken || this.createAccessToken.bind(this);
|
|
4282
|
+
}
|
|
4283
|
+
validateConfig(config) {
|
|
4284
|
+
const googleConfig = config?.publish?.google_play;
|
|
4285
|
+
if (!googleConfig?.enable) return false;
|
|
4286
|
+
if (!googleConfig.packageName || typeof googleConfig.packageName !== "string") {
|
|
4287
|
+
logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11 packageName");
|
|
4288
|
+
return false;
|
|
4289
|
+
}
|
|
4290
|
+
const track = googleConfig.track || "internal";
|
|
4291
|
+
if (track !== "internal") {
|
|
4292
|
+
logger.error("Google Play \u9996\u7248\u4EC5\u652F\u6301 internal \u8F68\u9053");
|
|
4293
|
+
return false;
|
|
4294
|
+
}
|
|
4295
|
+
const auth = googleConfig.auth;
|
|
4296
|
+
const hasCredentialContent = typeof auth?.serviceAccountJsonContent === "string" && auth.serviceAccountJsonContent.trim().length > 0;
|
|
4297
|
+
const credentialPath = auth?.serviceAccountJsonPath;
|
|
4298
|
+
const hasCredentialPath = typeof credentialPath === "string" && credentialPath.trim().length > 0 && fs13.existsSync(path11.resolve(credentialPath));
|
|
4299
|
+
if (!hasCredentialContent && !hasCredentialPath) {
|
|
4300
|
+
logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11\u6709\u6548\u7684\u670D\u52A1\u8D26\u53F7 JSON \u6587\u4EF6\u6216\u5185\u5BB9");
|
|
4301
|
+
return false;
|
|
4302
|
+
}
|
|
4303
|
+
return true;
|
|
4304
|
+
}
|
|
4305
|
+
canHandle(filePath) {
|
|
4306
|
+
return path11.extname(filePath).toLowerCase() === ".aab";
|
|
4307
|
+
}
|
|
4308
|
+
async upload(filePath, config, _metadata) {
|
|
4309
|
+
if (!this.validateConfig(config)) {
|
|
4310
|
+
return this.failure("Google Play \u914D\u7F6E\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u5305\u540D\u3001internal \u8F68\u9053\u548C\u670D\u52A1\u8D26\u53F7\u6587\u4EF6");
|
|
4311
|
+
}
|
|
4312
|
+
if (!this.canHandle(filePath)) {
|
|
4313
|
+
return this.failure("Google Play \u4EC5\u63A5\u53D7 Android App Bundle\uFF08.aab\uFF09");
|
|
4314
|
+
}
|
|
4315
|
+
if (!fs13.existsSync(filePath)) {
|
|
4316
|
+
return this.failure(`\u672A\u627E\u5230\u5F85\u4E0A\u4F20\u7684 AAB\uFF1A${filePath}`);
|
|
4317
|
+
}
|
|
4318
|
+
const googleConfig = config.publish.google_play;
|
|
4319
|
+
const packageName = googleConfig.packageName.trim();
|
|
4320
|
+
const encodedPackageName = encodeURIComponent(packageName);
|
|
4321
|
+
let editId;
|
|
4322
|
+
let accessToken;
|
|
4323
|
+
let committed = false;
|
|
4324
|
+
let stage = "create_edit";
|
|
4325
|
+
logger.info(`\u51C6\u5907\u5C06 ${path11.basename(filePath)} \u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F`);
|
|
4326
|
+
try {
|
|
4327
|
+
accessToken = await this.getAccessToken(googleConfig.auth);
|
|
4328
|
+
const requestConfig = {
|
|
4329
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
4330
|
+
};
|
|
4331
|
+
const editResponse = await this.httpClient.post(
|
|
4332
|
+
`${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits`,
|
|
4333
|
+
{},
|
|
4334
|
+
requestConfig
|
|
4335
|
+
);
|
|
4336
|
+
editId = editResponse.data.id;
|
|
4337
|
+
if (!editId) {
|
|
4338
|
+
throw new Error("Google Play \u672A\u8FD4\u56DE editId");
|
|
4339
|
+
}
|
|
4340
|
+
stage = "upload_bundle";
|
|
4341
|
+
const encodedEditId = encodeURIComponent(editId);
|
|
4342
|
+
const bundleResponse = await this.httpClient.post(
|
|
4343
|
+
`${UPLOAD_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/bundles?uploadType=media`,
|
|
4344
|
+
fs13.createReadStream(filePath),
|
|
4345
|
+
{
|
|
4346
|
+
timeout: UPLOAD_TIMEOUT_MS,
|
|
4347
|
+
maxBodyLength: Infinity,
|
|
4348
|
+
maxContentLength: Infinity,
|
|
4349
|
+
headers: {
|
|
4350
|
+
Authorization: `Bearer ${accessToken}`,
|
|
4351
|
+
"Content-Type": "application/octet-stream",
|
|
4352
|
+
"Content-Length": fs13.statSync(filePath).size
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
);
|
|
4356
|
+
const versionCode = bundleResponse.data.versionCode;
|
|
4357
|
+
if (!Number.isInteger(versionCode)) {
|
|
4358
|
+
throw new Error("Google Play \u4E0A\u4F20\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u6709\u6548\u7684 versionCode");
|
|
4359
|
+
}
|
|
4360
|
+
stage = "update_internal_track";
|
|
4361
|
+
await this.httpClient.put(
|
|
4362
|
+
`${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/tracks/${GOOGLE_PLAY_INTERNAL_API_TRACK}`,
|
|
4363
|
+
{
|
|
4364
|
+
track: GOOGLE_PLAY_INTERNAL_API_TRACK,
|
|
4365
|
+
releases: [
|
|
4366
|
+
{
|
|
4367
|
+
versionCodes: [String(versionCode)],
|
|
4368
|
+
status: "draft"
|
|
4369
|
+
}
|
|
4370
|
+
]
|
|
4371
|
+
},
|
|
4372
|
+
requestConfig
|
|
4373
|
+
);
|
|
4374
|
+
stage = "validate_edit";
|
|
4375
|
+
await this.httpClient.post(
|
|
4376
|
+
`${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:validate`,
|
|
4377
|
+
{},
|
|
4378
|
+
requestConfig
|
|
4379
|
+
);
|
|
4380
|
+
stage = "commit_edit";
|
|
4381
|
+
await this.httpClient.post(
|
|
4382
|
+
`${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:commit`,
|
|
4383
|
+
{},
|
|
4384
|
+
requestConfig
|
|
4385
|
+
);
|
|
4386
|
+
committed = true;
|
|
4387
|
+
logger.success(`Google Play internal \u8349\u7A3F\u5DF2\u521B\u5EFA\uFF08versionCode ${versionCode}\uFF09`);
|
|
4388
|
+
return {
|
|
4389
|
+
platform: "google_play",
|
|
4390
|
+
success: true,
|
|
4391
|
+
message: `AAB \u5DF2\u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F\uFF08versionCode ${versionCode}\uFF09`,
|
|
4392
|
+
consoleUrl: GOOGLE_PLAY_CONSOLE_URL,
|
|
4393
|
+
data: {
|
|
4394
|
+
editId,
|
|
4395
|
+
versionCode,
|
|
4396
|
+
track: "internal",
|
|
4397
|
+
apiTrack: GOOGLE_PLAY_INTERNAL_API_TRACK,
|
|
4398
|
+
status: "draft",
|
|
4399
|
+
committed: true
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
if (editId && accessToken && !committed) {
|
|
4404
|
+
await this.deleteEditBestEffort(encodedPackageName, editId, accessToken);
|
|
4405
|
+
}
|
|
4406
|
+
const message = this.describeError(error, stage);
|
|
4407
|
+
logger.error(`Google Play \u4E0A\u4F20\u5931\u8D25\uFF1A${message}`);
|
|
4408
|
+
return this.failure(message, error);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
async createAccessToken(authConfig) {
|
|
4412
|
+
let credentials;
|
|
4413
|
+
if (authConfig.serviceAccountJsonContent?.trim()) {
|
|
4414
|
+
try {
|
|
4415
|
+
credentials = JSON.parse(authConfig.serviceAccountJsonContent);
|
|
4416
|
+
} catch {
|
|
4417
|
+
throw new Error("\u670D\u52A1\u8D26\u53F7 JSON \u5185\u5BB9\u683C\u5F0F\u65E0\u6548");
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
const keyFile = !credentials && authConfig.serviceAccountJsonPath ? path11.resolve(authConfig.serviceAccountJsonPath) : void 0;
|
|
4421
|
+
const auth = new GoogleAuth({
|
|
4422
|
+
scopes: [ANDROID_PUBLISHER_SCOPE],
|
|
4423
|
+
keyFile,
|
|
4424
|
+
credentials
|
|
4425
|
+
});
|
|
4426
|
+
const token = await auth.getAccessToken();
|
|
4427
|
+
if (!token) {
|
|
4428
|
+
throw new Error("Google Play \u8BA4\u8BC1\u672A\u8FD4\u56DE\u8BBF\u95EE\u4EE4\u724C");
|
|
4429
|
+
}
|
|
4430
|
+
return token;
|
|
4431
|
+
}
|
|
4432
|
+
async deleteEditBestEffort(encodedPackageName, editId, accessToken) {
|
|
4433
|
+
try {
|
|
4434
|
+
await this.httpClient.delete(
|
|
4435
|
+
`${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodeURIComponent(editId)}`,
|
|
4436
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
4437
|
+
);
|
|
4438
|
+
} catch {
|
|
4439
|
+
logger.warn("Google Play \u672A\u63D0\u4EA4 edit \u7684\u81EA\u52A8\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u5728 Play Console \u4E2D\u68C0\u67E5\u8349\u7A3F\u72B6\u6001");
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
describeError(error, stage) {
|
|
4443
|
+
const status = error?.response?.status;
|
|
4444
|
+
const rawMessage = error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || "\u672A\u77E5\u9519\u8BEF";
|
|
4445
|
+
const safeMessage = this.redactAndTruncate(String(rawMessage));
|
|
4446
|
+
if (/version\s*code|versionCode/i.test(safeMessage)) {
|
|
4447
|
+
return `Google Play \u62D2\u7EDD\u8BE5 versionCode\uFF0C\u8BF7\u63D0\u9AD8 Flutter \u6784\u5EFA\u53F7\u540E\u91CD\u65B0\u751F\u6210 AAB\uFF1A${safeMessage}`;
|
|
4448
|
+
}
|
|
4449
|
+
if (status === 401) {
|
|
4450
|
+
return "Google Play \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u8D26\u53F7 JSON \u662F\u5426\u6709\u6548";
|
|
4451
|
+
}
|
|
4452
|
+
if (status === 403) {
|
|
4453
|
+
return "Google Play \u62D2\u7EDD\u8BBF\u95EE\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u8D26\u53F7\u5DF2\u52A0\u5165 Play Console \u5E76\u5177\u6709\u5185\u90E8\u6D4B\u8BD5\u7248\u672C\u4E0A\u4F20\u6743\u9650";
|
|
4454
|
+
}
|
|
4455
|
+
if (status === 404) {
|
|
4456
|
+
if (stage === "update_internal_track") {
|
|
4457
|
+
return "Google Play \u4E2D\u672A\u627E\u5230 internal \u5185\u90E8\u6D4B\u8BD5\u8F68\u9053\uFF0C\u8BF7\u786E\u8BA4\u5E94\u7528\u5DF2\u5728 Play Console \u521B\u5EFA\u5185\u90E8\u6D4B\u8BD5\u8F68\u9053";
|
|
4458
|
+
}
|
|
4459
|
+
if (stage !== "create_edit") {
|
|
4460
|
+
return `Google Play API \u5728 ${stage} \u9636\u6BB5\u672A\u627E\u5230\u76EE\u6807\u8D44\u6E90\uFF1A${safeMessage}`;
|
|
4461
|
+
}
|
|
4462
|
+
return "Google Play \u4E2D\u672A\u627E\u5230\u53EF\u4F9B API \u7F16\u8F91\u7684\u5E94\u7528\u3002\u8BF7\u786E\u8BA4 packageName \u5B8C\u5168\u4E00\u81F4\uFF1B\u82E5\u8FD9\u662F\u9996\u6B21\u4E0A\u4F20\uFF0C\u8BF7\u5148\u5728 Play Console \u624B\u5DE5\u4E0A\u4F20\u5E76\u4FDD\u5B58\u4E00\u4E2A APK/AAB\uFF0C\u518D\u91CD\u8BD5 API \u4E0A\u4F20";
|
|
4463
|
+
}
|
|
4464
|
+
if (error?.code === "ECONNABORTED" || /timeout/i.test(safeMessage)) {
|
|
4465
|
+
return "Google Play \u4E0A\u4F20\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u672C\u5730 AAB \u672A\u88AB\u5220\u9664";
|
|
4466
|
+
}
|
|
4467
|
+
return `Google Play API \u8BF7\u6C42\u5931\u8D25${status ? `\uFF08HTTP ${status}\uFF09` : ""}\uFF1A${safeMessage}`;
|
|
4468
|
+
}
|
|
4469
|
+
redactAndTruncate(message) {
|
|
4470
|
+
return message.replace(/-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/gi, "[\u5DF2\u8131\u654F\u79C1\u94A5]").replace(/Bearer\s+[^\s"']+/gi, "Bearer [\u5DF2\u8131\u654F]").replace(/"private_key"\s*:\s*"[^"]*"/gi, '"private_key":"[\u5DF2\u8131\u654F]"').replace(/"access_token"\s*:\s*"[^"]*"/gi, '"access_token":"[\u5DF2\u8131\u654F]"').slice(0, 500);
|
|
4471
|
+
}
|
|
4472
|
+
failure(message, error) {
|
|
4473
|
+
return {
|
|
4474
|
+
platform: "google_play",
|
|
4475
|
+
success: false,
|
|
4476
|
+
message,
|
|
4477
|
+
// 原始 Axios 错误可能包含 Authorization 请求头,只返回脱敏后的 Error。
|
|
4478
|
+
error: error ? new Error(message) : void 0
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
|
|
3990
4483
|
// src/utils/upload-manager.ts
|
|
3991
4484
|
var GROUPED_ARTIFACT_PLATFORMS = /* @__PURE__ */ new Set(["huawei", "xiaomi", "oppo", "vivo", "tencent"]);
|
|
3992
4485
|
var UploadManager = class {
|
|
@@ -4010,6 +4503,7 @@ var UploadManager = class {
|
|
|
4010
4503
|
this.registerUploader("oppo", new OppoUploader());
|
|
4011
4504
|
this.registerUploader("vivo", new VivoUploader());
|
|
4012
4505
|
this.registerUploader("tencent", new TencentUploader());
|
|
4506
|
+
this.registerUploader("google_play", new GooglePlayUploader());
|
|
4013
4507
|
}
|
|
4014
4508
|
/**
|
|
4015
4509
|
* 注册上传器
|
|
@@ -4028,6 +4522,8 @@ var UploadManager = class {
|
|
|
4028
4522
|
if (config?.publish?.oppo?.enable) enabled.push("oppo");
|
|
4029
4523
|
if (config?.publish?.vivo?.enable) enabled.push("vivo");
|
|
4030
4524
|
if (config?.publish?.tencent?.enable) enabled.push("tencent");
|
|
4525
|
+
if (config?.publish?.harmony?.enable) enabled.push("harmony");
|
|
4526
|
+
if (config?.publish?.google_play?.enable) enabled.push("google_play");
|
|
4031
4527
|
if (config?.publish?.app_store?.enable) {
|
|
4032
4528
|
const mode = config.publish.app_store.mode || "app_store";
|
|
4033
4529
|
enabled.push(mode);
|
|
@@ -4128,7 +4624,7 @@ var UploadManager = class {
|
|
|
4128
4624
|
}
|
|
4129
4625
|
logger.info(`Scheduled ${tasks.length} upload task(s) for ${validPlatforms.length} platform(s)`);
|
|
4130
4626
|
const uploadPromises = tasks.map((task) => {
|
|
4131
|
-
logger.info(`[${task.platform}] Matched file: ${
|
|
4627
|
+
logger.info(`[${task.platform}] Matched file: ${path12.basename(task.file)}`);
|
|
4132
4628
|
return this.uploadToPlatform(
|
|
4133
4629
|
task.platform,
|
|
4134
4630
|
task.file,
|
|
@@ -4171,7 +4667,7 @@ var UploadManager = class {
|
|
|
4171
4667
|
artifactPath: artifactSelection
|
|
4172
4668
|
};
|
|
4173
4669
|
await this.validateArtifactSelection(platform, artifactSelection, effectiveMetadata);
|
|
4174
|
-
const pkgInfo = await parsePackageInfo(filePath, {
|
|
4670
|
+
const pkgInfo = path12.extname(filePath).toLowerCase() === ".aab" ? null : await parsePackageInfo(filePath, {
|
|
4175
4671
|
androidSdkPath: effectiveMetadata.globalSettings?.androidSdkPath
|
|
4176
4672
|
});
|
|
4177
4673
|
if (pkgInfo) {
|
|
@@ -4233,7 +4729,7 @@ var UploadManager = class {
|
|
|
4233
4729
|
);
|
|
4234
4730
|
}
|
|
4235
4731
|
inferArtifactKey(filePath, index) {
|
|
4236
|
-
const fileName =
|
|
4732
|
+
const fileName = path12.basename(filePath).toLowerCase();
|
|
4237
4733
|
if (fileName.includes("arm64-v8a")) return "arm64-v8a";
|
|
4238
4734
|
if (fileName.includes("armeabi-v7a")) return "armeabi-v7a";
|
|
4239
4735
|
if (fileName.includes("x86_64")) return "x86_64";
|
|
@@ -4390,7 +4886,7 @@ var UploadManager = class {
|
|
|
4390
4886
|
};
|
|
4391
4887
|
|
|
4392
4888
|
// src/validators/config_validator.ts
|
|
4393
|
-
import
|
|
4889
|
+
import fs14 from "fs-extra";
|
|
4394
4890
|
import yaml from "js-yaml";
|
|
4395
4891
|
|
|
4396
4892
|
// src/validators/env_validator.ts
|
|
@@ -4416,20 +4912,22 @@ var ConfigValidator = class {
|
|
|
4416
4912
|
* @param filePath 配置文件路径
|
|
4417
4913
|
*/
|
|
4418
4914
|
static loadAndValidate(filePath) {
|
|
4419
|
-
if (!
|
|
4915
|
+
if (!fs14.existsSync(filePath)) {
|
|
4420
4916
|
logger.error(`Config file not found: ${filePath}`);
|
|
4421
4917
|
return null;
|
|
4422
4918
|
}
|
|
4423
|
-
let content = fs13.readFileSync(filePath, "utf8");
|
|
4424
|
-
const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
4425
|
-
const matches = [...content.matchAll(envRegex)];
|
|
4426
|
-
const requiredEnvs = Array.from(new Set(matches.map((m) => m[1])));
|
|
4427
|
-
if (!EnvValidator.validate(requiredEnvs)) {
|
|
4428
|
-
return null;
|
|
4429
|
-
}
|
|
4430
|
-
content = content.replace(envRegex, (_, varName) => process.env[varName] || "");
|
|
4431
4919
|
try {
|
|
4432
|
-
const
|
|
4920
|
+
const content = fs14.readFileSync(filePath, "utf8");
|
|
4921
|
+
const rawConfig = yaml.load(content);
|
|
4922
|
+
if (!rawConfig || typeof rawConfig !== "object") {
|
|
4923
|
+
logger.error("Config file must contain a YAML object");
|
|
4924
|
+
return null;
|
|
4925
|
+
}
|
|
4926
|
+
const requiredEnvs = this.collectRequiredEnvVars(rawConfig);
|
|
4927
|
+
if (!EnvValidator.validate(requiredEnvs)) {
|
|
4928
|
+
return null;
|
|
4929
|
+
}
|
|
4930
|
+
const config = this.resolveEnvVars(rawConfig);
|
|
4433
4931
|
if (!this.validateSchema(config)) {
|
|
4434
4932
|
return null;
|
|
4435
4933
|
}
|
|
@@ -4439,15 +4937,71 @@ var ConfigValidator = class {
|
|
|
4439
4937
|
return null;
|
|
4440
4938
|
}
|
|
4441
4939
|
}
|
|
4940
|
+
/**
|
|
4941
|
+
* 只收集本次实际启用配置中的环境变量,避免禁用平台阻塞其它上传任务。
|
|
4942
|
+
*/
|
|
4943
|
+
static collectRequiredEnvVars(config) {
|
|
4944
|
+
const activeConfig = {
|
|
4945
|
+
...config,
|
|
4946
|
+
publish: Object.fromEntries(
|
|
4947
|
+
Object.entries(config.publish || {}).filter(([, value]) => value?.enable === true)
|
|
4948
|
+
)
|
|
4949
|
+
};
|
|
4950
|
+
const requiredEnvs = /* @__PURE__ */ new Set();
|
|
4951
|
+
const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
4952
|
+
const visit = (value) => {
|
|
4953
|
+
if (typeof value === "string") {
|
|
4954
|
+
for (const match of value.matchAll(envRegex)) {
|
|
4955
|
+
requiredEnvs.add(match[1]);
|
|
4956
|
+
}
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
if (Array.isArray(value)) {
|
|
4960
|
+
value.forEach(visit);
|
|
4961
|
+
return;
|
|
4962
|
+
}
|
|
4963
|
+
if (value && typeof value === "object") {
|
|
4964
|
+
Object.values(value).forEach(visit);
|
|
4965
|
+
}
|
|
4966
|
+
};
|
|
4967
|
+
visit(activeConfig);
|
|
4968
|
+
return Array.from(requiredEnvs);
|
|
4969
|
+
}
|
|
4970
|
+
/** 在内存中递归解析环境变量,不重新拼接 YAML,避免密钥字符破坏 YAML 结构。 */
|
|
4971
|
+
static resolveEnvVars(value) {
|
|
4972
|
+
if (typeof value === "string") {
|
|
4973
|
+
return value.replace(
|
|
4974
|
+
/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
|
|
4975
|
+
(_, varName) => process.env[varName] || ""
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4978
|
+
if (Array.isArray(value)) {
|
|
4979
|
+
return value.map((item) => this.resolveEnvVars(item));
|
|
4980
|
+
}
|
|
4981
|
+
if (value && typeof value === "object") {
|
|
4982
|
+
return Object.fromEntries(
|
|
4983
|
+
Object.entries(value).map(([key, item]) => [key, this.resolveEnvVars(item)])
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4986
|
+
return value;
|
|
4987
|
+
}
|
|
4442
4988
|
/**
|
|
4443
4989
|
* 验证配置 Schema
|
|
4444
4990
|
* @param config 配置对象
|
|
4445
4991
|
*/
|
|
4446
4992
|
static validateSchema(config) {
|
|
4447
4993
|
if (config.publish?.app_store?.enable) {
|
|
4994
|
+
const mode = config.publish.app_store.mode || "app_store";
|
|
4448
4995
|
const auth = config.publish.app_store.auth;
|
|
4449
|
-
|
|
4450
|
-
|
|
4996
|
+
const hasCliAuth = Boolean(auth?.appleId && auth?.appPassword);
|
|
4997
|
+
const hasApiAuth = Boolean(
|
|
4998
|
+
auth?.apiKeyId && auth?.apiIssuerId && (auth?.privateKeyPath || auth?.privateKeyContent)
|
|
4999
|
+
);
|
|
5000
|
+
const hasRequiredAuth = mode === "adhoc" || (mode === "testflight" ? hasApiAuth : hasCliAuth || hasApiAuth);
|
|
5001
|
+
if (!hasRequiredAuth) {
|
|
5002
|
+
logger.error(
|
|
5003
|
+
mode === "testflight" ? "TestFlight enabled but missing complete API key credentials" : "App Store enabled but missing Apple ID credentials or complete API key credentials"
|
|
5004
|
+
);
|
|
4451
5005
|
return false;
|
|
4452
5006
|
}
|
|
4453
5007
|
}
|
|
@@ -4493,23 +5047,38 @@ var ConfigValidator = class {
|
|
|
4493
5047
|
return false;
|
|
4494
5048
|
}
|
|
4495
5049
|
}
|
|
5050
|
+
if (config.publish?.google_play?.enable) {
|
|
5051
|
+
const googlePlay = config.publish.google_play;
|
|
5052
|
+
const auth = googlePlay.auth;
|
|
5053
|
+
if (!googlePlay.packageName || !auth?.serviceAccountJsonPath && !auth?.serviceAccountJsonContent) {
|
|
5054
|
+
logger.error(
|
|
5055
|
+
"Google Play enabled but missing packageName or service account JSON credentials"
|
|
5056
|
+
);
|
|
5057
|
+
return false;
|
|
5058
|
+
}
|
|
5059
|
+
if (googlePlay.track && googlePlay.track !== "internal") {
|
|
5060
|
+
logger.error("Google Play first release only supports the internal track");
|
|
5061
|
+
return false;
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
4496
5064
|
return true;
|
|
4497
5065
|
}
|
|
4498
5066
|
};
|
|
4499
5067
|
|
|
4500
5068
|
// src/cli.ts
|
|
4501
|
-
import
|
|
4502
|
-
import
|
|
5069
|
+
import path13 from "path";
|
|
5070
|
+
import fs15 from "fs-extra";
|
|
4503
5071
|
import yaml2 from "js-yaml";
|
|
4504
|
-
var VERSION = "0.1.
|
|
5072
|
+
var VERSION = "0.1.2";
|
|
4505
5073
|
function printHelp() {
|
|
4506
5074
|
console.log(`
|
|
4507
5075
|
app-ship v${VERSION} \u2014 Multi-platform app store uploader
|
|
4508
5076
|
|
|
4509
5077
|
Usage:
|
|
4510
|
-
app-ship upload -c <config> -f <file> [--platform pgyer,huawei]
|
|
5078
|
+
app-ship upload -c <config> -f <file> [--platform pgyer,huawei] [--project <dir>]
|
|
4511
5079
|
app-ship platforms List supported platforms
|
|
4512
5080
|
app-ship init Generate sample config file
|
|
5081
|
+
app-ship history [--project <dir>] [--json]
|
|
4513
5082
|
app-ship --help Show this help
|
|
4514
5083
|
app-ship --version Show version
|
|
4515
5084
|
|
|
@@ -4520,7 +5089,13 @@ Upload Options:
|
|
|
4520
5089
|
-v, --version <ver> Version string (default: from config or "1.0.0")
|
|
4521
5090
|
-b, --build-number <n> Build number (default: "1")
|
|
4522
5091
|
--changelog <text> Changelog / release notes
|
|
5092
|
+
--project <dir> Project directory recorded in the release receipt
|
|
5093
|
+
--confirm-google-play Confirm upload to Google Play internal draft
|
|
4523
5094
|
--verbose Show detailed output
|
|
5095
|
+
|
|
5096
|
+
History Options:
|
|
5097
|
+
--project <dir> Project directory (default: current directory)
|
|
5098
|
+
--json Print structured receipts as JSON
|
|
4524
5099
|
`);
|
|
4525
5100
|
}
|
|
4526
5101
|
function parseArgs(argv) {
|
|
@@ -4534,6 +5109,12 @@ function parseArgs(argv) {
|
|
|
4534
5109
|
args.showVersion = true;
|
|
4535
5110
|
} else if (arg === "--verbose") {
|
|
4536
5111
|
args.verbose = true;
|
|
5112
|
+
} else if (arg === "--confirm-google-play") {
|
|
5113
|
+
args.confirmGooglePlay = true;
|
|
5114
|
+
} else if (arg === "--json") {
|
|
5115
|
+
args.json = true;
|
|
5116
|
+
} else if (arg === "--project" && argv[i + 1]) {
|
|
5117
|
+
args.project = argv[++i];
|
|
4537
5118
|
} else if ((arg === "-c" || arg === "--config") && argv[i + 1]) {
|
|
4538
5119
|
args.config = argv[++i];
|
|
4539
5120
|
} else if ((arg === "-f" || arg === "--file") && argv[i + 1]) {
|
|
@@ -4564,9 +5145,9 @@ async function cmdUpload(args) {
|
|
|
4564
5145
|
logger.error("Missing file path. Use: app-ship upload -c <config.yaml> -f <file>");
|
|
4565
5146
|
process.exit(1);
|
|
4566
5147
|
}
|
|
4567
|
-
const absConfig =
|
|
4568
|
-
const absFile =
|
|
4569
|
-
if (!
|
|
5148
|
+
const absConfig = path13.resolve(configPath);
|
|
5149
|
+
const absFile = path13.resolve(filePath);
|
|
5150
|
+
if (!fs15.existsSync(absFile)) {
|
|
4570
5151
|
logger.error(`File not found: ${absFile}`);
|
|
4571
5152
|
process.exit(1);
|
|
4572
5153
|
}
|
|
@@ -4575,27 +5156,92 @@ async function cmdUpload(args) {
|
|
|
4575
5156
|
logger.error("Config validation failed");
|
|
4576
5157
|
process.exit(1);
|
|
4577
5158
|
}
|
|
4578
|
-
const platforms = args.platform ?
|
|
5159
|
+
const platforms = args.platform ? Array.from(
|
|
5160
|
+
new Set(
|
|
5161
|
+
args.platform.split(",").map((platform) => platform.trim()).filter(Boolean)
|
|
5162
|
+
)
|
|
5163
|
+
) : void 0;
|
|
5164
|
+
const manager = new UploadManager({
|
|
5165
|
+
maxConcurrent: 3,
|
|
5166
|
+
continueOnError: true,
|
|
5167
|
+
verbose: !!args.verbose
|
|
5168
|
+
});
|
|
5169
|
+
const effectivePlatforms = platforms || manager.getEnabledPlatforms(config);
|
|
5170
|
+
if (effectivePlatforms.length === 0) {
|
|
5171
|
+
logger.error("No upload platform is enabled. Enable one in config or pass --platform.");
|
|
5172
|
+
process.exit(1);
|
|
5173
|
+
}
|
|
4579
5174
|
const metadata = {
|
|
4580
5175
|
version: args.ver || config.version || "1.0.0",
|
|
4581
5176
|
buildNumber: args.buildNumber || "1",
|
|
4582
|
-
changelog: args.changelog || ""
|
|
5177
|
+
changelog: args.changelog || "",
|
|
5178
|
+
platform: effectivePlatforms
|
|
4583
5179
|
};
|
|
4584
5180
|
logger.title("App Ship \u2014 Starting Upload");
|
|
4585
5181
|
logger.info(`File: ${absFile}`);
|
|
4586
5182
|
logger.info(`Config: ${absConfig}`);
|
|
4587
|
-
|
|
4588
|
-
|
|
5183
|
+
logger.info(`Platforms: ${effectivePlatforms.join(", ") || "none"}`);
|
|
5184
|
+
if (effectivePlatforms.includes("google_play")) {
|
|
5185
|
+
if (path13.extname(absFile).toLowerCase() !== ".aab") {
|
|
5186
|
+
logger.error("Google Play only accepts Android App Bundle (.aab) files");
|
|
5187
|
+
process.exit(1);
|
|
5188
|
+
}
|
|
5189
|
+
logger.info(`Google Play package: ${config.publish?.google_play?.packageName || "unknown"}`);
|
|
5190
|
+
logger.info("Google Play target: internal draft");
|
|
5191
|
+
if (!args.confirmGooglePlay) {
|
|
5192
|
+
logger.error(
|
|
5193
|
+
"Google Play upload requires explicit confirmation. Review the preview and rerun with --confirm-google-play"
|
|
5194
|
+
);
|
|
5195
|
+
process.exit(1);
|
|
5196
|
+
}
|
|
4589
5197
|
}
|
|
4590
5198
|
logger.newLine();
|
|
4591
|
-
const
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
5199
|
+
const startedAt = Date.now();
|
|
5200
|
+
const report = await manager.uploadToMultiplePlatforms(
|
|
5201
|
+
absFile,
|
|
5202
|
+
config,
|
|
5203
|
+
metadata,
|
|
5204
|
+
effectivePlatforms
|
|
5205
|
+
);
|
|
5206
|
+
try {
|
|
5207
|
+
const receipt = await new ReleaseReceiptStore().save({
|
|
5208
|
+
projectRoot: resolveReleaseProjectRoot(args.project, absConfig),
|
|
5209
|
+
kind: "upload",
|
|
5210
|
+
appName: metadata.appName,
|
|
5211
|
+
packageName: resolvePackageName(config),
|
|
5212
|
+
versionName: metadata.version,
|
|
5213
|
+
buildNumber: metadata.buildNumber,
|
|
5214
|
+
changelog: metadata.changelog,
|
|
5215
|
+
artifactPaths: absFile,
|
|
5216
|
+
targets: createUploadTargets(report.results),
|
|
5217
|
+
startedAt
|
|
5218
|
+
});
|
|
5219
|
+
logger.info(`Receipt: ${receipt.id}`);
|
|
5220
|
+
} catch (error) {
|
|
5221
|
+
logger.warn(`Failed to save release receipt: ${error.message}`);
|
|
5222
|
+
}
|
|
4597
5223
|
process.exit(report.failureCount > 0 ? 1 : 0);
|
|
4598
5224
|
}
|
|
5225
|
+
async function cmdHistory(args) {
|
|
5226
|
+
const projectRoot = path13.resolve(args.project || process.cwd());
|
|
5227
|
+
const receipts = await new ReleaseReceiptStore().list(projectRoot);
|
|
5228
|
+
if (args.json) {
|
|
5229
|
+
console.log(JSON.stringify(receipts, null, 2));
|
|
5230
|
+
return;
|
|
5231
|
+
}
|
|
5232
|
+
if (receipts.length === 0) {
|
|
5233
|
+
logger.info(`No release receipts found for ${projectRoot}`);
|
|
5234
|
+
return;
|
|
5235
|
+
}
|
|
5236
|
+
logger.title(`Release Receipts \u2014 ${projectRoot}`);
|
|
5237
|
+
receipts.forEach((receipt, index) => {
|
|
5238
|
+
if (index > 0) console.log("");
|
|
5239
|
+
console.log(formatReleaseReceipt(receipt));
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
function resolvePackageName(config) {
|
|
5243
|
+
return config.appId || config.publish?.google_play?.packageName || Object.values(config.publish || {}).find((value) => value?.packageName)?.packageName;
|
|
5244
|
+
}
|
|
4599
5245
|
function cmdPlatforms() {
|
|
4600
5246
|
const manager = new UploadManager();
|
|
4601
5247
|
const platforms = manager.getAvailablePlatforms();
|
|
@@ -4608,7 +5254,7 @@ Total: ${platforms.length} platforms`);
|
|
|
4608
5254
|
}
|
|
4609
5255
|
function cmdInit() {
|
|
4610
5256
|
const filename = "app-ship.yaml";
|
|
4611
|
-
if (
|
|
5257
|
+
if (fs15.existsSync(filename)) {
|
|
4612
5258
|
logger.warn(`${filename} already exists. Skipping.`);
|
|
4613
5259
|
return;
|
|
4614
5260
|
}
|
|
@@ -4616,7 +5262,7 @@ function cmdInit() {
|
|
|
4616
5262
|
version: "1.0.0",
|
|
4617
5263
|
publish: {
|
|
4618
5264
|
pgyer: {
|
|
4619
|
-
enable:
|
|
5265
|
+
enable: false,
|
|
4620
5266
|
apiKey: "YOUR_PGYER_API_KEY"
|
|
4621
5267
|
},
|
|
4622
5268
|
huawei: {
|
|
@@ -4652,6 +5298,7 @@ function cmdInit() {
|
|
|
4652
5298
|
tencent: {
|
|
4653
5299
|
enable: false,
|
|
4654
5300
|
auth: {
|
|
5301
|
+
userId: "",
|
|
4655
5302
|
appId: "",
|
|
4656
5303
|
accessSecret: ""
|
|
4657
5304
|
}
|
|
@@ -4670,10 +5317,18 @@ function cmdInit() {
|
|
|
4670
5317
|
appleId: "",
|
|
4671
5318
|
appPassword: ""
|
|
4672
5319
|
}
|
|
5320
|
+
},
|
|
5321
|
+
google_play: {
|
|
5322
|
+
enable: false,
|
|
5323
|
+
auth: {
|
|
5324
|
+
serviceAccountJsonPath: "./service-account.json"
|
|
5325
|
+
},
|
|
5326
|
+
packageName: "com.example.app",
|
|
5327
|
+
track: "internal"
|
|
4673
5328
|
}
|
|
4674
5329
|
}
|
|
4675
5330
|
};
|
|
4676
|
-
|
|
5331
|
+
fs15.writeFileSync(filename, yaml2.dump(sample, { indent: 2 }), "utf8");
|
|
4677
5332
|
logger.success(`Created ${filename}`);
|
|
4678
5333
|
logger.info("Edit the file to configure your platform credentials.");
|
|
4679
5334
|
}
|
|
@@ -4698,6 +5353,9 @@ async function main() {
|
|
|
4698
5353
|
case "init":
|
|
4699
5354
|
cmdInit();
|
|
4700
5355
|
break;
|
|
5356
|
+
case "history":
|
|
5357
|
+
await cmdHistory(args);
|
|
5358
|
+
break;
|
|
4701
5359
|
default:
|
|
4702
5360
|
printHelp();
|
|
4703
5361
|
break;
|