appstore-tools 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
4
+ import { AppStoreConnectClient, InfrastructureError } from "./api/client.js";
5
+ import { appsListCommand } from "./commands/apps-list.js";
6
+ import { buildsUploadCommand } from "./commands/builds-upload.js";
7
+ import { ipaGenerateCommand } from "./commands/ipa-generate.js";
8
+ const DEFAULT_BASE_URL = "https://api.appstoreconnect.apple.com/";
9
+ export async function resolveCliEnvironment(env) {
10
+ const issuerId = env.ASC_ISSUER_ID?.trim();
11
+ const keyId = env.ASC_KEY_ID?.trim();
12
+ const privateKeyPath = env.ASC_PRIVATE_KEY_PATH?.trim();
13
+ const privateKeyRaw = env.ASC_PRIVATE_KEY?.trim();
14
+ const baseUrl = env.ASC_BASE_URL?.trim() || DEFAULT_BASE_URL;
15
+ const missingKeys = [];
16
+ if (!issuerId) {
17
+ missingKeys.push("ASC_ISSUER_ID");
18
+ }
19
+ if (!keyId) {
20
+ missingKeys.push("ASC_KEY_ID");
21
+ }
22
+ if (!privateKeyPath && !privateKeyRaw) {
23
+ missingKeys.push("ASC_PRIVATE_KEY or ASC_PRIVATE_KEY_PATH");
24
+ }
25
+ if (missingKeys.length > 0) {
26
+ throw new InfrastructureError(`Missing required environment variables: ${missingKeys.join(", ")}`);
27
+ }
28
+ const privateKey = privateKeyPath
29
+ ? await readPrivateKeyFile(privateKeyPath)
30
+ : normalizePrivateKey(privateKeyRaw);
31
+ return {
32
+ issuerId: issuerId,
33
+ keyId: keyId,
34
+ privateKey,
35
+ baseUrl
36
+ };
37
+ }
38
+ async function readPrivateKeyFile(filePath) {
39
+ try {
40
+ return (await readFile(filePath, "utf-8")).trim();
41
+ }
42
+ catch (error) {
43
+ throw new InfrastructureError(`Failed to read private key file at "${filePath}".`, error);
44
+ }
45
+ }
46
+ function normalizePrivateKey(rawValue) {
47
+ return rawValue.includes("\\n") ? rawValue.replace(/\\n/g, "\n") : rawValue;
48
+ }
49
+ export function parseCliCommand(argv) {
50
+ if (argv.includes("--help") || argv.includes("-h")) {
51
+ return { kind: "help" };
52
+ }
53
+ if (argv.length === 0) {
54
+ return { kind: "apps-list", json: false };
55
+ }
56
+ const [head, next, ...rest] = argv;
57
+ if (head === "apps" && next === "list") {
58
+ const flags = parseFlags(rest);
59
+ return { kind: "apps-list", json: flags.booleans.has("json") };
60
+ }
61
+ if (head === "builds" && next === "upload") {
62
+ const flags = parseFlags(rest);
63
+ return parseBuildsUploadCommand(flags);
64
+ }
65
+ if (head === "ipa" && next === "generate") {
66
+ const flags = parseFlags(rest);
67
+ return parseIpaGenerateCommand(flags);
68
+ }
69
+ throw new InfrastructureError(`Unknown command: ${argv.join(" ")}`);
70
+ }
71
+ function parseBuildsUploadCommand(flags) {
72
+ const appReference = requireFlag(flags, "app");
73
+ const version = requireFlag(flags, "version");
74
+ const buildNumber = requireFlag(flags, "build-number");
75
+ const ipaSource = parseIpaSource(flags, false);
76
+ return {
77
+ kind: "builds-upload",
78
+ json: flags.booleans.has("json"),
79
+ apply: flags.booleans.has("apply"),
80
+ waitProcessing: flags.booleans.has("wait-processing"),
81
+ appReference,
82
+ version,
83
+ buildNumber,
84
+ ipaSource
85
+ };
86
+ }
87
+ function parseIpaGenerateCommand(flags) {
88
+ const outputIpaPath = requireFlag(flags, "output-ipa");
89
+ const ipaSource = parseIpaSource(flags, true);
90
+ if (ipaSource.kind === "prebuilt") {
91
+ throw new InfrastructureError("ipa generate does not support --ipa prebuilt input.");
92
+ }
93
+ return {
94
+ kind: "ipa-generate",
95
+ json: flags.booleans.has("json"),
96
+ outputIpaPath,
97
+ ipaSource
98
+ };
99
+ }
100
+ function parseIpaSource(flags, isGenerateCommand) {
101
+ const ipaPath = flags.values.ipa;
102
+ const hasIpa = Boolean(ipaPath);
103
+ const hasBuildCommand = Boolean(flags.values["build-command"]);
104
+ const hasGeneratedIpaPath = Boolean(flags.values["generated-ipa-path"]);
105
+ const hasCustomCommandInputs = hasBuildCommand || hasGeneratedIpaPath;
106
+ const hasScheme = Boolean(flags.values.scheme);
107
+ const hasExportOptionsPlist = Boolean(flags.values["export-options-plist"]);
108
+ const hasWorkspacePath = Boolean(flags.values["workspace-path"]);
109
+ const hasProjectPath = Boolean(flags.values["project-path"]);
110
+ const hasXcodebuildInputs = hasScheme ||
111
+ hasExportOptionsPlist ||
112
+ hasWorkspacePath ||
113
+ hasProjectPath ||
114
+ Boolean(flags.values.configuration) ||
115
+ Boolean(flags.values["archive-path"]) ||
116
+ Boolean(flags.values["derived-data-path"]);
117
+ const sourceModes = [hasIpa, hasCustomCommandInputs, hasXcodebuildInputs].filter(Boolean)
118
+ .length;
119
+ if (sourceModes !== 1) {
120
+ throw new InfrastructureError("Exactly one IPA source mode is required: --ipa, xcodebuild options, or custom command options.");
121
+ }
122
+ if (hasIpa) {
123
+ if (hasCustomCommandInputs || hasXcodebuildInputs) {
124
+ throw new InfrastructureError("--ipa cannot be combined with generation options (--scheme/--build-command/etc).");
125
+ }
126
+ return {
127
+ kind: "prebuilt",
128
+ ipaPath: ipaPath
129
+ };
130
+ }
131
+ if (hasCustomCommandInputs) {
132
+ const buildCommand = requireFlag(flags, "build-command");
133
+ const generatedIpaPath = requireFlag(flags, "generated-ipa-path");
134
+ const outputIpaPath = isGenerateCommand ? requireFlag(flags, "output-ipa") : null;
135
+ return {
136
+ kind: "customCommand",
137
+ buildCommand,
138
+ generatedIpaPath,
139
+ ...(outputIpaPath ? { outputIpaPath } : {})
140
+ };
141
+ }
142
+ const scheme = requireFlag(flags, "scheme");
143
+ const exportOptionsPlist = requireFlag(flags, "export-options-plist");
144
+ if (hasWorkspacePath === hasProjectPath) {
145
+ throw new InfrastructureError("Exactly one of --workspace-path or --project-path is required for xcodebuild mode.");
146
+ }
147
+ const outputIpaPath = isGenerateCommand
148
+ ? requireFlag(flags, "output-ipa")
149
+ : flags.values["output-ipa"];
150
+ return {
151
+ kind: "xcodebuild",
152
+ scheme,
153
+ exportOptionsPlist,
154
+ ...(flags.values["workspace-path"] ? { workspacePath: flags.values["workspace-path"] } : {}),
155
+ ...(flags.values["project-path"] ? { projectPath: flags.values["project-path"] } : {}),
156
+ ...(flags.values.configuration ? { configuration: flags.values.configuration } : {}),
157
+ ...(flags.values["archive-path"] ? { archivePath: flags.values["archive-path"] } : {}),
158
+ ...(flags.values["derived-data-path"]
159
+ ? { derivedDataPath: flags.values["derived-data-path"] }
160
+ : {}),
161
+ ...(outputIpaPath ? { outputIpaPath } : {})
162
+ };
163
+ }
164
+ function parseFlags(argv) {
165
+ const values = {};
166
+ const booleans = new Set();
167
+ const positionals = [];
168
+ for (let index = 0; index < argv.length; index += 1) {
169
+ const token = argv[index];
170
+ if (!token) {
171
+ continue;
172
+ }
173
+ if (!token.startsWith("--")) {
174
+ positionals.push(token);
175
+ continue;
176
+ }
177
+ const tokenWithoutPrefix = token.slice(2);
178
+ const equalIndex = tokenWithoutPrefix.indexOf("=");
179
+ const rawFlag = equalIndex >= 0
180
+ ? tokenWithoutPrefix.slice(0, equalIndex)
181
+ : tokenWithoutPrefix;
182
+ const rawValue = equalIndex >= 0 ? tokenWithoutPrefix.slice(equalIndex + 1) : undefined;
183
+ const flag = rawFlag.trim();
184
+ if (!flag) {
185
+ throw new InfrastructureError(`Invalid flag: ${token}`);
186
+ }
187
+ if (rawValue !== undefined) {
188
+ values[flag] = rawValue;
189
+ continue;
190
+ }
191
+ const nextToken = argv[index + 1];
192
+ if (nextToken && !nextToken.startsWith("--")) {
193
+ values[flag] = nextToken;
194
+ index += 1;
195
+ continue;
196
+ }
197
+ booleans.add(flag);
198
+ }
199
+ return { positionals, values, booleans };
200
+ }
201
+ function requireFlag(flags, name) {
202
+ const value = flags.values[name];
203
+ if (!value || value.trim().length === 0) {
204
+ throw new InfrastructureError(`Missing required option: --${name}`);
205
+ }
206
+ return value;
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // CLI options
210
+ // ---------------------------------------------------------------------------
211
+ // ---------------------------------------------------------------------------
212
+ // Help
213
+ // ---------------------------------------------------------------------------
214
+ function printHelp() {
215
+ console.log(`appstore-tools CLI
216
+
217
+ Usage:
218
+ appstore-tools --help
219
+ appstore-tools apps list [--json]
220
+ appstore-tools ipa generate --output-ipa <path> [xcodebuild/custom options] [--json]
221
+ appstore-tools builds upload --app <appId|bundleId> --version <x.y.z> --build-number <n> (--ipa <path> | [generation options]) [--wait-processing] [--json] [--apply]
222
+
223
+ Required environment variables:
224
+ ASC_ISSUER_ID
225
+ ASC_KEY_ID
226
+ ASC_PRIVATE_KEY or ASC_PRIVATE_KEY_PATH
227
+
228
+ Optional environment variables:
229
+ ASC_BASE_URL (default: https://api.appstoreconnect.apple.com/)
230
+
231
+ Generation options (xcodebuild mode):
232
+ --scheme <name> --export-options-plist <path> (--workspace-path <path> | --project-path <path>)
233
+ [--configuration <Release>] [--archive-path <path>] [--derived-data-path <path>] [--output-ipa <path>]
234
+
235
+ Generation options (custom mode):
236
+ --build-command "<shell command>" --generated-ipa-path <path> [--output-ipa <path>]
237
+ `);
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // Main
241
+ // ---------------------------------------------------------------------------
242
+ export async function runCli(argv, env) {
243
+ try {
244
+ const command = parseCliCommand(argv);
245
+ return handleCliCommand(command, env);
246
+ }
247
+ catch (error) {
248
+ const message = error instanceof Error ? error.message : "Unknown CLI error";
249
+ console.error(message);
250
+ return 1;
251
+ }
252
+ }
253
+ async function handleCliCommand(command, env) {
254
+ if (command.kind === "help") {
255
+ printHelp();
256
+ return 0;
257
+ }
258
+ if (command.kind === "ipa-generate") {
259
+ return ipaGenerateCommand(command);
260
+ }
261
+ const config = await resolveCliEnvironment(env);
262
+ const client = new AppStoreConnectClient({
263
+ issuerId: config.issuerId,
264
+ keyId: config.keyId,
265
+ privateKey: config.privateKey
266
+ }, {
267
+ baseUrl: config.baseUrl
268
+ });
269
+ if (command.kind === "apps-list") {
270
+ return appsListCommand(client, { json: command.json });
271
+ }
272
+ if (command.kind === "builds-upload") {
273
+ return buildsUploadCommand(client, command);
274
+ }
275
+ return assertNever(command);
276
+ }
277
+ function assertNever(value) {
278
+ throw new Error(`Unsupported command payload: ${JSON.stringify(value)}`);
279
+ }
280
+ function isExecutedAsScript() {
281
+ const executedPath = process.argv[1];
282
+ if (!executedPath) {
283
+ return false;
284
+ }
285
+ return import.meta.url === pathToFileURL(executedPath).href;
286
+ }
287
+ if (isExecutedAsScript()) {
288
+ const exitCode = await runCli(process.argv.slice(2), process.env);
289
+ if (exitCode !== 0) {
290
+ process.exit(exitCode);
291
+ }
292
+ }
@@ -0,0 +1,12 @@
1
+ import { type AppStoreConnectClient } from "../api/client.js";
2
+ export interface AppSummary {
3
+ readonly id: string;
4
+ readonly name: string;
5
+ readonly bundleId: string;
6
+ readonly sku?: string;
7
+ }
8
+ export declare function appsListCommand(client: AppStoreConnectClient, options: {
9
+ readonly json: boolean;
10
+ }): Promise<number>;
11
+ export declare function listApps(client: AppStoreConnectClient): Promise<readonly AppSummary[]>;
12
+ //# sourceMappingURL=apps-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps-list.d.ts","sourceRoot":"","sources":["../../src/commands/apps-list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAqBnF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAQD,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAClC,OAAO,CAAC,MAAM,CAAC,CA+CjB;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CA4B5F"}
@@ -0,0 +1,63 @@
1
+ import { InfrastructureError } from "../api/client.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Command
4
+ // ---------------------------------------------------------------------------
5
+ const DEFAULT_LIST_LIMIT = 50;
6
+ export async function appsListCommand(client, options) {
7
+ const response = await client.request({
8
+ method: "GET",
9
+ path: "/v1/apps",
10
+ query: { limit: DEFAULT_LIST_LIMIT }
11
+ });
12
+ const apps = response.data.data.map((item) => {
13
+ const attributes = item.attributes;
14
+ if (!item.id || !attributes?.name || !attributes?.bundleId) {
15
+ throw new InfrastructureError("Malformed app payload received from App Store Connect.");
16
+ }
17
+ const summary = {
18
+ id: item.id,
19
+ name: attributes.name,
20
+ bundleId: attributes.bundleId
21
+ };
22
+ if (attributes.sku !== undefined) {
23
+ return { ...summary, sku: attributes.sku };
24
+ }
25
+ return summary;
26
+ });
27
+ const sorted = [...apps].sort((a, b) => a.name.localeCompare(b.name));
28
+ if (options.json) {
29
+ console.log(JSON.stringify(sorted, null, 2));
30
+ return 0;
31
+ }
32
+ if (sorted.length === 0) {
33
+ console.log("No apps were returned by App Store Connect.");
34
+ return 0;
35
+ }
36
+ console.log("Apps:");
37
+ for (const app of sorted) {
38
+ console.log(`- ${app.name} (${app.bundleId}) [${app.id}]`);
39
+ }
40
+ return 0;
41
+ }
42
+ export async function listApps(client) {
43
+ const response = await client.request({
44
+ method: "GET",
45
+ path: "/v1/apps",
46
+ query: { limit: DEFAULT_LIST_LIMIT }
47
+ });
48
+ return response.data.data.map((item) => {
49
+ const attributes = item.attributes;
50
+ if (!item.id || !attributes?.name || !attributes?.bundleId) {
51
+ throw new InfrastructureError("Malformed app payload received from App Store Connect.");
52
+ }
53
+ const summary = {
54
+ id: item.id,
55
+ name: attributes.name,
56
+ bundleId: attributes.bundleId
57
+ };
58
+ if (attributes.sku !== undefined) {
59
+ return { ...summary, sku: attributes.sku };
60
+ }
61
+ return summary;
62
+ });
63
+ }
@@ -0,0 +1,45 @@
1
+ import { type AppStoreConnectClient } from "../api/client.js";
2
+ import { type IpaSource, type ProcessRunner } from "../ipa/artifact.js";
3
+ export interface BuildsUploadInput {
4
+ readonly ipaSource: IpaSource;
5
+ readonly appId: string;
6
+ readonly expectedBundleId: string;
7
+ readonly expectedVersion: string;
8
+ readonly expectedBuildNumber: string;
9
+ readonly waitProcessing: boolean;
10
+ readonly apply: boolean;
11
+ }
12
+ export interface BuildsUploadResult {
13
+ readonly mode: "dry-run" | "applied";
14
+ readonly preflightReport: {
15
+ readonly ipaPath: string;
16
+ readonly bundleId: string | null;
17
+ readonly version: string | null;
18
+ readonly buildNumber: string | null;
19
+ readonly sizeBytes: number;
20
+ readonly sha256: string | null;
21
+ readonly md5: string | null;
22
+ readonly signingValidated: boolean;
23
+ readonly errors: readonly string[];
24
+ readonly warnings: readonly string[];
25
+ };
26
+ readonly plannedOperations: readonly string[];
27
+ readonly buildUploadId: string | null;
28
+ readonly finalBuildUploadState: string | null;
29
+ }
30
+ export declare function uploadBuild(client: AppStoreConnectClient, input: BuildsUploadInput, options?: {
31
+ readonly sleep?: (ms: number) => Promise<void>;
32
+ readonly pollIntervalMs?: number;
33
+ readonly pollTimeoutMs?: number;
34
+ readonly processRunner?: ProcessRunner;
35
+ }): Promise<BuildsUploadResult>;
36
+ export declare function buildsUploadCommand(client: AppStoreConnectClient, command: {
37
+ readonly appReference: string;
38
+ readonly version: string;
39
+ readonly buildNumber: string;
40
+ readonly ipaSource: IpaSource;
41
+ readonly waitProcessing: boolean;
42
+ readonly apply: boolean;
43
+ readonly json: boolean;
44
+ }): Promise<number>;
45
+ //# sourceMappingURL=builds-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builds-upload.d.ts","sourceRoot":"","sources":["../../src/commands/builds-upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAsB,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAuO5F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,eAAe,EAAE;QACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;QACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;QACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;KACtC,CAAC;IACF,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C;AAKD,wBAAsB,WAAW,CAC/B,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;CACxC,GACA,OAAO,CAAC,kBAAkB,CAAC,CAoG7B;AAiCD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE;IACP,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,GACA,OAAO,CAAC,MAAM,CAAC,CA2BjB"}
@@ -0,0 +1,262 @@
1
+ import { DomainError, InfrastructureError } from "../api/client.js";
2
+ import { executeUploadOperations, parseUploadOperations } from "../api/types.js";
3
+ import { resolveIpaArtifact } from "../ipa/artifact.js";
4
+ import { verifyIpa } from "../ipa/preflight.js";
5
+ import { listApps } from "./apps-list.js";
6
+ // ---------------------------------------------------------------------------
7
+ // API helpers
8
+ // ---------------------------------------------------------------------------
9
+ function mapBuildUpload(response) {
10
+ const data = response.data;
11
+ const id = data.id;
12
+ if (!id) {
13
+ throw new InfrastructureError("Malformed build upload payload: missing id.");
14
+ }
15
+ const stateValue = data.attributes?.state?.state;
16
+ if (!stateValue) {
17
+ throw new InfrastructureError("Malformed build upload payload: missing state.");
18
+ }
19
+ const stateObj = data.attributes?.state;
20
+ return {
21
+ id,
22
+ state: {
23
+ state: stateValue,
24
+ errors: (stateObj?.errors ?? []).map((item) => item.description ?? "Unknown error"),
25
+ warnings: (stateObj?.warnings ?? []).map((item) => item.description ?? "Unknown warning"),
26
+ infos: (stateObj?.infos ?? []).map((item) => item.description ?? "Unknown info")
27
+ }
28
+ };
29
+ }
30
+ async function createBuildUpload(client, input) {
31
+ const response = await client.request({
32
+ method: "POST",
33
+ path: "/v1/buildUploads",
34
+ body: {
35
+ data: {
36
+ type: "buildUploads",
37
+ attributes: {
38
+ cfBundleShortVersionString: input.versionString,
39
+ cfBundleVersion: input.buildNumber,
40
+ platform: input.platform
41
+ },
42
+ relationships: {
43
+ app: {
44
+ data: {
45
+ type: "apps",
46
+ id: input.appId
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ });
53
+ return mapBuildUpload(response.data);
54
+ }
55
+ async function createBuildUploadFile(client, input) {
56
+ const response = await client.request({
57
+ method: "POST",
58
+ path: "/v1/buildUploadFiles",
59
+ body: {
60
+ data: {
61
+ type: "buildUploadFiles",
62
+ attributes: {
63
+ assetType: input.assetType,
64
+ fileName: input.fileName,
65
+ fileSize: input.fileSize,
66
+ uti: input.uti
67
+ },
68
+ relationships: {
69
+ buildUpload: {
70
+ data: {
71
+ type: "buildUploads",
72
+ id: input.buildUploadId
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ });
79
+ const id = response.data.data.id;
80
+ const operationsPayload = response.data.data.attributes?.uploadOperations ?? [];
81
+ if (!id) {
82
+ throw new InfrastructureError("Malformed build upload file payload: missing id.");
83
+ }
84
+ return {
85
+ id,
86
+ uploadOperations: parseUploadOperations(operationsPayload, "build upload file")
87
+ };
88
+ }
89
+ async function markBuildUploadFileUploaded(client, input) {
90
+ await client.request({
91
+ method: "PATCH",
92
+ path: `/v1/buildUploadFiles/${input.buildUploadFileId}`,
93
+ body: {
94
+ data: {
95
+ type: "buildUploadFiles",
96
+ id: input.buildUploadFileId,
97
+ attributes: {
98
+ sourceFileChecksums: {
99
+ file: {
100
+ hash: input.sha256,
101
+ algorithm: "SHA_256"
102
+ },
103
+ composite: {
104
+ hash: input.md5,
105
+ algorithm: "MD5"
106
+ }
107
+ },
108
+ uploaded: true
109
+ }
110
+ }
111
+ }
112
+ });
113
+ }
114
+ async function getBuildUpload(client, buildUploadId) {
115
+ const response = await client.request({
116
+ method: "GET",
117
+ path: `/v1/buildUploads/${buildUploadId}`,
118
+ query: {
119
+ "fields[buildUploads]": "state"
120
+ }
121
+ });
122
+ return mapBuildUpload(response.data);
123
+ }
124
+ const DEFAULT_POLL_INTERVAL_MS = 3_000;
125
+ const DEFAULT_POLL_TIMEOUT_MS = 10 * 60 * 1_000;
126
+ export async function uploadBuild(client, input, options) {
127
+ const sleep = options?.sleep ??
128
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
129
+ const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
130
+ const pollTimeoutMs = options?.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
131
+ const artifact = await resolveIpaArtifact(input.ipaSource, options?.processRunner);
132
+ try {
133
+ const preflightReport = await verifyIpa({
134
+ ipaPath: artifact.ipaPath,
135
+ expectedBundleId: input.expectedBundleId,
136
+ expectedVersion: input.expectedVersion,
137
+ expectedBuildNumber: input.expectedBuildNumber
138
+ }, options?.processRunner);
139
+ if (preflightReport.errors.length > 0) {
140
+ throw new DomainError(`IPA preflight verification failed: ${preflightReport.errors.join(" | ")}`);
141
+ }
142
+ const plannedOperations = [
143
+ `Create build upload for app ${input.appId}`,
144
+ `Create build upload file for ${artifact.ipaPath}`,
145
+ "Upload chunks using App Store Connect upload operations",
146
+ "Mark build upload file as uploaded with checksums",
147
+ input.waitProcessing
148
+ ? "Poll build upload until terminal state"
149
+ : "Fetch current build upload state once"
150
+ ];
151
+ if (!input.apply) {
152
+ return {
153
+ mode: "dry-run",
154
+ preflightReport,
155
+ plannedOperations,
156
+ buildUploadId: null,
157
+ finalBuildUploadState: null
158
+ };
159
+ }
160
+ const buildUpload = await createBuildUpload(client, {
161
+ appId: input.appId,
162
+ versionString: input.expectedVersion,
163
+ buildNumber: input.expectedBuildNumber,
164
+ platform: "IOS"
165
+ });
166
+ const fileName = artifact.ipaPath.split(/[\\/]/).at(-1) ?? "build.ipa";
167
+ const buildUploadFile = await createBuildUploadFile(client, {
168
+ buildUploadId: buildUpload.id,
169
+ fileName,
170
+ fileSize: preflightReport.sizeBytes,
171
+ uti: "com.apple.ipa",
172
+ assetType: "ASSET"
173
+ });
174
+ await executeUploadOperations(artifact.ipaPath, buildUploadFile.uploadOperations);
175
+ const sha256 = preflightReport.sha256;
176
+ const md5 = preflightReport.md5;
177
+ if (!sha256 || !md5) {
178
+ throw new DomainError("Missing checksums in preflight report; cannot mark build upload file as uploaded.");
179
+ }
180
+ await markBuildUploadFileUploaded(client, {
181
+ buildUploadFileId: buildUploadFile.id,
182
+ sha256,
183
+ md5
184
+ });
185
+ const finalState = input.waitProcessing
186
+ ? await pollBuildUploadState(client, buildUpload.id, sleep, pollIntervalMs, pollTimeoutMs)
187
+ : (await getBuildUpload(client, buildUpload.id)).state.state;
188
+ if (finalState === "FAILED") {
189
+ throw new DomainError("Build upload failed in App Store Connect.");
190
+ }
191
+ return {
192
+ mode: "applied",
193
+ preflightReport,
194
+ plannedOperations,
195
+ buildUploadId: buildUpload.id,
196
+ finalBuildUploadState: finalState
197
+ };
198
+ }
199
+ finally {
200
+ if (artifact.dispose) {
201
+ await artifact.dispose();
202
+ }
203
+ }
204
+ }
205
+ async function pollBuildUploadState(client, buildUploadId, sleep, pollIntervalMs, pollTimeoutMs) {
206
+ const startedAt = Date.now();
207
+ while (true) {
208
+ const buildUpload = await getBuildUpload(client, buildUploadId);
209
+ const state = buildUpload.state.state;
210
+ if (state === "COMPLETE" || state === "FAILED") {
211
+ return state;
212
+ }
213
+ if (Date.now() - startedAt > pollTimeoutMs) {
214
+ throw new DomainError(`Timed out while waiting for build upload processing (${buildUploadId}).`);
215
+ }
216
+ await sleep(pollIntervalMs);
217
+ }
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // CLI command
221
+ // ---------------------------------------------------------------------------
222
+ export async function buildsUploadCommand(client, command) {
223
+ const apps = await listApps(client);
224
+ const app = apps.find((a) => a.id === command.appReference || a.bundleId === command.appReference);
225
+ if (!app) {
226
+ throw new Error(`Could not resolve app reference "${command.appReference}".`);
227
+ }
228
+ const result = await uploadBuild(client, {
229
+ ipaSource: command.ipaSource,
230
+ appId: app.id,
231
+ expectedBundleId: app.bundleId,
232
+ expectedVersion: command.version,
233
+ expectedBuildNumber: command.buildNumber,
234
+ waitProcessing: command.waitProcessing,
235
+ apply: command.apply
236
+ });
237
+ if (command.json) {
238
+ console.log(JSON.stringify(result, null, 2));
239
+ }
240
+ else {
241
+ printBuildUploadResult(result, app);
242
+ }
243
+ return 0;
244
+ }
245
+ function printBuildUploadResult(result, app) {
246
+ console.log(`Mode: ${result.mode}`);
247
+ console.log(`App: ${app.name} (${app.bundleId}) [${app.id}]`);
248
+ console.log(`IPA: ${result.preflightReport.ipaPath}`);
249
+ console.log(`SHA-256: ${result.preflightReport.sha256 ?? "unavailable"}`);
250
+ console.log(`Signing validated: ${result.preflightReport.signingValidated ? "yes" : "no"}`);
251
+ console.log("Planned operations:");
252
+ result.plannedOperations.forEach((operation) => {
253
+ console.log(`- ${operation}`);
254
+ });
255
+ if (result.mode === "dry-run") {
256
+ console.log("Dry-run completed. No App Store Connect mutation requests were sent.");
257
+ }
258
+ else {
259
+ console.log(`Build upload id: ${result.buildUploadId}`);
260
+ console.log(`Final state: ${result.finalBuildUploadState}`);
261
+ }
262
+ }