@absolutejs/deploy 0.22.0 → 0.24.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 +199 -121
- package/dist/appStoreConnect.d.ts +182 -0
- package/dist/appStoreConnect.js +585 -0
- package/dist/appStoreConnect.js.map +10 -0
- package/dist/googlePlay.d.ts +180 -0
- package/dist/googlePlay.js +10897 -0
- package/dist/googlePlay.js.map +90 -0
- package/dist/index.d.ts +16 -12
- package/dist/index.js +11460 -14
- package/dist/index.js.map +86 -4
- package/dist/nativeRelease.d.ts +18 -1
- package/dist/nativeRelease.js +53 -13
- package/dist/nativeRelease.js.map +3 -3
- package/package.json +12 -2
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
+
var __require = import.meta.require;
|
|
34
|
+
|
|
35
|
+
// src/appStoreConnect.ts
|
|
36
|
+
import { createHash, createPrivateKey, sign } from "crypto";
|
|
37
|
+
import path from "path";
|
|
38
|
+
var API_ROOT = "https://api.appstoreconnect.apple.com/v1";
|
|
39
|
+
var DEFAULT_RECEIPT_PREFIX = "absolutejs/app-store-connect-receipts";
|
|
40
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
41
|
+
|
|
42
|
+
class AppStoreConnectReleaseError extends Error {
|
|
43
|
+
status;
|
|
44
|
+
constructor(message, status) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "AppStoreConnectReleaseError";
|
|
47
|
+
this.status = status;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
var encodedJson = (value) => new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
|
|
52
|
+
`);
|
|
53
|
+
var decodedJson = (value) => JSON.parse(new TextDecoder().decode(value));
|
|
54
|
+
var sha256Bytes = (value) => createHash("sha256").update(value).digest("hex");
|
|
55
|
+
var hashIdentity = (value) => createHash("sha256").update(value).digest("hex");
|
|
56
|
+
var isIsoTimestamp = (value) => typeof value === "string" && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
57
|
+
var normalizedPrefix = (value) => {
|
|
58
|
+
const prefix = value.replace(/^\/+|\/+$/g, "");
|
|
59
|
+
if (!prefix || prefix.split("/").some((part) => part === "." || part === ".."))
|
|
60
|
+
throw new AppStoreConnectReleaseError("App Store Connect receipt prefix is invalid");
|
|
61
|
+
return prefix;
|
|
62
|
+
};
|
|
63
|
+
var normalizedIntent = (target = {}) => {
|
|
64
|
+
const groups = [
|
|
65
|
+
...new Set((target.groups ?? []).map((value) => value.trim()))
|
|
66
|
+
].sort();
|
|
67
|
+
if (groups.some((value) => !value))
|
|
68
|
+
throw new AppStoreConnectReleaseError("TestFlight group names or IDs must not be empty");
|
|
69
|
+
const locales = new Set;
|
|
70
|
+
const whatsNew = [...target.whatsNew ?? []].map(({ locale, text }) => ({ locale: locale.trim(), text: text.trim() })).sort((left, right) => left.locale.localeCompare(right.locale));
|
|
71
|
+
for (const item of whatsNew) {
|
|
72
|
+
if (!item.locale || !item.text || locales.has(item.locale))
|
|
73
|
+
throw new AppStoreConnectReleaseError("TestFlight notes require unique locales and non-empty text");
|
|
74
|
+
locales.add(item.locale);
|
|
75
|
+
}
|
|
76
|
+
if (target.submitForReview && groups.length === 0)
|
|
77
|
+
throw new AppStoreConnectReleaseError("TestFlight beta review requires at least one external group");
|
|
78
|
+
return { groups, submitForReview: target.submitForReview ?? false, whatsNew };
|
|
79
|
+
};
|
|
80
|
+
var parseReceipt = (value) => {
|
|
81
|
+
if (!isRecord(value) || value.format !== 1 || value.provider !== "app-store-connect" || typeof value.appleAppId !== "string" || typeof value.releaseId !== "string" || typeof value.sha256 !== "string" || !SHA256_PATTERN.test(value.sha256) || !Number.isSafeInteger(value.buildNumber) || Number(value.buildNumber) < 1 || typeof value.marketingVersion !== "string" || !isIsoTimestamp(value.updatedAt) || ![
|
|
82
|
+
"preparing",
|
|
83
|
+
"uploading",
|
|
84
|
+
"processing",
|
|
85
|
+
"distributed",
|
|
86
|
+
"review-submitted"
|
|
87
|
+
].includes(String(value.stage)) || !isRecord(value.intent))
|
|
88
|
+
throw new AppStoreConnectReleaseError("App Store Connect release receipt is invalid");
|
|
89
|
+
for (const field of ["buildId", "buildUploadFileId", "buildUploadId"])
|
|
90
|
+
if (value[field] !== undefined && typeof value[field] !== "string")
|
|
91
|
+
throw new AppStoreConnectReleaseError("App Store Connect release receipt is invalid");
|
|
92
|
+
const buildId = typeof value.buildId === "string" ? value.buildId : undefined;
|
|
93
|
+
const buildUploadFileId = typeof value.buildUploadFileId === "string" ? value.buildUploadFileId : undefined;
|
|
94
|
+
const buildUploadId = typeof value.buildUploadId === "string" ? value.buildUploadId : undefined;
|
|
95
|
+
return {
|
|
96
|
+
appleAppId: value.appleAppId,
|
|
97
|
+
buildNumber: Number(value.buildNumber),
|
|
98
|
+
format: 1,
|
|
99
|
+
intent: normalizedIntent(value.intent),
|
|
100
|
+
marketingVersion: value.marketingVersion,
|
|
101
|
+
provider: "app-store-connect",
|
|
102
|
+
releaseId: value.releaseId,
|
|
103
|
+
sha256: value.sha256,
|
|
104
|
+
stage: value.stage,
|
|
105
|
+
updatedAt: value.updatedAt,
|
|
106
|
+
...buildId ? { buildId } : {},
|
|
107
|
+
...buildUploadFileId ? { buildUploadFileId } : {},
|
|
108
|
+
...buildUploadId ? { buildUploadId } : {}
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
var base64url = (value) => Buffer.from(value).toString("base64url");
|
|
112
|
+
var createTokenProvider = (auth, clock) => {
|
|
113
|
+
if (!auth.issuerId || !auth.keyId || !auth.privateKey)
|
|
114
|
+
throw new AppStoreConnectReleaseError("App Store Connect API credentials are incomplete");
|
|
115
|
+
const key = createPrivateKey(auth.privateKey);
|
|
116
|
+
return () => {
|
|
117
|
+
const issuedAt = Math.floor(clock().getTime() / 1000);
|
|
118
|
+
const header = base64url(JSON.stringify({ alg: "ES256", kid: auth.keyId, typ: "JWT" }));
|
|
119
|
+
const claims = base64url(JSON.stringify({
|
|
120
|
+
aud: "appstoreconnect-v1",
|
|
121
|
+
exp: issuedAt + 1199,
|
|
122
|
+
iat: issuedAt,
|
|
123
|
+
iss: auth.issuerId
|
|
124
|
+
}));
|
|
125
|
+
const payload = `${header}.${claims}`;
|
|
126
|
+
return `${payload}.${base64url(sign("sha256", Buffer.from(payload), { dsaEncoding: "ieee-p1363", key }))}`;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
var responseError = async (response) => {
|
|
130
|
+
const body = await response.text().catch(() => "");
|
|
131
|
+
return new AppStoreConnectReleaseError(`App Store Connect request failed (${response.status})${body ? `: ${body}` : ""}`, response.status);
|
|
132
|
+
};
|
|
133
|
+
var createAppStoreConnectClient = (options) => {
|
|
134
|
+
const requestFetch = options.fetch ?? fetch;
|
|
135
|
+
const token = createTokenProvider(options.auth, options.clock ?? (() => new Date));
|
|
136
|
+
const request = async (route, init = {}) => {
|
|
137
|
+
const response = await requestFetch(`${API_ROOT}${route}`, {
|
|
138
|
+
...init,
|
|
139
|
+
headers: {
|
|
140
|
+
Accept: "application/json",
|
|
141
|
+
Authorization: `Bearer ${token()}`,
|
|
142
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
143
|
+
...init.headers
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok)
|
|
147
|
+
throw await responseError(response);
|
|
148
|
+
return response.status === 204 ? undefined : await response.json();
|
|
149
|
+
};
|
|
150
|
+
const dataList = async (route) => {
|
|
151
|
+
const result = [];
|
|
152
|
+
let next = route;
|
|
153
|
+
while (next) {
|
|
154
|
+
const page = await request(next.startsWith(API_ROOT) ? next.slice(API_ROOT.length) : next);
|
|
155
|
+
if (Array.isArray(page.data))
|
|
156
|
+
result.push(...page.data.filter(isRecord));
|
|
157
|
+
const links = isRecord(page.links) ? page.links : undefined;
|
|
158
|
+
next = typeof links?.next === "string" ? links.next : undefined;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
};
|
|
162
|
+
const buildFrom = (data) => {
|
|
163
|
+
const attributes = isRecord(data.attributes) ? data.attributes : {};
|
|
164
|
+
return {
|
|
165
|
+
id: String(data.id),
|
|
166
|
+
processingState: String(attributes.processingState),
|
|
167
|
+
version: String(attributes.version)
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const uploadFrom = (data) => {
|
|
171
|
+
const attributes = isRecord(data.attributes) ? data.attributes : {};
|
|
172
|
+
const state = isRecord(attributes.state) ? attributes.state.state : attributes.state;
|
|
173
|
+
return {
|
|
174
|
+
id: String(data.id),
|
|
175
|
+
state: String(state)
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
findAppId: async ({ bundleId, signal }) => {
|
|
180
|
+
const items = await dataList(`/apps?filter[bundleId]=${encodeURIComponent(bundleId)}&fields[apps]=bundleId&limit=2`);
|
|
181
|
+
const exact = items.filter((item) => isRecord(item.attributes) && item.attributes.bundleId === bundleId);
|
|
182
|
+
if (exact.length !== 1)
|
|
183
|
+
throw new AppStoreConnectReleaseError(`Expected one App Store Connect app for bundle ID ${bundleId}`);
|
|
184
|
+
signal?.throwIfAborted();
|
|
185
|
+
return String(exact[0].id);
|
|
186
|
+
},
|
|
187
|
+
listBuildNumbers: async ({ appId, signal }) => {
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
const items = await dataList(`/builds?filter[app]=${encodeURIComponent(appId)}&fields[builds]=version&limit=200`);
|
|
190
|
+
return items.map((item) => Number(isRecord(item.attributes) ? item.attributes.version : NaN)).filter((value) => Number.isSafeInteger(value) && value > 0);
|
|
191
|
+
},
|
|
192
|
+
findBuild: async ({ appId, buildNumber, signal }) => {
|
|
193
|
+
signal?.throwIfAborted();
|
|
194
|
+
const items = await dataList(`/builds?filter[app]=${encodeURIComponent(appId)}&filter[version]=${buildNumber}&fields[builds]=version,processingState&limit=2`);
|
|
195
|
+
return items.length ? buildFrom(items[0]) : null;
|
|
196
|
+
},
|
|
197
|
+
findBuildUpload: async ({
|
|
198
|
+
appId,
|
|
199
|
+
buildNumber,
|
|
200
|
+
marketingVersion,
|
|
201
|
+
signal
|
|
202
|
+
}) => {
|
|
203
|
+
signal?.throwIfAborted();
|
|
204
|
+
const items = await dataList(`/apps/${encodeURIComponent(appId)}/buildUploads?filter[cfBundleVersion]=${buildNumber}&filter[cfBundleShortVersionString]=${encodeURIComponent(marketingVersion)}&filter[platform]=IOS&fields[buildUploads]=state,cfBundleVersion,cfBundleShortVersionString&limit=2`);
|
|
205
|
+
return items.length ? uploadFrom(items[0]) : null;
|
|
206
|
+
},
|
|
207
|
+
createBuildUpload: async ({
|
|
208
|
+
appId,
|
|
209
|
+
buildNumber,
|
|
210
|
+
marketingVersion,
|
|
211
|
+
signal
|
|
212
|
+
}) => {
|
|
213
|
+
const response = await request("/buildUploads", {
|
|
214
|
+
method: "POST",
|
|
215
|
+
signal,
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
data: {
|
|
218
|
+
type: "buildUploads",
|
|
219
|
+
attributes: {
|
|
220
|
+
cfBundleVersion: String(buildNumber),
|
|
221
|
+
cfBundleShortVersionString: marketingVersion,
|
|
222
|
+
platform: "IOS"
|
|
223
|
+
},
|
|
224
|
+
relationships: { app: { data: { type: "apps", id: appId } } }
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
});
|
|
228
|
+
return uploadFrom(response.data);
|
|
229
|
+
},
|
|
230
|
+
getBuildUpload: async ({ buildUploadId, signal }) => {
|
|
231
|
+
const response = await request(`/buildUploads/${encodeURIComponent(buildUploadId)}?fields[buildUploads]=state`, { signal });
|
|
232
|
+
return uploadFrom(response.data);
|
|
233
|
+
},
|
|
234
|
+
createBuildUploadFile: async ({
|
|
235
|
+
buildUploadId,
|
|
236
|
+
bytes,
|
|
237
|
+
fileName,
|
|
238
|
+
signal
|
|
239
|
+
}) => {
|
|
240
|
+
const response = await request("/buildUploadFiles", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
signal,
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
data: {
|
|
245
|
+
type: "buildUploadFiles",
|
|
246
|
+
attributes: {
|
|
247
|
+
assetType: "ASSET",
|
|
248
|
+
fileName,
|
|
249
|
+
fileSize: bytes,
|
|
250
|
+
uti: "com.apple.ipa"
|
|
251
|
+
},
|
|
252
|
+
relationships: {
|
|
253
|
+
buildUpload: {
|
|
254
|
+
data: { type: "buildUploads", id: buildUploadId }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
return String(response.data.id);
|
|
261
|
+
},
|
|
262
|
+
findBuildUploadFile: async ({ buildUploadId, signal }) => {
|
|
263
|
+
signal?.throwIfAborted();
|
|
264
|
+
const items = await dataList(`/buildUploads/${encodeURIComponent(buildUploadId)}/buildUploadFiles?fields[buildUploadFiles]=fileName&limit=2`);
|
|
265
|
+
return items.length ? String(items[0].id) : null;
|
|
266
|
+
},
|
|
267
|
+
uploadBuildFile: async ({ artifactPath, fileId, signal }) => {
|
|
268
|
+
const response = await request(`/buildUploadFiles/${encodeURIComponent(fileId)}?fields[buildUploadFiles]=uploadOperations`, { signal });
|
|
269
|
+
const data = response.data;
|
|
270
|
+
const attrs = isRecord(data.attributes) ? data.attributes : {};
|
|
271
|
+
const operations = Array.isArray(attrs.uploadOperations) ? attrs.uploadOperations.filter(isRecord) : [];
|
|
272
|
+
if (!operations.length)
|
|
273
|
+
throw new AppStoreConnectReleaseError("App Store Connect returned no IPA upload operations");
|
|
274
|
+
await Promise.all(operations.map(async (operation) => {
|
|
275
|
+
const offset = Number(operation.offset);
|
|
276
|
+
const length = Number(operation.length);
|
|
277
|
+
const headers = new Headers;
|
|
278
|
+
if (Array.isArray(operation.requestHeaders)) {
|
|
279
|
+
for (const entry of operation.requestHeaders)
|
|
280
|
+
if (isRecord(entry))
|
|
281
|
+
headers.set(String(entry.name), String(entry.value));
|
|
282
|
+
}
|
|
283
|
+
const upload = await requestFetch(String(operation.url), {
|
|
284
|
+
method: String(operation.method),
|
|
285
|
+
headers,
|
|
286
|
+
body: Bun.file(artifactPath).slice(offset, offset + length),
|
|
287
|
+
signal
|
|
288
|
+
});
|
|
289
|
+
if (!upload.ok)
|
|
290
|
+
throw await responseError(upload);
|
|
291
|
+
}));
|
|
292
|
+
},
|
|
293
|
+
commitBuildUploadFile: async ({ fileId, sha256, signal }) => {
|
|
294
|
+
await request(`/buildUploadFiles/${encodeURIComponent(fileId)}`, {
|
|
295
|
+
method: "PATCH",
|
|
296
|
+
signal,
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
data: {
|
|
299
|
+
type: "buildUploadFiles",
|
|
300
|
+
id: fileId,
|
|
301
|
+
attributes: {
|
|
302
|
+
uploaded: true,
|
|
303
|
+
sourceFileChecksums: {
|
|
304
|
+
file: { algorithm: "SHA_256", hash: sha256 }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
resolveGroups: async ({ appId, groups, signal }) => {
|
|
312
|
+
signal?.throwIfAborted();
|
|
313
|
+
const items = await dataList(`/betaGroups?filter[app]=${encodeURIComponent(appId)}&fields[betaGroups]=name,isInternalGroup&limit=200`);
|
|
314
|
+
return groups.map((requested) => {
|
|
315
|
+
const matches = items.filter((item2) => item2.id === requested || isRecord(item2.attributes) && item2.attributes.name === requested);
|
|
316
|
+
if (matches.length !== 1)
|
|
317
|
+
throw new AppStoreConnectReleaseError(`Expected one TestFlight group matching ${requested}`);
|
|
318
|
+
const item = matches[0];
|
|
319
|
+
const attributes = item.attributes;
|
|
320
|
+
return {
|
|
321
|
+
id: String(item.id),
|
|
322
|
+
isInternal: attributes.isInternalGroup === true,
|
|
323
|
+
name: String(attributes.name)
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
addBuildToGroup: async ({ buildId, groupId, signal }) => {
|
|
328
|
+
await request(`/betaGroups/${encodeURIComponent(groupId)}/relationships/builds`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
signal,
|
|
331
|
+
body: JSON.stringify({ data: [{ type: "builds", id: buildId }] })
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
upsertWhatsNew: async ({ buildId, locale, text, signal }) => {
|
|
335
|
+
const items = await dataList(`/builds/${encodeURIComponent(buildId)}/betaBuildLocalizations?fields[betaBuildLocalizations]=locale,whatsNew&limit=200`);
|
|
336
|
+
const existing = items.find((item) => isRecord(item.attributes) && item.attributes.locale === locale);
|
|
337
|
+
if (existing) {
|
|
338
|
+
await request(`/betaBuildLocalizations/${encodeURIComponent(String(existing.id))}`, {
|
|
339
|
+
method: "PATCH",
|
|
340
|
+
signal,
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
data: {
|
|
343
|
+
type: "betaBuildLocalizations",
|
|
344
|
+
id: existing.id,
|
|
345
|
+
attributes: { whatsNew: text }
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
await request("/betaBuildLocalizations", {
|
|
351
|
+
method: "POST",
|
|
352
|
+
signal,
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
data: {
|
|
355
|
+
type: "betaBuildLocalizations",
|
|
356
|
+
attributes: { locale, whatsNew: text },
|
|
357
|
+
relationships: {
|
|
358
|
+
build: { data: { type: "builds", id: buildId } }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
submitBetaReview: async ({ buildId, signal }) => {
|
|
366
|
+
await request("/betaAppReviewSubmissions", {
|
|
367
|
+
method: "POST",
|
|
368
|
+
signal,
|
|
369
|
+
body: JSON.stringify({
|
|
370
|
+
data: {
|
|
371
|
+
type: "betaAppReviewSubmissions",
|
|
372
|
+
relationships: { build: { data: { type: "builds", id: buildId } } }
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
hasBetaReviewSubmission: async ({ buildId, signal }) => {
|
|
378
|
+
const response = await request(`/builds/${encodeURIComponent(buildId)}/relationships/betaAppReviewSubmission`, { signal });
|
|
379
|
+
return isRecord(response.data) && typeof response.data.id === "string";
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
var createAppStoreConnectReleasePublisher = (options) => {
|
|
384
|
+
const clock = options.clock ?? (() => new Date);
|
|
385
|
+
const client = options.client ?? (options.auth ? createAppStoreConnectClient({ auth: options.auth, clock }) : undefined);
|
|
386
|
+
if (!client)
|
|
387
|
+
throw new AppStoreConnectReleaseError("App Store Connect auth or client is required");
|
|
388
|
+
const prefix = normalizedPrefix(options.receiptPrefix ?? DEFAULT_RECEIPT_PREFIX);
|
|
389
|
+
const maxWaitMs = options.maxWaitMs ?? 20 * 60000;
|
|
390
|
+
const pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
391
|
+
const preparationKey = (bundleId, buildIdentity) => `${prefix}/${hashIdentity(bundleId)}/preparations/${hashIdentity(buildIdentity)}.json`;
|
|
392
|
+
const receiptKey = (appId, releaseId, intent) => `${prefix}/${hashIdentity(appId)}/${releaseId}/${hashIdentity(JSON.stringify(intent))}.json`;
|
|
393
|
+
const readStored = async (key) => {
|
|
394
|
+
const bytes = await options.receiptStore.get(key);
|
|
395
|
+
if (!bytes)
|
|
396
|
+
return null;
|
|
397
|
+
const head = await options.receiptStore.head(key);
|
|
398
|
+
if (!head || head.size !== bytes.byteLength || head.metadata?.sha256 !== sha256Bytes(bytes))
|
|
399
|
+
throw new AppStoreConnectReleaseError("Stored App Store Connect state failed integrity verification");
|
|
400
|
+
return decodedJson(bytes);
|
|
401
|
+
};
|
|
402
|
+
const writeStored = async (key, value, type) => {
|
|
403
|
+
const bytes = encodedJson(value);
|
|
404
|
+
await options.receiptStore.put(key, bytes, {
|
|
405
|
+
cacheControl: "no-cache",
|
|
406
|
+
contentType: "application/json",
|
|
407
|
+
maxBytes: bytes.byteLength,
|
|
408
|
+
metadata: { sha256: sha256Bytes(bytes), type }
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
const waitForBuild = async (appId, buildNumber, buildUploadId, signal) => {
|
|
412
|
+
const deadline = Date.now() + maxWaitMs;
|
|
413
|
+
while (Date.now() <= deadline) {
|
|
414
|
+
signal?.throwIfAborted();
|
|
415
|
+
const build = await client.findBuild({ appId, buildNumber, signal });
|
|
416
|
+
if (build?.processingState === "VALID")
|
|
417
|
+
return build;
|
|
418
|
+
if (build && (build.processingState === "FAILED" || build.processingState === "INVALID"))
|
|
419
|
+
throw new AppStoreConnectReleaseError(`Apple rejected build ${buildNumber} during processing (${build.processingState})`);
|
|
420
|
+
const upload = await client.getBuildUpload({ buildUploadId, signal });
|
|
421
|
+
if (upload.state === "FAILED")
|
|
422
|
+
throw new AppStoreConnectReleaseError(`Apple rejected build upload ${buildUploadId}`);
|
|
423
|
+
await Bun.sleep(pollIntervalMs);
|
|
424
|
+
}
|
|
425
|
+
throw new AppStoreConnectReleaseError(`Timed out waiting for Apple to process build ${buildNumber}`);
|
|
426
|
+
};
|
|
427
|
+
return {
|
|
428
|
+
...options.registry,
|
|
429
|
+
prepareIosRelease: async ({ buildIdentity, bundleId, signal }) => {
|
|
430
|
+
if (!buildIdentity)
|
|
431
|
+
throw new AppStoreConnectReleaseError("App Store Connect build identity is invalid");
|
|
432
|
+
const key = preparationKey(bundleId, buildIdentity);
|
|
433
|
+
const stored = await readStored(key);
|
|
434
|
+
if (isRecord(stored) && stored.bundleId === bundleId && stored.buildIdentity === buildIdentity && Number.isSafeInteger(stored.buildNumber) && Number(stored.buildNumber) > 0)
|
|
435
|
+
return { buildNumber: Number(stored.buildNumber) };
|
|
436
|
+
const appleAppId = await client.findAppId({ bundleId, signal });
|
|
437
|
+
const numbers = await client.listBuildNumbers({
|
|
438
|
+
appId: appleAppId,
|
|
439
|
+
signal
|
|
440
|
+
});
|
|
441
|
+
const highest = numbers.reduce((maximum, value) => Math.max(maximum, value), 0);
|
|
442
|
+
if (!Number.isSafeInteger(highest) || highest >= Number.MAX_SAFE_INTEGER)
|
|
443
|
+
throw new AppStoreConnectReleaseError("App Store Connect cannot allocate another iOS build number");
|
|
444
|
+
const buildNumber = highest + 1;
|
|
445
|
+
await writeStored(key, {
|
|
446
|
+
appleAppId,
|
|
447
|
+
buildIdentity,
|
|
448
|
+
buildNumber,
|
|
449
|
+
bundleId,
|
|
450
|
+
format: 1,
|
|
451
|
+
preparedAt: clock().toISOString()
|
|
452
|
+
}, "app-store-connect-build-preparation");
|
|
453
|
+
return { buildNumber };
|
|
454
|
+
},
|
|
455
|
+
publish: async (input) => {
|
|
456
|
+
const publication = await options.registry.publish(input);
|
|
457
|
+
const metadata = publication.record.metadata;
|
|
458
|
+
const target = input.appStoreConnect ?? options.target;
|
|
459
|
+
if (!target)
|
|
460
|
+
return publication;
|
|
461
|
+
if (metadata.platform !== "ios" || metadata.type !== "ipa")
|
|
462
|
+
throw new AppStoreConnectReleaseError("App Store Connect publishing requires an iOS IPA release");
|
|
463
|
+
if (!metadata.signed || !metadata.buildNumber)
|
|
464
|
+
throw new AppStoreConnectReleaseError("App Store Connect publishing requires a signed, versioned IPA");
|
|
465
|
+
const intent = normalizedIntent(target);
|
|
466
|
+
const appleAppId = await client.findAppId({
|
|
467
|
+
bundleId: metadata.appId,
|
|
468
|
+
signal: input.signal
|
|
469
|
+
});
|
|
470
|
+
const key = receiptKey(appleAppId, metadata.releaseId, intent);
|
|
471
|
+
const existingValue = await readStored(key);
|
|
472
|
+
let receipt = existingValue ? parseReceipt(existingValue) : {
|
|
473
|
+
appleAppId,
|
|
474
|
+
buildNumber: metadata.buildNumber,
|
|
475
|
+
format: 1,
|
|
476
|
+
intent,
|
|
477
|
+
marketingVersion: metadata.marketingVersion,
|
|
478
|
+
provider: "app-store-connect",
|
|
479
|
+
releaseId: metadata.releaseId,
|
|
480
|
+
sha256: metadata.sha256,
|
|
481
|
+
stage: "preparing",
|
|
482
|
+
updatedAt: clock().toISOString()
|
|
483
|
+
};
|
|
484
|
+
const writeReceipt = async (values) => {
|
|
485
|
+
receipt = { ...receipt, ...values, updatedAt: clock().toISOString() };
|
|
486
|
+
await writeStored(key, receipt, "app-store-connect-release-receipt");
|
|
487
|
+
};
|
|
488
|
+
if (receipt.stage === "distributed" || receipt.stage === "review-submitted")
|
|
489
|
+
return { ...publication, appStoreConnect: { receipt, reused: true } };
|
|
490
|
+
let upload = receipt.buildUploadId ? await client.getBuildUpload({
|
|
491
|
+
buildUploadId: receipt.buildUploadId,
|
|
492
|
+
signal: input.signal
|
|
493
|
+
}) : await client.findBuildUpload({
|
|
494
|
+
appId: appleAppId,
|
|
495
|
+
buildNumber: metadata.buildNumber,
|
|
496
|
+
marketingVersion: metadata.marketingVersion,
|
|
497
|
+
signal: input.signal
|
|
498
|
+
});
|
|
499
|
+
if (!upload)
|
|
500
|
+
upload = await client.createBuildUpload({
|
|
501
|
+
appId: appleAppId,
|
|
502
|
+
buildNumber: metadata.buildNumber,
|
|
503
|
+
marketingVersion: metadata.marketingVersion,
|
|
504
|
+
signal: input.signal
|
|
505
|
+
});
|
|
506
|
+
await writeReceipt({ buildUploadId: upload.id, stage: "uploading" });
|
|
507
|
+
let build = await client.findBuild({
|
|
508
|
+
appId: appleAppId,
|
|
509
|
+
buildNumber: metadata.buildNumber,
|
|
510
|
+
signal: input.signal
|
|
511
|
+
});
|
|
512
|
+
if (!build || build.processingState !== "VALID") {
|
|
513
|
+
let fileId = receipt.buildUploadFileId;
|
|
514
|
+
if (!fileId) {
|
|
515
|
+
fileId = await client.findBuildUploadFile({
|
|
516
|
+
buildUploadId: upload.id,
|
|
517
|
+
signal: input.signal
|
|
518
|
+
}) ?? await client.createBuildUploadFile({
|
|
519
|
+
buildUploadId: upload.id,
|
|
520
|
+
bytes: metadata.bytes,
|
|
521
|
+
fileName: metadata.artifact,
|
|
522
|
+
signal: input.signal
|
|
523
|
+
});
|
|
524
|
+
await writeReceipt({ buildUploadFileId: fileId });
|
|
525
|
+
}
|
|
526
|
+
if (upload.state === "AWAITING_UPLOAD") {
|
|
527
|
+
const artifactPath = path.join(input.releaseRoot, metadata.artifact);
|
|
528
|
+
await client.uploadBuildFile({
|
|
529
|
+
artifactPath,
|
|
530
|
+
fileId,
|
|
531
|
+
signal: input.signal
|
|
532
|
+
});
|
|
533
|
+
await client.commitBuildUploadFile({
|
|
534
|
+
fileId,
|
|
535
|
+
sha256: metadata.sha256,
|
|
536
|
+
signal: input.signal
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
await writeReceipt({ stage: "processing" });
|
|
540
|
+
build = await waitForBuild(appleAppId, metadata.buildNumber, upload.id, input.signal);
|
|
541
|
+
}
|
|
542
|
+
await writeReceipt({ buildId: build.id });
|
|
543
|
+
const groups = await client.resolveGroups({
|
|
544
|
+
appId: appleAppId,
|
|
545
|
+
groups: intent.groups,
|
|
546
|
+
signal: input.signal
|
|
547
|
+
});
|
|
548
|
+
if (intent.submitForReview && groups.some((group) => group.isInternal))
|
|
549
|
+
throw new AppStoreConnectReleaseError("TestFlight beta review may only be requested for external groups");
|
|
550
|
+
for (const note of intent.whatsNew)
|
|
551
|
+
await client.upsertWhatsNew({
|
|
552
|
+
buildId: build.id,
|
|
553
|
+
locale: note.locale,
|
|
554
|
+
text: note.text,
|
|
555
|
+
signal: input.signal
|
|
556
|
+
});
|
|
557
|
+
for (const group of groups)
|
|
558
|
+
await client.addBuildToGroup({
|
|
559
|
+
buildId: build.id,
|
|
560
|
+
groupId: group.id,
|
|
561
|
+
signal: input.signal
|
|
562
|
+
});
|
|
563
|
+
if (intent.submitForReview && !await client.hasBetaReviewSubmission({
|
|
564
|
+
buildId: build.id,
|
|
565
|
+
signal: input.signal
|
|
566
|
+
}))
|
|
567
|
+
await client.submitBetaReview({
|
|
568
|
+
buildId: build.id,
|
|
569
|
+
signal: input.signal
|
|
570
|
+
});
|
|
571
|
+
await writeReceipt({
|
|
572
|
+
stage: intent.submitForReview ? "review-submitted" : "distributed"
|
|
573
|
+
});
|
|
574
|
+
return { ...publication, appStoreConnect: { receipt, reused: false } };
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
};
|
|
578
|
+
export {
|
|
579
|
+
createAppStoreConnectReleasePublisher,
|
|
580
|
+
createAppStoreConnectClient,
|
|
581
|
+
AppStoreConnectReleaseError
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
//# debugId=EB7E25D0A1E879F564756E2164756E21
|
|
585
|
+
//# sourceMappingURL=appStoreConnect.js.map
|