@better-update/cli 0.14.3 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +2247 -1838
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -13,11 +13,11 @@ import { once } from "node:events";
|
|
|
13
13
|
import { createServer } from "node:http";
|
|
14
14
|
import { maxBy, uniqBy } from "es-toolkit";
|
|
15
15
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
16
|
+
import forge from "node-forge";
|
|
16
17
|
import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
import plistMod from "@expo/plist";
|
|
19
20
|
import { ExpoRunFormatter } from "@expo/xcpretty";
|
|
20
|
-
import forge from "node-forge";
|
|
21
21
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
22
22
|
import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
|
|
23
23
|
import qrcode from "qrcode-terminal";
|
|
@@ -28,7 +28,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
28
28
|
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region package.json
|
|
31
|
-
var version = "0.
|
|
31
|
+
var version = "0.15.1";
|
|
32
32
|
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/lib/interactive-mode.ts
|
|
@@ -738,13 +738,27 @@ const IosBuildPushKey = Schema.Struct({
|
|
|
738
738
|
keyId: Schema.String,
|
|
739
739
|
teamId: Schema.String
|
|
740
740
|
});
|
|
741
|
+
/**
|
|
742
|
+
* Auto-provision context surfaced to the CLI. When `ascApiKeyId` is present,
|
|
743
|
+
* the CLI may use it to generate missing provisioning profiles for extension
|
|
744
|
+
* targets (auto-provision flow). `distributionCertificateId` + `appleTeamId`
|
|
745
|
+
* are backend row ids; `appleTeamIdentifier` is the 10-char Apple portal team
|
|
746
|
+
* id (e.g. "ABCDE12345").
|
|
747
|
+
*/
|
|
748
|
+
const IosBuildContext = Schema.Struct({
|
|
749
|
+
ascApiKeyId: Schema.NullOr(Schema.String),
|
|
750
|
+
distributionCertificateId: Schema.String,
|
|
751
|
+
appleTeamId: Schema.String,
|
|
752
|
+
appleTeamIdentifier: Schema.String
|
|
753
|
+
});
|
|
741
754
|
const ResolveBuildCredentialsIosResult = Schema.Struct({
|
|
742
755
|
platform: Schema.Literal("ios"),
|
|
743
756
|
distributionCertificate: IosBuildDistributionCertificate,
|
|
744
757
|
provisioningProfile: IosBuildProvisioningProfile,
|
|
745
758
|
pushKey: Schema.NullOr(IosBuildPushKey),
|
|
746
759
|
profileStale: Schema.Boolean,
|
|
747
|
-
currentDeviceRosterHash: Schema.NullOr(Schema.String)
|
|
760
|
+
currentDeviceRosterHash: Schema.NullOr(Schema.String),
|
|
761
|
+
context: IosBuildContext
|
|
748
762
|
});
|
|
749
763
|
const AndroidBuildKeystore = Schema.Struct({
|
|
750
764
|
keystoreBase64: Schema.String,
|
|
@@ -1638,6 +1652,7 @@ var PresignedUrlExpiredError = class extends Data.TaggedError("PresignedUrlExpir
|
|
|
1638
1652
|
var ArtifactNotFoundError = class extends Data.TaggedError("ArtifactNotFoundError") {};
|
|
1639
1653
|
var KeychainError = class extends Data.TaggedError("KeychainError") {};
|
|
1640
1654
|
var ProvisioningError = class extends Data.TaggedError("ProvisioningError") {};
|
|
1655
|
+
var XcodeProjectError = class extends Data.TaggedError("XcodeProjectError") {};
|
|
1641
1656
|
var EnvExportError = class extends Data.TaggedError("EnvExportError") {};
|
|
1642
1657
|
var UpdatePublishError = class extends Data.TaggedError("UpdatePublishError") {};
|
|
1643
1658
|
var UpdateRollbackError = class extends Data.TaggedError("UpdateRollbackError") {};
|
|
@@ -3310,2047 +3325,2441 @@ const fromHex = (hex) => {
|
|
|
3310
3325
|
};
|
|
3311
3326
|
|
|
3312
3327
|
//#endregion
|
|
3313
|
-
//#region src/lib/
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3328
|
+
//#region src/lib/android-keystore.ts
|
|
3329
|
+
const DEFAULT_KEYSTORE_VALIDITY_DAYS = 1e4;
|
|
3330
|
+
const renderDistinguishedName = (params) => `CN=${params.commonName}, O=${params.organization}`;
|
|
3331
|
+
const generateAndroidKeystore = (input) => Command.exitCode(Command.make("keytool", "-genkeypair", "-v", "-storetype", "JKS", "-keystore", input.outputPath, "-alias", input.keyAlias, "-keyalg", "RSA", "-keysize", "2048", "-validity", String(input.validityDays ?? DEFAULT_KEYSTORE_VALIDITY_DAYS), "-storepass", input.storePassword, "-keypass", input.keyPassword, "-dname", renderDistinguishedName({
|
|
3332
|
+
commonName: input.commonName,
|
|
3333
|
+
organization: input.organization
|
|
3334
|
+
}), "-noprompt").pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
3335
|
+
step: "generate android keystore",
|
|
3336
|
+
exitCode: 1,
|
|
3337
|
+
message: `generate android keystore failed to spawn: ${String(cause)}`
|
|
3338
|
+
})), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
|
|
3339
|
+
step: "generate android keystore",
|
|
3340
|
+
exitCode: code,
|
|
3341
|
+
message: `generate android keystore exited with code ${code}`
|
|
3342
|
+
}))));
|
|
3343
|
+
|
|
3344
|
+
//#endregion
|
|
3345
|
+
//#region src/lib/apple-pem.ts
|
|
3346
|
+
const PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
|
|
3347
|
+
const PEM_FOOTER = "-----END PRIVATE KEY-----";
|
|
3348
|
+
const pemToPkcs8Der = (pem) => {
|
|
3349
|
+
const normalized = pem.replaceAll("\r\n", "\n").trim();
|
|
3350
|
+
const start = normalized.indexOf(PEM_HEADER);
|
|
3351
|
+
const end = normalized.indexOf(PEM_FOOTER);
|
|
3352
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
3353
|
+
const body = normalized.slice(start + 27, end).replaceAll(/\s+/gu, "").trim();
|
|
3354
|
+
if (body.length === 0) return null;
|
|
3355
|
+
try {
|
|
3356
|
+
return fromBase64(body);
|
|
3357
|
+
} catch {
|
|
3358
|
+
return null;
|
|
3359
|
+
}
|
|
3319
3360
|
};
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
const
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
if (tag === "Forbidden") return new MissingCredentialsError({
|
|
3330
|
-
message: message ?? `Permission denied when resolving ${platformLabel} build credentials`,
|
|
3331
|
-
hint: permissionHint
|
|
3332
|
-
});
|
|
3333
|
-
if (tag === "NotFound") return new MissingCredentialsError({
|
|
3334
|
-
message: message ?? `No ${platformLabel} build credentials configured`,
|
|
3335
|
-
hint: bind
|
|
3336
|
-
});
|
|
3337
|
-
if (tag === "BadRequest") return new MissingCredentialsError({
|
|
3338
|
-
message: message ?? `${platformLabel} build credentials are misconfigured`,
|
|
3339
|
-
hint: bind
|
|
3340
|
-
});
|
|
3341
|
-
return new MissingCredentialsError({
|
|
3342
|
-
message: message ?? `Failed to resolve ${platformLabel} build credentials`,
|
|
3343
|
-
hint: bind
|
|
3344
|
-
});
|
|
3361
|
+
|
|
3362
|
+
//#endregion
|
|
3363
|
+
//#region src/lib/apple-asc-jwt.ts
|
|
3364
|
+
var AppleAuthError = class extends Data.TaggedError("AppleAuthError") {};
|
|
3365
|
+
const MAX_JWT_LIFETIME_SECONDS = 1200;
|
|
3366
|
+
const asArrayBuffer = (bytes) => {
|
|
3367
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
3368
|
+
new Uint8Array(buffer).set(bytes);
|
|
3369
|
+
return buffer;
|
|
3345
3370
|
};
|
|
3346
|
-
const
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
distributionType: IOS_DISTRIBUTION_TO_TYPE[options.distribution]
|
|
3354
|
-
}
|
|
3355
|
-
}).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "ios")));
|
|
3356
|
-
if (resolved.platform !== "ios") return yield* Effect.fail(new MissingCredentialsError({
|
|
3357
|
-
message: "Server returned non-iOS credentials for an iOS build request",
|
|
3358
|
-
hint: bindHint
|
|
3359
|
-
}));
|
|
3360
|
-
const p12Path = path.join(options.tempDir, "signing.p12");
|
|
3361
|
-
const profileFilename = `${resolved.provisioningProfile.uuid ?? "profile"}.mobileprovision`;
|
|
3362
|
-
const profilePath = path.join(options.tempDir, profileFilename);
|
|
3363
|
-
yield* fs.writeFile(p12Path, fromBase64(resolved.distributionCertificate.p12Base64));
|
|
3364
|
-
yield* fs.writeFile(profilePath, fromBase64(resolved.provisioningProfile.mobileprovisionBase64));
|
|
3365
|
-
return {
|
|
3366
|
-
p12Path,
|
|
3367
|
-
p12Password: resolved.distributionCertificate.p12Password,
|
|
3368
|
-
profilePath,
|
|
3369
|
-
profileFilename,
|
|
3370
|
-
teamId: resolved.provisioningProfile.teamId
|
|
3371
|
+
const signAscJwt = (credentials) => Effect.gen(function* () {
|
|
3372
|
+
const der = pemToPkcs8Der(credentials.p8Pem);
|
|
3373
|
+
if (der === null) return yield* Effect.fail(new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") }));
|
|
3374
|
+
const header = {
|
|
3375
|
+
alg: "ES256",
|
|
3376
|
+
kid: credentials.keyId,
|
|
3377
|
+
typ: "JWT"
|
|
3371
3378
|
};
|
|
3372
|
-
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
platform: "android",
|
|
3379
|
-
applicationIdentifier: options.applicationIdentifier
|
|
3380
|
-
}
|
|
3381
|
-
}).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
|
|
3382
|
-
if (resolved.platform !== "android") return yield* Effect.fail(new MissingCredentialsError({
|
|
3383
|
-
message: "Server returned non-Android credentials for an Android build request",
|
|
3384
|
-
hint: androidBindHint
|
|
3385
|
-
}));
|
|
3386
|
-
const keystorePath = path.join(options.tempDir, "upload.keystore");
|
|
3387
|
-
yield* fs.writeFile(keystorePath, fromBase64(resolved.keystore.keystoreBase64));
|
|
3388
|
-
return {
|
|
3389
|
-
keystorePath,
|
|
3390
|
-
storePassword: resolved.keystore.storePassword,
|
|
3391
|
-
keyAlias: resolved.keystore.keyAlias,
|
|
3392
|
-
keyPassword: resolved.keystore.keyPassword
|
|
3379
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3380
|
+
const payload = {
|
|
3381
|
+
iss: credentials.issuerId,
|
|
3382
|
+
iat: now,
|
|
3383
|
+
exp: now + MAX_JWT_LIFETIME_SECONDS,
|
|
3384
|
+
aud: "appstoreconnect-v1"
|
|
3393
3385
|
};
|
|
3386
|
+
const signingInput = `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
|
|
3387
|
+
const key = yield* Effect.tryPromise({
|
|
3388
|
+
try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer(der), {
|
|
3389
|
+
name: "ECDSA",
|
|
3390
|
+
namedCurve: "P-256"
|
|
3391
|
+
}, false, ["sign"]),
|
|
3392
|
+
catch: (cause) => new AppleAuthError({ cause })
|
|
3393
|
+
});
|
|
3394
|
+
const signature = yield* Effect.tryPromise({
|
|
3395
|
+
try: async () => crypto.subtle.sign({
|
|
3396
|
+
name: "ECDSA",
|
|
3397
|
+
hash: "SHA-256"
|
|
3398
|
+
}, key, new TextEncoder().encode(signingInput)),
|
|
3399
|
+
catch: (cause) => new AppleAuthError({ cause })
|
|
3400
|
+
});
|
|
3401
|
+
return `${signingInput}.${toBase64Url(new Uint8Array(signature))}`;
|
|
3394
3402
|
});
|
|
3395
3403
|
|
|
3396
3404
|
//#endregion
|
|
3397
|
-
//#region src/lib/
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
const
|
|
3401
|
-
|
|
3402
|
-
if (!
|
|
3403
|
-
return
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3405
|
+
//#region src/lib/apple-asc-client.ts
|
|
3406
|
+
var AscApiError = class extends Data.TaggedError("AscApiError") {};
|
|
3407
|
+
var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
|
|
3408
|
+
const API_BASE = "https://api.appstoreconnect.apple.com";
|
|
3409
|
+
const extractErrors = (body) => {
|
|
3410
|
+
if (!isRecord(body) || !Array.isArray(body["errors"])) return [];
|
|
3411
|
+
return body["errors"].filter((value) => isRecord(value));
|
|
3412
|
+
};
|
|
3413
|
+
const parseApiError = (response, body, raw) => {
|
|
3414
|
+
const [first] = extractErrors(body);
|
|
3415
|
+
return new AscApiError({
|
|
3416
|
+
status: response.status,
|
|
3417
|
+
message: first?.detail ?? first?.title ?? response.statusText,
|
|
3418
|
+
code: first?.code,
|
|
3419
|
+
raw
|
|
3420
|
+
});
|
|
3421
|
+
};
|
|
3422
|
+
const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
|
|
3423
|
+
const response = yield* Effect.tryPromise({
|
|
3424
|
+
try: async () => fetch(`${API_BASE}${path}`, {
|
|
3425
|
+
method: init?.method ?? "GET",
|
|
3426
|
+
...init?.body === void 0 ? {} : { body: init.body },
|
|
3427
|
+
headers: {
|
|
3428
|
+
authorization: `Bearer ${jwt}`,
|
|
3429
|
+
"content-type": "application/json",
|
|
3430
|
+
accept: "application/json"
|
|
3431
|
+
}
|
|
3432
|
+
}),
|
|
3433
|
+
catch: (cause) => new AscNetworkError({ cause })
|
|
3434
|
+
});
|
|
3435
|
+
const text = yield* Effect.tryPromise({
|
|
3436
|
+
try: async () => response.text(),
|
|
3437
|
+
catch: (cause) => new AscNetworkError({ cause })
|
|
3438
|
+
});
|
|
3439
|
+
const body = text.length === 0 ? {} : JSON.parse(text);
|
|
3440
|
+
if (!response.ok) return yield* Effect.fail(parseApiError(response, body, text));
|
|
3441
|
+
return body;
|
|
3407
3442
|
});
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3443
|
+
const toAscCertificate = (value) => {
|
|
3444
|
+
if (!isRecord(value)) return null;
|
|
3445
|
+
const { id, attributes } = value;
|
|
3446
|
+
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
3447
|
+
const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
|
|
3448
|
+
if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
|
|
3411
3449
|
return {
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3450
|
+
id,
|
|
3451
|
+
serialNumber,
|
|
3452
|
+
certificateType,
|
|
3453
|
+
expirationDate,
|
|
3454
|
+
certificateContent: typeof certificateContent === "string" ? certificateContent : null,
|
|
3455
|
+
displayName: typeof displayName === "string" ? displayName : null
|
|
3415
3456
|
};
|
|
3416
|
-
}
|
|
3417
|
-
const
|
|
3418
|
-
|
|
3419
|
-
|
|
3457
|
+
};
|
|
3458
|
+
const toAscBundleId = (value) => {
|
|
3459
|
+
if (!isRecord(value)) return null;
|
|
3460
|
+
const { id, attributes } = value;
|
|
3461
|
+
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
3462
|
+
const { identifier, name } = attributes;
|
|
3463
|
+
if (typeof identifier !== "string" || typeof name !== "string") return null;
|
|
3420
3464
|
return {
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3465
|
+
id,
|
|
3466
|
+
identifier,
|
|
3467
|
+
name
|
|
3424
3468
|
};
|
|
3425
|
-
}
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3469
|
+
};
|
|
3470
|
+
const PROFILE_TYPES = [
|
|
3471
|
+
"IOS_APP_ADHOC",
|
|
3472
|
+
"IOS_APP_DEVELOPMENT",
|
|
3473
|
+
"IOS_APP_STORE",
|
|
3474
|
+
"IOS_APP_INHOUSE"
|
|
3475
|
+
];
|
|
3476
|
+
const asProfileType = (value) => {
|
|
3477
|
+
const match = PROFILE_TYPES.find((entry) => entry === value);
|
|
3478
|
+
return match === void 0 ? null : match;
|
|
3479
|
+
};
|
|
3480
|
+
const toAscProfile = (value) => {
|
|
3481
|
+
if (!isRecord(value)) return null;
|
|
3482
|
+
const { id, attributes } = value;
|
|
3483
|
+
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
3484
|
+
const { name, uuid, expirationDate, profileContent } = attributes;
|
|
3485
|
+
const profileType = asProfileType(attributes["profileType"]);
|
|
3486
|
+
if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
|
|
3433
3487
|
return {
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3488
|
+
id,
|
|
3489
|
+
name,
|
|
3490
|
+
uuid,
|
|
3491
|
+
expirationDate,
|
|
3492
|
+
profileContent,
|
|
3493
|
+
profileType
|
|
3438
3494
|
};
|
|
3439
|
-
}
|
|
3440
|
-
const
|
|
3441
|
-
|
|
3442
|
-
|
|
3495
|
+
};
|
|
3496
|
+
const toAscDevice = (value) => {
|
|
3497
|
+
if (!isRecord(value)) return null;
|
|
3498
|
+
const { id, attributes } = value;
|
|
3499
|
+
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
3500
|
+
const { udid, name } = attributes;
|
|
3501
|
+
if (typeof udid !== "string" || typeof name !== "string") return null;
|
|
3443
3502
|
return {
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
keyPassword: yield* asString$2(record["keyPassword"], "android.keystore.keyPassword")
|
|
3503
|
+
id,
|
|
3504
|
+
udid,
|
|
3505
|
+
name
|
|
3448
3506
|
};
|
|
3507
|
+
};
|
|
3508
|
+
const extractList = (body, map) => {
|
|
3509
|
+
if (!isRecord(body) || !Array.isArray(body["data"])) return [];
|
|
3510
|
+
return body["data"].map(map).filter((value) => value !== null);
|
|
3511
|
+
};
|
|
3512
|
+
const extractSingle = (body, map) => {
|
|
3513
|
+
if (!isRecord(body)) return null;
|
|
3514
|
+
return map(body["data"]);
|
|
3515
|
+
};
|
|
3516
|
+
const malformed = (resource) => new AscApiError({
|
|
3517
|
+
status: 500,
|
|
3518
|
+
message: `Malformed ${resource} response`,
|
|
3519
|
+
code: void 0,
|
|
3520
|
+
raw: ""
|
|
3449
3521
|
});
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: android.googleServiceAccountKey must be an object." });
|
|
3453
|
-
return { path: yield* asString$2(record["path"], "android.googleServiceAccountKey.path") };
|
|
3522
|
+
const withJwt = (credentials, fn) => Effect.gen(function* () {
|
|
3523
|
+
return yield* fn(yield* signAscJwt(credentials));
|
|
3454
3524
|
});
|
|
3455
|
-
const
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
const
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3525
|
+
const listCertificates = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3526
|
+
return extractList(yield* fetchRaw(jwt, `/v1/certificates${params?.certificateType ? `?filter[certificateType]=${params.certificateType}&limit=200` : "?limit=200"}`), toAscCertificate);
|
|
3527
|
+
}));
|
|
3528
|
+
const createCertificate = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3529
|
+
const csrContent = params.csrPem.replaceAll("-----BEGIN CERTIFICATE REQUEST-----", "").replaceAll("-----END CERTIFICATE REQUEST-----", "").replaceAll(/\s+/gu, "");
|
|
3530
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/certificates", {
|
|
3531
|
+
method: "POST",
|
|
3532
|
+
body: JSON.stringify({ data: {
|
|
3533
|
+
type: "certificates",
|
|
3534
|
+
attributes: {
|
|
3535
|
+
csrContent,
|
|
3536
|
+
certificateType: params.certificateType
|
|
3537
|
+
}
|
|
3538
|
+
} })
|
|
3539
|
+
}), toAscCertificate);
|
|
3540
|
+
if (resource === null) return yield* Effect.fail(malformed("certificate"));
|
|
3541
|
+
return resource;
|
|
3542
|
+
}));
|
|
3543
|
+
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3544
|
+
yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
3545
|
+
}));
|
|
3546
|
+
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3547
|
+
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
3548
|
+
}));
|
|
3549
|
+
const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3550
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/bundleIds", {
|
|
3551
|
+
method: "POST",
|
|
3552
|
+
body: JSON.stringify({ data: {
|
|
3553
|
+
type: "bundleIds",
|
|
3554
|
+
attributes: {
|
|
3555
|
+
identifier: params.identifier,
|
|
3556
|
+
name: params.name,
|
|
3557
|
+
platform: "IOS"
|
|
3558
|
+
}
|
|
3559
|
+
} })
|
|
3560
|
+
}), toAscBundleId);
|
|
3561
|
+
if (resource === null) return yield* Effect.fail(malformed("bundleId"));
|
|
3562
|
+
return resource;
|
|
3563
|
+
}));
|
|
3564
|
+
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3565
|
+
return extractList(yield* fetchRaw(jwt, "/v1/devices?limit=200"), toAscDevice);
|
|
3566
|
+
}));
|
|
3567
|
+
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
3568
|
+
const relationships = {
|
|
3569
|
+
bundleId: { data: {
|
|
3570
|
+
type: "bundleIds",
|
|
3571
|
+
id: params.bundleIdAscId
|
|
3572
|
+
} },
|
|
3573
|
+
certificates: { data: params.certificateAscIds.map((id) => ({
|
|
3574
|
+
type: "certificates",
|
|
3575
|
+
id
|
|
3576
|
+
})) },
|
|
3577
|
+
...params.deviceAscIds.length > 0 ? { devices: { data: params.deviceAscIds.map((id) => ({
|
|
3578
|
+
type: "devices",
|
|
3579
|
+
id
|
|
3580
|
+
})) } } : {}
|
|
3463
3581
|
};
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3582
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/profiles", {
|
|
3583
|
+
method: "POST",
|
|
3584
|
+
body: JSON.stringify({ data: {
|
|
3585
|
+
type: "profiles",
|
|
3586
|
+
attributes: {
|
|
3587
|
+
name: params.profileName,
|
|
3588
|
+
profileType: params.profileType
|
|
3589
|
+
},
|
|
3590
|
+
relationships
|
|
3591
|
+
} })
|
|
3592
|
+
}), toAscProfile);
|
|
3593
|
+
if (resource === null) return yield* Effect.fail(malformed("profile"));
|
|
3594
|
+
return resource;
|
|
3595
|
+
}));
|
|
3596
|
+
const isCertificateLimitError = (error) => {
|
|
3597
|
+
if (error._tag !== "AscApiError") return false;
|
|
3598
|
+
return /already have a current.*certificate|pending certificate request/iu.test(error.message);
|
|
3599
|
+
};
|
|
3600
|
+
|
|
3601
|
+
//#endregion
|
|
3602
|
+
//#region src/lib/apple-cert-to-p12.ts
|
|
3603
|
+
var CertParseError = class extends Data.TaggedError("CertParseError") {};
|
|
3604
|
+
const APPLE_TEAM_ID_RE$1 = /^[A-Z0-9]{10}$/u;
|
|
3605
|
+
const stringField = (cert, name) => {
|
|
3606
|
+
const value = cert.subject.getField(name)?.value;
|
|
3607
|
+
return typeof value === "string" ? value : null;
|
|
3608
|
+
};
|
|
3609
|
+
const matchTeamFromCommonName = (cn) => {
|
|
3610
|
+
const match = /\(([A-Z0-9]{10})\)/u.exec(cn);
|
|
3611
|
+
if (match === null) return null;
|
|
3612
|
+
const [, captured] = match;
|
|
3613
|
+
return captured === void 0 ? null : captured;
|
|
3614
|
+
};
|
|
3615
|
+
const extractTeamId$1 = (cert) => {
|
|
3616
|
+
const ou = stringField(cert, "OU");
|
|
3617
|
+
if (ou !== null && APPLE_TEAM_ID_RE$1.test(ou)) return ou;
|
|
3618
|
+
const cn = stringField(cert, "CN");
|
|
3619
|
+
if (cn === null) return null;
|
|
3620
|
+
return matchTeamFromCommonName(cn);
|
|
3621
|
+
};
|
|
3622
|
+
const parseCert = (certDerBytes) => {
|
|
3623
|
+
const asn1 = forge.asn1.fromDer(certDerBytes);
|
|
3624
|
+
return forge.pki.certificateFromAsn1(asn1);
|
|
3625
|
+
};
|
|
3626
|
+
const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
|
|
3627
|
+
const extractCertMetadata = (cert) => Effect.gen(function* () {
|
|
3628
|
+
const appleTeamId = extractTeamId$1(cert);
|
|
3629
|
+
if (appleTeamId === null) return yield* Effect.fail(new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" }));
|
|
3474
3630
|
return {
|
|
3475
|
-
|
|
3476
|
-
|
|
3631
|
+
serialNumber: cert.serialNumber.toUpperCase(),
|
|
3632
|
+
validFrom: cert.validity.notBefore.toISOString(),
|
|
3633
|
+
validUntil: cert.validity.notAfter.toISOString(),
|
|
3634
|
+
appleTeamId,
|
|
3635
|
+
appleTeamName: stringField(cert, "O"),
|
|
3636
|
+
developerIdIdentifier: stringField(cert, "UID"),
|
|
3637
|
+
commonName: stringField(cert, "CN")
|
|
3477
3638
|
};
|
|
3478
3639
|
});
|
|
3479
|
-
const credentialsJsonPath = (projectRoot) => path.join(projectRoot, CREDENTIALS_JSON_FILENAME);
|
|
3480
|
-
const readCredentialsJson = (projectRoot) => Effect.gen(function* () {
|
|
3481
|
-
const fs = yield* FileSystem.FileSystem;
|
|
3482
|
-
const filePath = credentialsJsonPath(projectRoot);
|
|
3483
|
-
if (!(yield* fs.exists(filePath).pipe(Effect.catchAll(() => Effect.succeed(false))))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
|
|
3484
|
-
return yield* parseCredentialsJson(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to read credentials.json: ${String(cause)}` }))));
|
|
3485
|
-
});
|
|
3486
|
-
const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
|
|
3487
|
-
const fs = yield* FileSystem.FileSystem;
|
|
3488
|
-
const filePath = credentialsJsonPath(projectRoot);
|
|
3489
|
-
const body = `${JSON.stringify(data, null, 2)}\n`;
|
|
3490
|
-
yield* fs.writeFileString(filePath, body).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to write credentials.json: ${String(cause)}` })));
|
|
3491
|
-
return filePath;
|
|
3492
|
-
});
|
|
3493
3640
|
/**
|
|
3494
|
-
*
|
|
3641
|
+
* Parse a PKCS#12 base64 bundle and extract certificate metadata. Used by the
|
|
3642
|
+
* Apple-ID flow which receives a P12 directly from `createCertificateAndP12Async`
|
|
3643
|
+
* and needs metadata before uploading to the better-update server.
|
|
3495
3644
|
*/
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
})
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
const data = yield* readCredentialsJson(options.projectRoot).pipe(Effect.mapError((cause) => new MissingCredentialsError({
|
|
3507
|
-
message: `Local credentials.json: ${cause.message}`,
|
|
3508
|
-
hint: "Create credentials.json or switch the build profile's credentialsSource back to \"remote\"."
|
|
3509
|
-
})));
|
|
3510
|
-
if (!data.ios) return yield* new MissingCredentialsError({
|
|
3511
|
-
message: "credentials.json has no `ios` section but the build is for iOS.",
|
|
3512
|
-
hint: "Add an `ios` block to credentials.json or switch credentialsSource to remote."
|
|
3645
|
+
const extractMetadataFromP12 = (params) => Effect.gen(function* () {
|
|
3646
|
+
const certBagOid = forge.pki.oids["certBag"];
|
|
3647
|
+
if (certBagOid === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" }));
|
|
3648
|
+
const [first] = yield* Effect.try({
|
|
3649
|
+
try: () => {
|
|
3650
|
+
const p12Der = forge.util.decode64(params.p12Base64);
|
|
3651
|
+
const p12Asn1 = forge.asn1.fromDer(p12Der);
|
|
3652
|
+
return forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, params.password).getBags({ bagType: certBagOid })[certBagOid] ?? [];
|
|
3653
|
+
},
|
|
3654
|
+
catch: (error) => new CertParseError({ message: `Failed to parse PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}` })
|
|
3513
3655
|
});
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
yield* requirePath(fs, p12Path, "distribution certificate (.p12)");
|
|
3517
|
-
yield* requirePath(fs, profilePath, "provisioning profile (.mobileprovision)");
|
|
3518
|
-
return {
|
|
3519
|
-
p12Path,
|
|
3520
|
-
p12Password: data.ios.distributionCertificate.password,
|
|
3521
|
-
profilePath,
|
|
3522
|
-
profileFilename: path.basename(profilePath),
|
|
3523
|
-
teamId: ""
|
|
3524
|
-
};
|
|
3656
|
+
if (first?.cert === void 0) return yield* Effect.fail(new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" }));
|
|
3657
|
+
return yield* extractCertMetadata(first.cert);
|
|
3525
3658
|
});
|
|
3526
|
-
const
|
|
3527
|
-
const
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3659
|
+
const buildDistributionCertP12 = (params) => Effect.gen(function* () {
|
|
3660
|
+
const result = yield* Effect.try({
|
|
3661
|
+
try: () => {
|
|
3662
|
+
const cert = parseCert(forge.util.decode64(params.certificateContentBase64));
|
|
3663
|
+
const password = generatePassword();
|
|
3664
|
+
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(params.privateKey, [cert], password, {
|
|
3665
|
+
friendlyName: "key",
|
|
3666
|
+
algorithm: "3des"
|
|
3667
|
+
});
|
|
3668
|
+
return {
|
|
3669
|
+
cert,
|
|
3670
|
+
p12Base64: forge.util.encode64(forge.asn1.toDer(p12Asn1).getBytes()),
|
|
3671
|
+
password
|
|
3672
|
+
};
|
|
3673
|
+
},
|
|
3674
|
+
catch: (error) => new CertParseError({ message: `Failed to assemble .p12: ${error instanceof Error ? error.message : String(error)}` })
|
|
3535
3675
|
});
|
|
3536
|
-
const
|
|
3537
|
-
yield* requirePath(fs, keystorePath, "keystore");
|
|
3676
|
+
const metadata = yield* extractCertMetadata(result.cert);
|
|
3538
3677
|
return {
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
});
|
|
3678
|
+
p12Base64: result.p12Base64,
|
|
3679
|
+
password: result.password,
|
|
3680
|
+
metadata
|
|
3681
|
+
};
|
|
3682
|
+
});
|
|
3545
3683
|
|
|
3546
3684
|
//#endregion
|
|
3547
|
-
//#region src/lib/
|
|
3548
|
-
const
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
})
|
|
3553
|
-
|
|
3554
|
-
const hash = createHash("sha256");
|
|
3555
|
-
const stream = createReadStream(path);
|
|
3556
|
-
let byteSize = 0;
|
|
3557
|
-
stream.on("data", (chunk) => {
|
|
3558
|
-
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
3559
|
-
byteSize += buffer.byteLength;
|
|
3560
|
-
hash.update(buffer);
|
|
3561
|
-
});
|
|
3562
|
-
stream.on("error", (error) => {
|
|
3563
|
-
resume(Effect.fail(hashReadError(`Failed to read file for SHA-256: ${error.message}`)));
|
|
3564
|
-
});
|
|
3565
|
-
stream.on("end", () => {
|
|
3566
|
-
resume(Effect.succeed({
|
|
3567
|
-
digest: formatDigest(hash.digest()),
|
|
3568
|
-
byteSize
|
|
3569
|
-
}));
|
|
3685
|
+
//#region src/lib/apple-csr.ts
|
|
3686
|
+
const generateRsaKeyPair = async () => new Promise((resolve) => {
|
|
3687
|
+
forge.pki.rsa.generateKeyPair({
|
|
3688
|
+
bits: 2048,
|
|
3689
|
+
workers: 2
|
|
3690
|
+
}, (_err, keyPair) => {
|
|
3691
|
+
resolve(keyPair);
|
|
3570
3692
|
});
|
|
3571
3693
|
});
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
})
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
*/
|
|
3588
|
-
const sha256Namespaced = (contentType, contentSha256Hex) => {
|
|
3589
|
-
const input = `${contentType}\0${contentSha256Hex}`;
|
|
3590
|
-
return toBase64Url(createHash("sha256").update(input).digest());
|
|
3694
|
+
const generateCertificateSigningRequest = async () => {
|
|
3695
|
+
const keyPair = await generateRsaKeyPair();
|
|
3696
|
+
const csr = forge.pki.createCertificationRequest();
|
|
3697
|
+
csr.publicKey = keyPair.publicKey;
|
|
3698
|
+
csr.setSubject([{
|
|
3699
|
+
name: "commonName",
|
|
3700
|
+
shortName: "CN",
|
|
3701
|
+
value: "PEM"
|
|
3702
|
+
}]);
|
|
3703
|
+
csr.sign(keyPair.privateKey, forge.md.sha1.create());
|
|
3704
|
+
return {
|
|
3705
|
+
csrPem: forge.pki.certificationRequestToPem(csr),
|
|
3706
|
+
privateKeyPem: forge.pki.privateKeyToPem(keyPair.privateKey),
|
|
3707
|
+
privateKey: keyPair.privateKey
|
|
3708
|
+
};
|
|
3591
3709
|
};
|
|
3592
3710
|
|
|
3593
3711
|
//#endregion
|
|
3594
|
-
//#region src/
|
|
3595
|
-
const runStep = (cmd, step) => Command.exitCode(cmd.pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
3596
|
-
step,
|
|
3597
|
-
exitCode: 1,
|
|
3598
|
-
message: `${step} failed to spawn: ${String(cause)}`
|
|
3599
|
-
})), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
|
|
3600
|
-
step,
|
|
3601
|
-
exitCode: code,
|
|
3602
|
-
message: `${step} exited with code ${code}`
|
|
3603
|
-
}))));
|
|
3712
|
+
//#region src/lib/temp-dir.ts
|
|
3604
3713
|
/**
|
|
3605
|
-
*
|
|
3606
|
-
*
|
|
3714
|
+
* Create a scoped temp directory prefixed with "better-update-" and `chmod 0o700`
|
|
3715
|
+
* it so only the current user can read its contents. The directory and all files
|
|
3716
|
+
* inside it are removed when the enclosing scope closes.
|
|
3607
3717
|
*/
|
|
3608
|
-
const
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
})));
|
|
3614
|
-
const stdoutFiber = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => {
|
|
3615
|
-
const formatted = formatter.pipe(line);
|
|
3616
|
-
return formatted.length > 0 ? Effect.sync(() => {
|
|
3617
|
-
for (const output of formatted) process$1.stdout.write(`${output}\n`);
|
|
3618
|
-
}) : Effect.void;
|
|
3619
|
-
}), Effect.mapError((cause) => new BuildFailedError({
|
|
3620
|
-
step,
|
|
3621
|
-
exitCode: 1,
|
|
3622
|
-
message: `${step} stdout stream error: ${String(cause)}`
|
|
3623
|
-
})), Effect.fork);
|
|
3624
|
-
const stderrFiber = yield* proc.stderr.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => Effect.sync(() => process$1.stderr.write(`${line}\n`))), Effect.mapError((cause) => new BuildFailedError({
|
|
3625
|
-
step,
|
|
3626
|
-
exitCode: 1,
|
|
3627
|
-
message: `${step} stderr stream error: ${String(cause)}`
|
|
3628
|
-
})), Effect.fork);
|
|
3629
|
-
yield* Effect.all([Fiber.join(stdoutFiber), Fiber.join(stderrFiber)], { concurrency: 2 }).pipe(Effect.catchAll(() => Effect.void));
|
|
3630
|
-
const code = yield* proc.exitCode.pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
3631
|
-
step,
|
|
3632
|
-
exitCode: 1,
|
|
3633
|
-
message: `${step} exit code error: ${String(cause)}`
|
|
3634
|
-
})));
|
|
3635
|
-
if (code !== 0) {
|
|
3636
|
-
const summary = formatter.getBuildSummary();
|
|
3637
|
-
if (summary) process$1.stderr.write(`${summary}\n`);
|
|
3638
|
-
return yield* new BuildFailedError({
|
|
3639
|
-
step,
|
|
3640
|
-
exitCode: code,
|
|
3641
|
-
message: `${step} exited with code ${code}`
|
|
3642
|
-
});
|
|
3643
|
-
}
|
|
3718
|
+
const acquireBuildTempDir = Effect.gen(function* () {
|
|
3719
|
+
const fs = yield* FileSystem.FileSystem;
|
|
3720
|
+
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "better-update-" });
|
|
3721
|
+
yield* fs.chmod(dir, 448);
|
|
3722
|
+
return dir;
|
|
3644
3723
|
});
|
|
3645
3724
|
|
|
3646
3725
|
//#endregion
|
|
3647
|
-
//#region src/
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
* - no flavor + aab + release → `bundleRelease`
|
|
3654
|
-
* - flavor=prod + aab + release → `bundleProdRelease`
|
|
3655
|
-
* - flavor=prod + apk + debug → `assembleProdDebug`
|
|
3656
|
-
*/
|
|
3657
|
-
const gradleTaskName = (format, flavor, buildType) => {
|
|
3658
|
-
const verb = format === "aab" ? "bundle" : "assemble";
|
|
3659
|
-
return flavor ? `${verb}${capitalize(flavor)}${capitalize(buildType)}` : `${verb}${capitalize(buildType)}`;
|
|
3726
|
+
//#region src/lib/credentials-generator.ts
|
|
3727
|
+
const DISTRIBUTION_TO_PROFILE_TYPE$1 = {
|
|
3728
|
+
APP_STORE: "IOS_APP_STORE",
|
|
3729
|
+
AD_HOC: "IOS_APP_ADHOC",
|
|
3730
|
+
DEVELOPMENT: "IOS_APP_DEVELOPMENT",
|
|
3731
|
+
ENTERPRISE: "IOS_APP_INHOUSE"
|
|
3660
3732
|
};
|
|
3661
|
-
const
|
|
3662
|
-
const
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3733
|
+
const computeDeviceRosterHashHex = (ascDeviceIds) => {
|
|
3734
|
+
const sorted = [...ascDeviceIds].toSorted();
|
|
3735
|
+
return createHash("sha256").update(sorted.join(","), "utf8").digest("hex");
|
|
3736
|
+
};
|
|
3737
|
+
var CertificateLimitError = class extends Data.TaggedError("CertificateLimitError") {};
|
|
3738
|
+
var GenerateFailedError = class extends Data.TaggedError("GenerateFailedError") {};
|
|
3739
|
+
const messageForAscCause = (cause) => {
|
|
3740
|
+
if (cause._tag === "AscApiError") return cause.message;
|
|
3741
|
+
if (cause._tag === "AppleAuthError") return "Apple JWT signing failed";
|
|
3742
|
+
return "Network error talking to Apple";
|
|
3743
|
+
};
|
|
3744
|
+
const wrapAscError = (step) => (cause) => {
|
|
3745
|
+
if (cause._tag === "AscApiError" && isCertificateLimitError(cause)) return new CertificateLimitError({ message: cause.message });
|
|
3746
|
+
return new GenerateFailedError({
|
|
3747
|
+
step,
|
|
3748
|
+
message: messageForAscCause(cause)
|
|
3674
3749
|
});
|
|
3675
|
-
|
|
3750
|
+
};
|
|
3751
|
+
const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(function* () {
|
|
3676
3752
|
const fs = yield* FileSystem.FileSystem;
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
keyAlias:
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
projectRoot,
|
|
3688
|
-
format,
|
|
3689
|
-
...flavor === void 0 ? {} : { flavor },
|
|
3690
|
-
buildType,
|
|
3691
|
-
minMtimeMs: buildStartMs
|
|
3753
|
+
const tempDir = yield* acquireBuildTempDir;
|
|
3754
|
+
const keystorePath = path.join(tempDir, "release.keystore");
|
|
3755
|
+
yield* generateAndroidKeystore({
|
|
3756
|
+
outputPath: keystorePath,
|
|
3757
|
+
keyAlias: input.keyAlias,
|
|
3758
|
+
storePassword: input.storePassword,
|
|
3759
|
+
keyPassword: input.keyPassword,
|
|
3760
|
+
commonName: input.commonName,
|
|
3761
|
+
organization: input.organization,
|
|
3762
|
+
...input.validityDays === void 0 ? {} : { validityDays: input.validityDays }
|
|
3692
3763
|
});
|
|
3693
|
-
const
|
|
3764
|
+
const bytes = yield* fs.readFile(keystorePath);
|
|
3765
|
+
const created = yield* api.androidUploadKeystores.upload({ payload: {
|
|
3766
|
+
keystoreBase64: toBase64(bytes),
|
|
3767
|
+
keyAlias: input.keyAlias,
|
|
3768
|
+
keystorePassword: input.storePassword,
|
|
3769
|
+
keyPassword: input.keyPassword
|
|
3770
|
+
} });
|
|
3694
3771
|
return {
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
sha256
|
|
3772
|
+
id: created.id,
|
|
3773
|
+
keyAlias: created.keyAlias
|
|
3698
3774
|
};
|
|
3699
|
-
});
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
const
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
const lines = [
|
|
3715
|
-
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
3716
|
-
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
3717
|
-
"<plist version=\"1.0\">",
|
|
3718
|
-
"<dict>",
|
|
3719
|
-
" <key>method</key>",
|
|
3720
|
-
`\t<string>${escapeXml(method)}</string>`,
|
|
3721
|
-
" <key>teamID</key>",
|
|
3722
|
-
`\t<string>${escapeXml(teamId)}</string>`,
|
|
3723
|
-
" <key>signingStyle</key>",
|
|
3724
|
-
" <string>manual</string>",
|
|
3725
|
-
" <key>compileBitcode</key>",
|
|
3726
|
-
`\t${boolTag(compileBitcode)}`,
|
|
3727
|
-
" <key>provisioningProfiles</key>",
|
|
3728
|
-
" <dict>",
|
|
3729
|
-
`\t\t<key>${escapeXml(bundleId)}</key>`,
|
|
3730
|
-
`\t\t<string>${escapeXml(provisioningProfileName)}</string>`,
|
|
3731
|
-
" </dict>"
|
|
3732
|
-
];
|
|
3733
|
-
if (method === "app-store") lines.push(" <key>uploadSymbols</key>", " <true/>");
|
|
3734
|
-
lines.push("</dict>", "</plist>", "");
|
|
3735
|
-
return lines.join("\n");
|
|
3736
|
-
};
|
|
3737
|
-
|
|
3738
|
-
//#endregion
|
|
3739
|
-
//#region src/lib/ios-keychain.ts
|
|
3740
|
-
const runOrFail = (cmd, step) => Command.string(cmd).pipe(Effect.mapError((cause) => new KeychainError({ message: `keychain ${step} failed: ${String(cause)}` })));
|
|
3741
|
-
const listCurrentKeychains = Effect.gen(function* () {
|
|
3742
|
-
return (yield* runOrFail(Command.make("security", "list-keychains", "-d", "user"), "list-keychains")).split("\n").map((line) => line.trim().replace(/^"/u, "").replace(/"$/u, "")).filter((line) => line.length > 0);
|
|
3743
|
-
});
|
|
3744
|
-
const parseSigningIdentity = (output) => {
|
|
3745
|
-
const lines = output.split("\n");
|
|
3746
|
-
for (const line of lines) {
|
|
3747
|
-
const match = /"([^"]+)"/u.exec(line);
|
|
3748
|
-
if (match?.[1]) return match[1];
|
|
3749
|
-
}
|
|
3750
|
-
};
|
|
3751
|
-
/**
|
|
3752
|
-
* Acquire an ephemeral macOS keychain, import a `.p12` into it, add it to the
|
|
3753
|
-
* user search list, and tear it all down on scope close. The keychain name is
|
|
3754
|
-
* namespaced as `better-update-<uuid>` and lives in `$tempDir`, so cleanup is
|
|
3755
|
-
* guaranteed under all termination paths.
|
|
3756
|
-
*/
|
|
3757
|
-
const acquireKeychain = ({ tempDir, p12Path, p12Password }) => {
|
|
3758
|
-
const keychainName = `better-update-${randomUUID()}.keychain-db`;
|
|
3759
|
-
const keychainPath = path.join(tempDir, keychainName);
|
|
3760
|
-
const keychainPassword = randomBytes(32).toString("hex");
|
|
3761
|
-
return Effect.acquireRelease(Effect.gen(function* () {
|
|
3762
|
-
const priorKeychains = yield* listCurrentKeychains;
|
|
3763
|
-
yield* runOrFail(Command.make("security", "create-keychain", "-p", keychainPassword, keychainPath), "create-keychain");
|
|
3764
|
-
yield* runOrFail(Command.make("security", "unlock-keychain", "-p", keychainPassword, keychainPath), "unlock-keychain");
|
|
3765
|
-
yield* runOrFail(Command.make("security", "set-keychain-settings", "-t", "3600", "-l", keychainPath), "set-keychain-settings");
|
|
3766
|
-
yield* runOrFail(Command.make("security", "import", p12Path, "-k", keychainPath, "-P", p12Password, "-T", "/usr/bin/codesign"), "import");
|
|
3767
|
-
yield* runOrFail(Command.make("security", "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", keychainPassword, keychainPath), "set-key-partition-list");
|
|
3768
|
-
yield* runOrFail(Command.make("security", "list-keychains", "-d", "user", "-s", keychainPath, ...priorKeychains), "list-keychains -s (add)");
|
|
3769
|
-
const signingIdentity = parseSigningIdentity(yield* runOrFail(Command.make("security", "find-identity", "-v", "-p", "codesigning", keychainPath), "find-identity"));
|
|
3770
|
-
if (!signingIdentity) return yield* new KeychainError({ message: "No code signing identity found after importing .p12 into ephemeral keychain." });
|
|
3771
|
-
return {
|
|
3772
|
-
handle: {
|
|
3773
|
-
keychainName,
|
|
3774
|
-
keychainPath,
|
|
3775
|
-
signingIdentity
|
|
3776
|
-
},
|
|
3777
|
-
priorKeychains
|
|
3778
|
-
};
|
|
3779
|
-
}), ({ priorKeychains }) => Effect.gen(function* () {
|
|
3780
|
-
yield* Command.string(Command.make("security", "list-keychains", "-d", "user", "-s", ...priorKeychains)).pipe(Effect.catchAll(() => Effect.void));
|
|
3781
|
-
yield* Command.string(Command.make("security", "delete-keychain", keychainPath)).pipe(Effect.catchAll(() => Effect.void));
|
|
3782
|
-
})).pipe(Effect.map(({ handle }) => handle));
|
|
3783
|
-
};
|
|
3784
|
-
|
|
3785
|
-
//#endregion
|
|
3786
|
-
//#region src/lib/plist.ts
|
|
3787
|
-
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
3788
|
-
/**
|
|
3789
|
-
* Parse an XML plist string into a typed object.
|
|
3790
|
-
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
3791
|
-
*/
|
|
3792
|
-
const parsePlistXml = (xml) => plist.parse(xml);
|
|
3793
|
-
/**
|
|
3794
|
-
* Parse a binary plist buffer into a typed object.
|
|
3795
|
-
* Uses bplist-parser for Apple's binary plist format.
|
|
3796
|
-
*/
|
|
3797
|
-
const parsePlistBinary = (buffer) => {
|
|
3798
|
-
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
3799
|
-
return result;
|
|
3800
|
-
};
|
|
3801
|
-
const BPLIST_MAGIC = Buffer.from("bplist00");
|
|
3802
|
-
/**
|
|
3803
|
-
* Auto-detect plist format (binary vs XML) and parse accordingly.
|
|
3804
|
-
*/
|
|
3805
|
-
const parsePlist = (data) => data.subarray(0, 8).equals(BPLIST_MAGIC) ? parsePlistBinary(data) : parsePlistXml(data.toString("utf8"));
|
|
3806
|
-
|
|
3807
|
-
//#endregion
|
|
3808
|
-
//#region src/lib/ios-provisioning.ts
|
|
3809
|
-
const getString = (obj, key) => {
|
|
3810
|
-
const value = obj[key];
|
|
3811
|
-
return typeof value === "string" ? value : void 0;
|
|
3812
|
-
};
|
|
3813
|
-
const getFirstArrayString = (obj, key) => {
|
|
3814
|
-
const value = obj[key];
|
|
3815
|
-
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
3816
|
-
};
|
|
3817
|
-
/**
|
|
3818
|
-
* Extract `UUID`, `Name`, and the first `TeamIdentifier` from the XML plist
|
|
3819
|
-
* output of `security cms -D -i <path>`. Returns `ProvisioningError` when any
|
|
3820
|
-
* of the three fields are missing.
|
|
3821
|
-
*/
|
|
3822
|
-
const extractProvisioningInfo = (plistXml) => Effect.gen(function* () {
|
|
3823
|
-
const parsed = yield* Effect.try({
|
|
3824
|
-
try: () => parsePlistXml(plistXml),
|
|
3825
|
-
catch: (error) => new ProvisioningError({ message: `Failed to parse provisioning profile plist: ${error instanceof Error ? error.message : String(error)}` })
|
|
3775
|
+
}));
|
|
3776
|
+
const fetchAscCredentials = (api, ascApiKeyId) => api.ascApiKeys.getCredentials({ path: { id: ascApiKeyId } });
|
|
3777
|
+
const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(function* () {
|
|
3778
|
+
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
3779
|
+
const ascCreds = {
|
|
3780
|
+
keyId: creds.keyId,
|
|
3781
|
+
issuerId: creds.issuerId,
|
|
3782
|
+
p8Pem: creds.p8Pem
|
|
3783
|
+
};
|
|
3784
|
+
const csrResult = yield* Effect.tryPromise({
|
|
3785
|
+
try: generateCertificateSigningRequest,
|
|
3786
|
+
catch: (cause) => new GenerateFailedError({
|
|
3787
|
+
step: "csr",
|
|
3788
|
+
message: `CSR generation failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
3789
|
+
})
|
|
3826
3790
|
});
|
|
3827
|
-
const
|
|
3828
|
-
const
|
|
3829
|
-
|
|
3830
|
-
|
|
3791
|
+
const certificateType = input.certificateType ?? "IOS_DISTRIBUTION";
|
|
3792
|
+
const apple = yield* createCertificate(ascCreds, {
|
|
3793
|
+
csrPem: csrResult.csrPem,
|
|
3794
|
+
certificateType
|
|
3795
|
+
}).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
|
|
3796
|
+
if (apple.certificateContent === null) return yield* Effect.fail(new GenerateFailedError({
|
|
3797
|
+
step: "apple-create-certificate",
|
|
3798
|
+
message: "Apple response missing certificateContent"
|
|
3799
|
+
}));
|
|
3800
|
+
const bundle = yield* buildDistributionCertP12({
|
|
3801
|
+
certificateContentBase64: apple.certificateContent,
|
|
3802
|
+
privateKey: csrResult.privateKey
|
|
3803
|
+
}).pipe(Effect.mapError((cause) => new GenerateFailedError({
|
|
3804
|
+
step: "p12-build",
|
|
3805
|
+
message: cause.message
|
|
3806
|
+
})));
|
|
3807
|
+
const created = yield* api.appleDistributionCertificates.upload({ payload: {
|
|
3808
|
+
p12Base64: bundle.p12Base64,
|
|
3809
|
+
p12Password: bundle.password,
|
|
3810
|
+
serialNumber: bundle.metadata.serialNumber,
|
|
3811
|
+
appleTeamIdentifier: bundle.metadata.appleTeamId,
|
|
3812
|
+
...bundle.metadata.appleTeamName === null ? {} : { appleTeamName: bundle.metadata.appleTeamName },
|
|
3813
|
+
...bundle.metadata.developerIdIdentifier === null ? {} : { developerIdIdentifier: bundle.metadata.developerIdIdentifier },
|
|
3814
|
+
validFrom: bundle.metadata.validFrom,
|
|
3815
|
+
validUntil: bundle.metadata.validUntil
|
|
3816
|
+
} });
|
|
3831
3817
|
return {
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3818
|
+
id: created.id,
|
|
3819
|
+
serialNumber: bundle.metadata.serialNumber,
|
|
3820
|
+
appleTeamId: created.appleTeamId,
|
|
3821
|
+
appleTeamIdentifier: bundle.metadata.appleTeamId,
|
|
3822
|
+
developerPortalIdentifier: apple.id
|
|
3835
3823
|
};
|
|
3836
3824
|
});
|
|
3837
|
-
const
|
|
3838
|
-
|
|
3839
|
-
*
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
const
|
|
3846
|
-
const
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
yield* fs.copyFile(profilePath, installedPath).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to copy provisioning profile into ${installedPath}: ${String(cause)}` })));
|
|
3857
|
-
return {
|
|
3858
|
-
...info,
|
|
3859
|
-
installedPath,
|
|
3860
|
-
ownsInstallation: true
|
|
3861
|
-
};
|
|
3862
|
-
}), (acquired) => Effect.gen(function* () {
|
|
3863
|
-
if (!acquired.ownsInstallation) return;
|
|
3864
|
-
yield* (yield* FileSystem.FileSystem).remove(acquired.installedPath).pipe(Effect.catchAll(() => Effect.void));
|
|
3865
|
-
})).pipe(Effect.map(({ uuid, name, teamId, installedPath }) => ({
|
|
3866
|
-
uuid,
|
|
3867
|
-
name,
|
|
3868
|
-
teamId,
|
|
3869
|
-
installedPath
|
|
3870
|
-
})));
|
|
3871
|
-
|
|
3872
|
-
//#endregion
|
|
3873
|
-
//#region src/lib/post-build-validation.ts
|
|
3874
|
-
/**
|
|
3875
|
-
* Validate an iOS build after xcodebuild completes. Checks:
|
|
3876
|
-
* 1. Bundle ID matches expected value
|
|
3877
|
-
* 2. Provisioning profile UUID matches
|
|
3878
|
-
* 3. Team ID matches
|
|
3879
|
-
*
|
|
3880
|
-
* All checks are non-blocking — returns warnings, never fails the build.
|
|
3881
|
-
*/
|
|
3882
|
-
const validateIosBuild = (params) => Effect.gen(function* () {
|
|
3883
|
-
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
3884
|
-
if (!appDir) return {
|
|
3885
|
-
passed: false,
|
|
3886
|
-
warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
|
|
3825
|
+
const revokeAppleCertificate = (api, input) => Effect.gen(function* () {
|
|
3826
|
+
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
3827
|
+
yield* deleteCertificate({
|
|
3828
|
+
keyId: creds.keyId,
|
|
3829
|
+
issuerId: creds.issuerId,
|
|
3830
|
+
p8Pem: creds.p8Pem
|
|
3831
|
+
}, input.developerPortalIdentifier).pipe(Effect.mapError(wrapAscError("apple-revoke-certificate")));
|
|
3832
|
+
});
|
|
3833
|
+
const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function* () {
|
|
3834
|
+
const local = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === input.distributionCertificateId);
|
|
3835
|
+
if (local === void 0) return yield* Effect.fail(new GenerateFailedError({
|
|
3836
|
+
step: "load-distribution-certificate",
|
|
3837
|
+
message: `Distribution certificate ${input.distributionCertificateId} not found on this account`
|
|
3838
|
+
}));
|
|
3839
|
+
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
3840
|
+
const ascCreds = {
|
|
3841
|
+
keyId: creds.keyId,
|
|
3842
|
+
issuerId: creds.issuerId,
|
|
3843
|
+
p8Pem: creds.p8Pem
|
|
3887
3844
|
};
|
|
3888
|
-
const
|
|
3889
|
-
const
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3845
|
+
const targetSerial = local.serialNumber.toUpperCase();
|
|
3846
|
+
const matching = yield* Effect.all([listCertificates(ascCreds, { certificateType: "IOS_DISTRIBUTION" }), listCertificates(ascCreds, { certificateType: "IOS_DEVELOPMENT" })], { concurrency: 2 }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")));
|
|
3847
|
+
const ascMatch = [...matching[0], ...matching[1]].find((entry) => entry.serialNumber.toUpperCase() === targetSerial);
|
|
3848
|
+
let revokedOnApple = false;
|
|
3849
|
+
if (ascMatch !== void 0) {
|
|
3850
|
+
yield* deleteCertificate(ascCreds, ascMatch.id).pipe(Effect.mapError(wrapAscError("apple-revoke-certificate")));
|
|
3851
|
+
revokedOnApple = true;
|
|
3852
|
+
}
|
|
3853
|
+
let deletedLocally = false;
|
|
3854
|
+
if (input.keepLocal !== true) {
|
|
3855
|
+
yield* api.appleDistributionCertificates.delete({ path: { id: input.distributionCertificateId } });
|
|
3856
|
+
deletedLocally = true;
|
|
3894
3857
|
}
|
|
3895
3858
|
return {
|
|
3896
|
-
|
|
3897
|
-
|
|
3859
|
+
localId: input.distributionCertificateId,
|
|
3860
|
+
serialNumber: local.serialNumber,
|
|
3861
|
+
revokedOnApple,
|
|
3862
|
+
deletedLocally
|
|
3898
3863
|
};
|
|
3899
3864
|
});
|
|
3900
|
-
const
|
|
3901
|
-
const
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
});
|
|
3907
|
-
const checkBundleId = (appDir, expectedBundleId) => Effect.gen(function* () {
|
|
3908
|
-
const fs = yield* FileSystem.FileSystem;
|
|
3909
|
-
const plistPath = path.join(appDir, "Info.plist");
|
|
3910
|
-
const data = yield* fs.readFile(plistPath);
|
|
3911
|
-
const actualBundleId = parsePlist(Buffer.from(data))["CFBundleIdentifier"];
|
|
3912
|
-
if (typeof actualBundleId === "string" && actualBundleId !== expectedBundleId) return `Bundle ID mismatch: expected "${expectedBundleId}", got "${actualBundleId}"`;
|
|
3865
|
+
const listAppleCertificates = (api, input) => Effect.gen(function* () {
|
|
3866
|
+
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
3867
|
+
return yield* listCertificates({
|
|
3868
|
+
keyId: creds.keyId,
|
|
3869
|
+
issuerId: creds.issuerId,
|
|
3870
|
+
p8Pem: creds.p8Pem
|
|
3871
|
+
}, input.certificateType === void 0 ? {} : { certificateType: input.certificateType }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")));
|
|
3913
3872
|
});
|
|
3914
|
-
const
|
|
3915
|
-
const
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
if (Array.isArray(teamIdentifiers)) {
|
|
3922
|
-
const [actualTeamId] = teamIdentifiers;
|
|
3923
|
-
if (typeof actualTeamId === "string" && actualTeamId !== expectedTeamId) warnings.push(`Team ID mismatch: expected "${expectedTeamId}", got "${actualTeamId}"`);
|
|
3924
|
-
}
|
|
3925
|
-
const expirationDate = parsed["ExpirationDate"];
|
|
3926
|
-
if (expirationDate instanceof Date && expirationDate.getTime() < Date.now()) warnings.push(`Embedded provisioning profile expired on ${expirationDate.toISOString()}`);
|
|
3927
|
-
return warnings;
|
|
3873
|
+
const resolveCertAscId = (creds, serialNumber, certificateType) => Effect.gen(function* () {
|
|
3874
|
+
const match = (yield* listCertificates(creds, { certificateType }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")))).find((entry) => entry.serialNumber.toUpperCase() === serialNumber);
|
|
3875
|
+
if (match === void 0) return yield* Effect.fail(new GenerateFailedError({
|
|
3876
|
+
step: "match-apple-certificate",
|
|
3877
|
+
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
3878
|
+
}));
|
|
3879
|
+
return match.id;
|
|
3928
3880
|
});
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
const
|
|
3938
|
-
const
|
|
3881
|
+
const ensureBundleId = (creds, bundleIdentifier) => Effect.gen(function* () {
|
|
3882
|
+
const existing = (yield* listBundleIds(creds).pipe(Effect.mapError(wrapAscError("apple-list-bundle-ids")))).find((entry) => entry.identifier === bundleIdentifier);
|
|
3883
|
+
if (existing !== void 0) return existing.id;
|
|
3884
|
+
return (yield* createBundleId(creds, {
|
|
3885
|
+
identifier: bundleIdentifier,
|
|
3886
|
+
name: bundleIdentifier
|
|
3887
|
+
}).pipe(Effect.mapError(wrapAscError("apple-create-bundle-id")))).id;
|
|
3888
|
+
});
|
|
3889
|
+
const collectDeviceAscIds = (creds, appleTeamId, deviceIds) => Effect.gen(function* () {
|
|
3890
|
+
const devices = yield* listDevices(creds).pipe(Effect.mapError(wrapAscError("apple-list-devices")));
|
|
3939
3891
|
return {
|
|
3940
|
-
|
|
3941
|
-
|
|
3892
|
+
ids: deviceIds === void 0 ? devices.map((device) => device.id) : devices.filter((device) => new Set(deviceIds).has(device.id)).map((device) => device.id),
|
|
3893
|
+
appleTeamId
|
|
3942
3894
|
};
|
|
3943
|
-
};
|
|
3895
|
+
});
|
|
3896
|
+
const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
3897
|
+
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
3898
|
+
const ascCreds = {
|
|
3899
|
+
keyId: creds.keyId,
|
|
3900
|
+
issuerId: creds.issuerId,
|
|
3901
|
+
p8Pem: creds.p8Pem
|
|
3902
|
+
};
|
|
3903
|
+
const cert = yield* api.appleDistributionCertificates.list().pipe(Effect.map(({ items }) => items.find((item) => item.id === input.distributionCertificateId)), Effect.flatMap((match) => match === void 0 ? Effect.fail(new GenerateFailedError({
|
|
3904
|
+
step: "load-distribution-certificate",
|
|
3905
|
+
message: `Distribution certificate ${input.distributionCertificateId} not found`
|
|
3906
|
+
})) : Effect.succeed(match)));
|
|
3907
|
+
const certificateType = input.distributionType === "DEVELOPMENT" ? "IOS_DEVELOPMENT" : "IOS_DISTRIBUTION";
|
|
3908
|
+
const [certAscId, bundleIdAscId] = yield* Effect.all([resolveCertAscId(ascCreds, cert.serialNumber.toUpperCase(), certificateType), ensureBundleId(ascCreds, input.bundleIdentifier)], { concurrency: 2 });
|
|
3909
|
+
const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
|
|
3910
|
+
const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
|
|
3911
|
+
if (useDevices && deviceAscIds.length === 0) return yield* Effect.fail(new GenerateFailedError({
|
|
3912
|
+
step: "collect-devices",
|
|
3913
|
+
message: "No registered devices to attach to the provisioning profile"
|
|
3914
|
+
}));
|
|
3915
|
+
const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
|
|
3916
|
+
profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
|
|
3917
|
+
profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
|
|
3918
|
+
bundleIdAscId,
|
|
3919
|
+
certificateAscIds: [certAscId],
|
|
3920
|
+
deviceAscIds
|
|
3921
|
+
}).pipe(Effect.mapError(wrapAscError("apple-create-profile")))).profileContent);
|
|
3922
|
+
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceAscIds) : void 0;
|
|
3923
|
+
const profileBase64 = toBase64(profileBytes);
|
|
3924
|
+
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
3925
|
+
profileBase64,
|
|
3926
|
+
appleDistributionCertificateId: input.distributionCertificateId,
|
|
3927
|
+
isManaged: true,
|
|
3928
|
+
...rosterHash === void 0 ? {} : { deviceRosterHash: rosterHash }
|
|
3929
|
+
} });
|
|
3930
|
+
return {
|
|
3931
|
+
id: created.id,
|
|
3932
|
+
bundleIdentifier: created.bundleIdentifier,
|
|
3933
|
+
distributionType: created.distributionType,
|
|
3934
|
+
profileName: created.profileName,
|
|
3935
|
+
validUntil: created.validUntil,
|
|
3936
|
+
developerPortalIdentifier: created.developerPortalIdentifier,
|
|
3937
|
+
/** Raw .mobileprovision bytes (base64) — callers can install directly without re-downloading. */
|
|
3938
|
+
profileBase64
|
|
3939
|
+
};
|
|
3940
|
+
});
|
|
3944
3941
|
|
|
3945
3942
|
//#endregion
|
|
3946
|
-
//#region src/
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3943
|
+
//#region src/lib/auto-provision-extension-profiles.ts
|
|
3944
|
+
/**
|
|
3945
|
+
* Mint a fresh provisioning profile for an extension bundle that has no
|
|
3946
|
+
* registered IosBundleConfiguration yet:
|
|
3947
|
+
* 1. Generate the profile via Apple ASC (reusing the main app's dist cert).
|
|
3948
|
+
* 2. Upload the .mobileprovision bytes to the backend so future resolves work.
|
|
3949
|
+
* 3. Create an IosBundleConfiguration binding so the new profile is wired up.
|
|
3950
|
+
*
|
|
3951
|
+
* Returns the profile bytes in-line so the build can install + sign immediately
|
|
3952
|
+
* without a follow-up resolve round-trip.
|
|
3953
|
+
*/
|
|
3954
|
+
const autoProvisionExtensionProfile = (api, input) => Effect.gen(function* () {
|
|
3955
|
+
const generated = yield* generateAndUploadProvisioningProfile(api, {
|
|
3956
|
+
ascApiKeyId: input.ascApiKeyId,
|
|
3957
|
+
distributionCertificateId: input.distributionCertificateId,
|
|
3958
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
3959
|
+
distributionType: input.distributionType
|
|
3953
3960
|
});
|
|
3954
|
-
|
|
3955
|
-
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
let depth = 0;
|
|
3964
|
-
while (stack.length > 0 && depth < 6) {
|
|
3965
|
-
const layer = stack.splice(0);
|
|
3966
|
-
depth += 1;
|
|
3967
|
-
for (const dir of layer) {
|
|
3968
|
-
const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
|
|
3969
|
-
for (const entry of entries) {
|
|
3970
|
-
const full = path.join(dir, entry);
|
|
3971
|
-
if (entry.endsWith(".app")) return full;
|
|
3972
|
-
const stat = yield* fs.stat(full).pipe(Effect.option);
|
|
3973
|
-
if (stat._tag === "Some" && stat.value.type === "Directory") stack.push(full);
|
|
3974
|
-
}
|
|
3961
|
+
const binding = yield* api.iosBundleConfigurations.create({
|
|
3962
|
+
path: { projectId: input.projectId },
|
|
3963
|
+
payload: {
|
|
3964
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
3965
|
+
distributionType: input.distributionType,
|
|
3966
|
+
appleTeamId: input.appleTeamId,
|
|
3967
|
+
appleDistributionCertificateId: input.distributionCertificateId,
|
|
3968
|
+
appleProvisioningProfileId: generated.id,
|
|
3969
|
+
ascApiKeyId: input.ascApiKeyId
|
|
3975
3970
|
}
|
|
3976
|
-
}
|
|
3977
|
-
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
3978
|
-
});
|
|
3979
|
-
const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
3980
|
-
const { projectRoot, iosProfile, envVars, tempDir } = input;
|
|
3981
|
-
const runtime = yield* CliRuntime;
|
|
3982
|
-
const iosDir = path.join(projectRoot, "ios");
|
|
3983
|
-
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
3984
|
-
yield* prebuildAndPods({
|
|
3985
|
-
projectRoot,
|
|
3986
|
-
iosDir,
|
|
3987
|
-
commandEnv
|
|
3988
3971
|
});
|
|
3989
|
-
const workspaceFilename = yield* findXcworkspace(iosDir);
|
|
3990
|
-
const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
|
|
3991
|
-
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
3992
|
-
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
3993
|
-
const buildCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-sdk", "iphonesimulator", "-destination", "generic/platform=iOS Simulator", "-derivedDataPath", derivedDataPath, "build", "CODE_SIGNING_ALLOWED=NO", "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY=").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
3994
|
-
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
3995
|
-
yield* formatter ? runStepFormatted(buildCmd, "xcodebuild build (simulator)", formatter) : runStep(buildCmd, "xcodebuild build (simulator)");
|
|
3996
|
-
const appDir = yield* findAppDirectory(path.join(derivedDataPath, "Build", "Products", `${configuration}-iphonesimulator`));
|
|
3997
|
-
const archiveName = `${path.basename(appDir, ".app")}-simulator.tar.gz`;
|
|
3998
|
-
const archivePath = path.join(tempDir, archiveName);
|
|
3999
|
-
yield* runStep(Command.make("tar", "-czf", archivePath, "-C", path.dirname(appDir), path.basename(appDir)).pipe(Command.env(commandEnv)), "tar simulator .app");
|
|
4000
|
-
const { sha256, byteSize } = yield* sha256File(archivePath);
|
|
4001
3972
|
return {
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
3973
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
3974
|
+
profileBase64: generated.profileBase64,
|
|
3975
|
+
profileUuid: generated.developerPortalIdentifier ?? input.bundleIdentifier,
|
|
3976
|
+
profileName: generated.profileName,
|
|
3977
|
+
appleProvisioningProfileId: generated.id,
|
|
3978
|
+
iosBundleConfigurationId: binding.id
|
|
4005
3979
|
};
|
|
4006
3980
|
});
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
3981
|
+
|
|
3982
|
+
//#endregion
|
|
3983
|
+
//#region src/lib/credentials-downloader.ts
|
|
3984
|
+
const IOS_DISTRIBUTION_TO_TYPE = {
|
|
3985
|
+
"app-store": "APP_STORE",
|
|
3986
|
+
"ad-hoc": "AD_HOC",
|
|
3987
|
+
development: "DEVELOPMENT",
|
|
3988
|
+
enterprise: "ENTERPRISE"
|
|
3989
|
+
};
|
|
3990
|
+
const bindHint = "Bind the bundle via the dashboard (Credentials → iOS Bundle Configurations) and make sure a distribution certificate, provisioning profile, and ASC API key are attached.";
|
|
3991
|
+
const permissionHint = "Ask an org admin to grant the build-credentials download permission.";
|
|
3992
|
+
const androidBindHint = "Register the package in the dashboard (Credentials → Android Build Credentials) and bind a keystore to the default group.";
|
|
3993
|
+
const hasTag$1 = (cause) => typeof cause === "object" && cause !== null && "_tag" in cause;
|
|
3994
|
+
const resolveErrorToMissingCredentials = (cause, platform, bundleIdentifier) => {
|
|
3995
|
+
const tag = hasTag$1(cause) ? cause._tag : null;
|
|
3996
|
+
const message = hasTag$1(cause) && typeof cause.message === "string" ? cause.message : null;
|
|
3997
|
+
const platformLabel = platform === "ios" ? "iOS" : "Android";
|
|
3998
|
+
const bind = platform === "ios" ? bindHint : androidBindHint;
|
|
3999
|
+
const bundleSuffix = bundleIdentifier ? ` (bundle "${bundleIdentifier}")` : "";
|
|
4000
|
+
if (tag === "Forbidden") return new MissingCredentialsError({
|
|
4001
|
+
message: message ?? `Permission denied when resolving ${platformLabel} build credentials${bundleSuffix}`,
|
|
4002
|
+
hint: permissionHint
|
|
4018
4003
|
});
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
commandEnv
|
|
4004
|
+
if (tag === "NotFound") return new MissingCredentialsError({
|
|
4005
|
+
message: message ?? `No ${platformLabel} build credentials configured${bundleSuffix}`,
|
|
4006
|
+
hint: bind
|
|
4023
4007
|
});
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
p12Password: credentials.p12Password
|
|
4008
|
+
if (tag === "BadRequest") return new MissingCredentialsError({
|
|
4009
|
+
message: message ?? `${platformLabel} build credentials are misconfigured${bundleSuffix}`,
|
|
4010
|
+
hint: bind
|
|
4028
4011
|
});
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4012
|
+
return new MissingCredentialsError({
|
|
4013
|
+
message: message ?? `Failed to resolve ${platformLabel} build credentials${bundleSuffix}`,
|
|
4014
|
+
hint: bind
|
|
4015
|
+
});
|
|
4016
|
+
};
|
|
4017
|
+
const resolveOneBundleSettled = (api, options) => api.buildCredentials.resolve({
|
|
4018
|
+
path: { projectId: options.projectId },
|
|
4019
|
+
payload: {
|
|
4020
|
+
platform: "ios",
|
|
4021
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
4022
|
+
distributionType: IOS_DISTRIBUTION_TO_TYPE[options.distribution]
|
|
4023
|
+
}
|
|
4024
|
+
}).pipe(Effect.flatMap((resolved) => {
|
|
4025
|
+
if (resolved.platform !== "ios") return Effect.succeed({
|
|
4026
|
+
status: "failed",
|
|
4027
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
4028
|
+
error: new MissingCredentialsError({
|
|
4029
|
+
message: `Server returned non-iOS credentials for iOS bundle "${options.bundleIdentifier}"`,
|
|
4030
|
+
hint: bindHint
|
|
4031
|
+
})
|
|
4032
|
+
});
|
|
4033
|
+
return Effect.succeed({
|
|
4034
|
+
status: "ok",
|
|
4035
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
4036
|
+
value: {
|
|
4037
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
4038
|
+
p12Base64: resolved.distributionCertificate.p12Base64,
|
|
4039
|
+
p12Password: resolved.distributionCertificate.p12Password,
|
|
4040
|
+
mobileprovisionBase64: resolved.provisioningProfile.mobileprovisionBase64,
|
|
4041
|
+
profileUuid: resolved.provisioningProfile.uuid,
|
|
4042
|
+
context: resolved.context
|
|
4043
|
+
}
|
|
4044
|
+
});
|
|
4045
|
+
}), Effect.catchAll((cause) => {
|
|
4046
|
+
if ((hasTag$1(cause) ? cause._tag : null) === "NotFound") return Effect.succeed({
|
|
4047
|
+
status: "not-registered",
|
|
4048
|
+
bundleIdentifier: options.bundleIdentifier
|
|
4049
|
+
});
|
|
4050
|
+
return Effect.succeed({
|
|
4051
|
+
status: "failed",
|
|
4052
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
4053
|
+
error: resolveErrorToMissingCredentials(cause, "ios", options.bundleIdentifier)
|
|
4054
|
+
});
|
|
4055
|
+
}));
|
|
4056
|
+
const autoProvisionHint = "Upload an ASC API key for this Apple team in the dashboard (Credentials → ASC API Keys) so missing extension bundles can be auto-provisioned, or register them manually.";
|
|
4057
|
+
const downloadIosCredentials = (api, options) => Effect.gen(function* () {
|
|
4037
4058
|
const fs = yield* FileSystem.FileSystem;
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4059
|
+
if (options.bundleIdentifiers.length === 0) return yield* new MissingCredentialsError({
|
|
4060
|
+
message: "downloadIosCredentials called with an empty bundleIdentifiers list.",
|
|
4061
|
+
hint: bindHint
|
|
4062
|
+
});
|
|
4063
|
+
if (!options.bundleIdentifiers.includes(options.mainBundleIdentifier)) return yield* new MissingCredentialsError({
|
|
4064
|
+
message: `Main bundle "${options.mainBundleIdentifier}" missing from bundleIdentifiers list.`,
|
|
4065
|
+
hint: bindHint
|
|
4066
|
+
});
|
|
4067
|
+
const settled = yield* Effect.forEach(options.bundleIdentifiers, (bundleIdentifier) => resolveOneBundleSettled(api, {
|
|
4068
|
+
projectId: options.projectId,
|
|
4069
|
+
bundleIdentifier,
|
|
4070
|
+
distribution: options.distribution
|
|
4071
|
+
}), { concurrency: 4 });
|
|
4072
|
+
const hardFailure = settled.find((entry) => entry.status === "failed");
|
|
4073
|
+
if (hardFailure) return yield* hardFailure.error;
|
|
4074
|
+
const mainEntry = settled.find((entry) => entry.bundleIdentifier === options.mainBundleIdentifier);
|
|
4075
|
+
if (mainEntry?.status !== "ok") return yield* new MissingCredentialsError({
|
|
4076
|
+
message: `Main app bundle "${options.mainBundleIdentifier}" is not registered on the backend.`,
|
|
4077
|
+
hint: bindHint
|
|
4078
|
+
});
|
|
4079
|
+
const resolved = settled.filter((entry) => entry.status === "ok");
|
|
4080
|
+
const missing = settled.filter((entry) => entry.status === "not-registered").map((entry) => entry.bundleIdentifier);
|
|
4081
|
+
const provisioned = yield* maybeAutoProvision(api, {
|
|
4082
|
+
mainContext: mainEntry.value.context,
|
|
4083
|
+
missing,
|
|
4084
|
+
projectId: options.projectId,
|
|
4085
|
+
distributionType: IOS_DISTRIBUTION_TO_TYPE[options.distribution]
|
|
4086
|
+
});
|
|
4087
|
+
const p12Path = path.join(options.tempDir, "signing.p12");
|
|
4088
|
+
yield* fs.writeFile(p12Path, fromBase64(mainEntry.value.p12Base64));
|
|
4089
|
+
const profiles = [];
|
|
4090
|
+
for (const entry of resolved) profiles.push(yield* writeProfile(fs, options.tempDir, {
|
|
4091
|
+
bundleIdentifier: entry.value.bundleIdentifier,
|
|
4092
|
+
base64: entry.value.mobileprovisionBase64,
|
|
4093
|
+
uuid: entry.value.profileUuid
|
|
4044
4094
|
}));
|
|
4045
|
-
const
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4095
|
+
for (const entry of provisioned) profiles.push(yield* writeProfile(fs, options.tempDir, {
|
|
4096
|
+
bundleIdentifier: entry.bundleIdentifier,
|
|
4097
|
+
base64: entry.profileBase64,
|
|
4098
|
+
uuid: entry.profileUuid
|
|
4099
|
+
}));
|
|
4100
|
+
return {
|
|
4101
|
+
p12Path,
|
|
4102
|
+
p12Password: mainEntry.value.p12Password,
|
|
4103
|
+
profiles
|
|
4104
|
+
};
|
|
4105
|
+
});
|
|
4106
|
+
const maybeAutoProvision = (api, params) => Effect.gen(function* () {
|
|
4107
|
+
if (params.missing.length === 0) return [];
|
|
4108
|
+
const { ascApiKeyId } = params.mainContext;
|
|
4109
|
+
if (ascApiKeyId === null) return yield* new MissingCredentialsError({
|
|
4110
|
+
message: `No iOS bundle configuration for extension bundle(s) ${params.missing.map((id) => `"${id}"`).join(", ")}, and no ASC API key is available for this Apple team to auto-provision them.`,
|
|
4111
|
+
hint: autoProvisionHint
|
|
4053
4112
|
});
|
|
4054
|
-
|
|
4055
|
-
|
|
4113
|
+
yield* Console.log(`Auto-provisioning ${params.missing.length} missing extension profile(s) via Apple ASC...`);
|
|
4114
|
+
return yield* Effect.forEach(params.missing, (bundleIdentifier) => autoProvisionExtensionProfile(api, {
|
|
4115
|
+
projectId: params.projectId,
|
|
4116
|
+
bundleIdentifier,
|
|
4117
|
+
distributionType: params.distributionType,
|
|
4118
|
+
ascApiKeyId,
|
|
4119
|
+
distributionCertificateId: params.mainContext.distributionCertificateId,
|
|
4120
|
+
appleTeamId: params.mainContext.appleTeamId
|
|
4121
|
+
}).pipe(Effect.map((created) => ({
|
|
4122
|
+
bundleIdentifier: created.bundleIdentifier,
|
|
4123
|
+
profileBase64: created.profileBase64,
|
|
4124
|
+
profileUuid: created.profileUuid
|
|
4125
|
+
})), Effect.mapError((cause) => {
|
|
4126
|
+
const tag = hasTag$1(cause) ? cause._tag : "AutoProvisionError";
|
|
4127
|
+
return new MissingCredentialsError({
|
|
4128
|
+
message: `Auto-provision failed for "${bundleIdentifier}": ${hasTag$1(cause) && typeof cause.message === "string" ? cause.message : `Failed to auto-provision profile for "${bundleIdentifier}" (${tag})`}`,
|
|
4129
|
+
hint: autoProvisionHint
|
|
4130
|
+
});
|
|
4131
|
+
})), { concurrency: 2 });
|
|
4132
|
+
});
|
|
4133
|
+
const writeProfile = (fs, tempDir, params) => Effect.gen(function* () {
|
|
4134
|
+
const profileFilename = `${params.uuid ?? params.bundleIdentifier}.mobileprovision`;
|
|
4135
|
+
const profilePath = path.join(tempDir, profileFilename);
|
|
4136
|
+
yield* fs.writeFile(profilePath, fromBase64(params.base64));
|
|
4056
4137
|
return {
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4138
|
+
bundleIdentifier: params.bundleIdentifier,
|
|
4139
|
+
profilePath,
|
|
4140
|
+
profileFilename
|
|
4141
|
+
};
|
|
4142
|
+
});
|
|
4143
|
+
const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
|
|
4144
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4145
|
+
const resolved = yield* api.buildCredentials.resolve({
|
|
4146
|
+
path: { projectId: options.projectId },
|
|
4147
|
+
payload: {
|
|
4148
|
+
platform: "android",
|
|
4149
|
+
applicationIdentifier: options.applicationIdentifier
|
|
4150
|
+
}
|
|
4151
|
+
}).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
|
|
4152
|
+
if (resolved.platform !== "android") return yield* Effect.fail(new MissingCredentialsError({
|
|
4153
|
+
message: "Server returned non-Android credentials for an Android build request",
|
|
4154
|
+
hint: androidBindHint
|
|
4155
|
+
}));
|
|
4156
|
+
const keystorePath = path.join(options.tempDir, "upload.keystore");
|
|
4157
|
+
yield* fs.writeFile(keystorePath, fromBase64(resolved.keystore.keystoreBase64));
|
|
4158
|
+
return {
|
|
4159
|
+
keystorePath,
|
|
4160
|
+
storePassword: resolved.keystore.storePassword,
|
|
4161
|
+
keyAlias: resolved.keystore.keyAlias,
|
|
4162
|
+
keyPassword: resolved.keystore.keyPassword
|
|
4060
4163
|
};
|
|
4061
4164
|
});
|
|
4062
|
-
const runIosBuild = (input) => input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
4063
4165
|
|
|
4064
4166
|
//#endregion
|
|
4065
|
-
//#region src/
|
|
4066
|
-
const
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
...input.gitContext.ref === void 0 ? {} : { gitRef: input.gitContext.ref },
|
|
4076
|
-
...input.gitContext.commit === void 0 ? {} : { gitCommit: input.gitContext.commit },
|
|
4077
|
-
...input.message === void 0 ? {} : { message: input.message }
|
|
4167
|
+
//#region src/lib/credentials-json.ts
|
|
4168
|
+
const CREDENTIALS_JSON_FILENAME = "credentials.json";
|
|
4169
|
+
const asString$2 = (value, field) => typeof value === "string" && value.length > 0 ? Effect.succeed(value) : Effect.fail(new CredentialsJsonError({ message: `credentials.json: field "${field}" must be a non-empty string.` }));
|
|
4170
|
+
const parseIosDistributionCertificate = (raw) => Effect.gen(function* () {
|
|
4171
|
+
const record = asRecord(raw);
|
|
4172
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: ios.distributionCertificate must be an object." });
|
|
4173
|
+
return {
|
|
4174
|
+
path: yield* asString$2(record["path"], "ios.distributionCertificate.path"),
|
|
4175
|
+
password: yield* asString$2(record["password"], "ios.distributionCertificate.password")
|
|
4176
|
+
};
|
|
4078
4177
|
});
|
|
4079
|
-
const
|
|
4080
|
-
const
|
|
4081
|
-
|
|
4082
|
-
if (target.platform === "ios") return target.distribution === "simulator" ? api.builds.reserve({ payload: {
|
|
4083
|
-
...common,
|
|
4084
|
-
platform: "ios",
|
|
4085
|
-
distribution: "simulator",
|
|
4086
|
-
artifactFormat: "tar.gz"
|
|
4087
|
-
} }) : api.builds.reserve({ payload: {
|
|
4088
|
-
...common,
|
|
4089
|
-
platform: "ios",
|
|
4090
|
-
distribution: target.distribution,
|
|
4091
|
-
artifactFormat: "ipa"
|
|
4092
|
-
} });
|
|
4093
|
-
return target.distribution === "play-store" ? api.builds.reserve({ payload: {
|
|
4094
|
-
...common,
|
|
4095
|
-
platform: "android",
|
|
4096
|
-
distribution: "play-store",
|
|
4097
|
-
artifactFormat: "aab"
|
|
4098
|
-
} }) : api.builds.reserve({ payload: {
|
|
4099
|
-
...common,
|
|
4100
|
-
platform: "android",
|
|
4101
|
-
distribution: "direct",
|
|
4102
|
-
artifactFormat: "apk"
|
|
4103
|
-
} });
|
|
4104
|
-
};
|
|
4105
|
-
/**
|
|
4106
|
-
* Reserve a build record on the server, upload the artifact to the returned
|
|
4107
|
-
* presigned URL, and finalize the build with its sha256 + byteSize.
|
|
4108
|
-
*/
|
|
4109
|
-
const reserveAndUpload = (api, input) => Effect.gen(function* () {
|
|
4110
|
-
const presignedUploadClient = yield* PresignedUploadClient;
|
|
4111
|
-
const reserveResult = yield* callReserve(api, input).pipe(Effect.mapError((cause) => new ReserveError({ message: `Failed to reserve build: ${formatCause(cause)}` })));
|
|
4112
|
-
yield* presignedUploadClient.putToPresignedUrl({
|
|
4113
|
-
url: reserveResult.uploadUrl,
|
|
4114
|
-
filePath: input.artifactPath,
|
|
4115
|
-
byteSize: input.byteSize,
|
|
4116
|
-
expiresAt: reserveResult.uploadExpiresAt,
|
|
4117
|
-
headers: reserveResult.uploadHeaders
|
|
4118
|
-
});
|
|
4119
|
-
const completed = yield* api.builds.complete({
|
|
4120
|
-
path: { id: reserveResult.id },
|
|
4121
|
-
payload: {
|
|
4122
|
-
sha256: input.sha256,
|
|
4123
|
-
byteSize: input.byteSize
|
|
4124
|
-
}
|
|
4125
|
-
}).pipe(Effect.mapError((cause) => new CompleteError({ message: `Failed to complete build ${reserveResult.id}: ${formatCause(cause)}` })));
|
|
4126
|
-
if (!completed.artifact) return yield* new CompleteError({ message: `Build ${completed.id} completed but server returned no artifact record.` });
|
|
4178
|
+
const parseIosPushKey = (raw) => Effect.gen(function* () {
|
|
4179
|
+
const record = asRecord(raw);
|
|
4180
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: ios.pushKey must be an object." });
|
|
4127
4181
|
return {
|
|
4128
|
-
|
|
4129
|
-
|
|
4182
|
+
path: yield* asString$2(record["path"], "ios.pushKey.path"),
|
|
4183
|
+
keyId: yield* asString$2(record["keyId"], "ios.pushKey.keyId"),
|
|
4184
|
+
teamId: yield* asString$2(record["teamId"], "ios.pushKey.teamId")
|
|
4130
4185
|
};
|
|
4131
4186
|
});
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4187
|
+
const parseIosAscApiKey = (raw) => Effect.gen(function* () {
|
|
4188
|
+
const record = asRecord(raw);
|
|
4189
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: ios.ascApiKey must be an object." });
|
|
4190
|
+
return {
|
|
4191
|
+
path: yield* asString$2(record["path"], "ios.ascApiKey.path"),
|
|
4192
|
+
keyId: yield* asString$2(record["keyId"], "ios.ascApiKey.keyId"),
|
|
4193
|
+
issuerId: yield* asString$2(record["issuerId"], "ios.ascApiKey.issuerId")
|
|
4194
|
+
};
|
|
4140
4195
|
});
|
|
4141
|
-
const
|
|
4142
|
-
const
|
|
4143
|
-
if (!
|
|
4144
|
-
return
|
|
4196
|
+
const parseIosAdditionalProvisioningProfile = (raw, index) => Effect.gen(function* () {
|
|
4197
|
+
const record = asRecord(raw);
|
|
4198
|
+
if (!record) return yield* new CredentialsJsonError({ message: `credentials.json: ios.additionalProvisioningProfiles[${index}] must be an object.` });
|
|
4199
|
+
return {
|
|
4200
|
+
bundleIdentifier: yield* asString$2(record["bundleIdentifier"], `ios.additionalProvisioningProfiles[${index}].bundleIdentifier`),
|
|
4201
|
+
path: yield* asString$2(record["path"], `ios.additionalProvisioningProfiles[${index}].path`)
|
|
4202
|
+
};
|
|
4145
4203
|
});
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
const
|
|
4150
|
-
|
|
4151
|
-
const [, major, minor, patch, suffix] = match;
|
|
4152
|
-
const nextPatch = Number.parseInt(patch ?? "0", 10) + 1;
|
|
4153
|
-
return `${major ?? "0"}.${minor ?? "0"}.${String(nextPatch)}${suffix ?? ""}`;
|
|
4204
|
+
const parseIosAdditionalProvisioningProfiles = (raw) => Effect.gen(function* () {
|
|
4205
|
+
if (!Array.isArray(raw)) return yield* new CredentialsJsonError({ message: "credentials.json: ios.additionalProvisioningProfiles must be an array." });
|
|
4206
|
+
const entries = [];
|
|
4207
|
+
for (const [index, item] of raw.entries()) entries.push(yield* parseIosAdditionalProvisioningProfile(item, index));
|
|
4208
|
+
return entries;
|
|
4154
4209
|
});
|
|
4155
|
-
const
|
|
4156
|
-
|
|
4210
|
+
const parseIos = (raw) => Effect.gen(function* () {
|
|
4211
|
+
const record = asRecord(raw);
|
|
4212
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: \"ios\" must be an object." });
|
|
4213
|
+
const provisioningProfilePath = yield* asString$2(record["provisioningProfilePath"], "ios.provisioningProfilePath");
|
|
4214
|
+
const distributionCertificate = yield* parseIosDistributionCertificate(record["distributionCertificate"]);
|
|
4215
|
+
const additionalProvisioningProfiles = record["additionalProvisioningProfiles"] === void 0 ? void 0 : yield* parseIosAdditionalProvisioningProfiles(record["additionalProvisioningProfiles"]);
|
|
4216
|
+
const pushKey = record["pushKey"] === void 0 ? void 0 : yield* parseIosPushKey(record["pushKey"]);
|
|
4217
|
+
const ascApiKey = record["ascApiKey"] === void 0 ? void 0 : yield* parseIosAscApiKey(record["ascApiKey"]);
|
|
4157
4218
|
return {
|
|
4158
|
-
|
|
4159
|
-
|
|
4219
|
+
provisioningProfilePath,
|
|
4220
|
+
distributionCertificate,
|
|
4221
|
+
...additionalProvisioningProfiles === void 0 ? {} : { additionalProvisioningProfiles },
|
|
4222
|
+
...pushKey === void 0 ? {} : { pushKey },
|
|
4223
|
+
...ascApiKey === void 0 ? {} : { ascApiKey }
|
|
4160
4224
|
};
|
|
4161
4225
|
});
|
|
4162
|
-
const
|
|
4163
|
-
|
|
4226
|
+
const parseAndroidKeystore = (raw) => Effect.gen(function* () {
|
|
4227
|
+
const record = asRecord(raw);
|
|
4228
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: android.keystore must be an object." });
|
|
4164
4229
|
return {
|
|
4165
|
-
|
|
4166
|
-
|
|
4230
|
+
keystorePath: yield* asString$2(record["keystorePath"], "android.keystore.keystorePath"),
|
|
4231
|
+
keystorePassword: yield* asString$2(record["keystorePassword"], "android.keystore.keystorePassword"),
|
|
4232
|
+
keyAlias: yield* asString$2(record["keyAlias"], "android.keystore.keyAlias"),
|
|
4233
|
+
keyPassword: yield* asString$2(record["keyPassword"], "android.keystore.keyPassword")
|
|
4167
4234
|
};
|
|
4168
4235
|
});
|
|
4169
|
-
const
|
|
4170
|
-
const
|
|
4171
|
-
if (
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
};
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4236
|
+
const parseGoogleServiceAccountKey = (raw) => Effect.gen(function* () {
|
|
4237
|
+
const record = asRecord(raw);
|
|
4238
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: android.googleServiceAccountKey must be an object." });
|
|
4239
|
+
return { path: yield* asString$2(record["path"], "android.googleServiceAccountKey.path") };
|
|
4240
|
+
});
|
|
4241
|
+
const parseAndroid = (raw) => Effect.gen(function* () {
|
|
4242
|
+
const record = asRecord(raw);
|
|
4243
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: \"android\" must be an object." });
|
|
4244
|
+
const keystore = yield* parseAndroidKeystore(record["keystore"]);
|
|
4245
|
+
const googleServiceAccountKey = record["googleServiceAccountKey"] === void 0 ? void 0 : yield* parseGoogleServiceAccountKey(record["googleServiceAccountKey"]);
|
|
4246
|
+
return {
|
|
4247
|
+
keystore,
|
|
4248
|
+
...googleServiceAccountKey === void 0 ? {} : { googleServiceAccountKey }
|
|
4249
|
+
};
|
|
4250
|
+
});
|
|
4251
|
+
const parseCredentialsJson = (raw) => Effect.gen(function* () {
|
|
4252
|
+
const root = asRecord(yield* Effect.try({
|
|
4253
|
+
try: () => JSON.parse(raw),
|
|
4254
|
+
catch: () => new CredentialsJsonError({ message: "credentials.json is not valid JSON." })
|
|
4255
|
+
}));
|
|
4256
|
+
if (!root) return yield* new CredentialsJsonError({ message: "credentials.json must be a JSON object at the top level." });
|
|
4257
|
+
const ios = root["ios"] === void 0 ? void 0 : yield* parseIos(root["ios"]);
|
|
4258
|
+
const android = root["android"] === void 0 ? void 0 : yield* parseAndroid(root["android"]);
|
|
4259
|
+
if (!ios && !android) return yield* new CredentialsJsonError({ message: "credentials.json must contain at least one of \"ios\" or \"android\"." });
|
|
4260
|
+
return {
|
|
4261
|
+
...ios === void 0 ? {} : { ios },
|
|
4262
|
+
...android === void 0 ? {} : { android }
|
|
4263
|
+
};
|
|
4264
|
+
});
|
|
4265
|
+
const credentialsJsonPath = (projectRoot) => path.join(projectRoot, CREDENTIALS_JSON_FILENAME);
|
|
4266
|
+
const readCredentialsJson = (projectRoot) => Effect.gen(function* () {
|
|
4267
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4268
|
+
const filePath = credentialsJsonPath(projectRoot);
|
|
4269
|
+
if (!(yield* fs.exists(filePath).pipe(Effect.catchAll(() => Effect.succeed(false))))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
|
|
4270
|
+
return yield* parseCredentialsJson(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to read credentials.json: ${String(cause)}` }))));
|
|
4271
|
+
});
|
|
4272
|
+
const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
|
|
4273
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4274
|
+
const filePath = credentialsJsonPath(projectRoot);
|
|
4275
|
+
const body = `${JSON.stringify(data, null, 2)}\n`;
|
|
4276
|
+
yield* fs.writeFileString(filePath, body).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to write credentials.json: ${String(cause)}` })));
|
|
4277
|
+
return filePath;
|
|
4278
|
+
});
|
|
4188
4279
|
/**
|
|
4189
|
-
*
|
|
4190
|
-
* autoIncrement mode, persist via `@expo/config.modifyConfigAsync`, and log a
|
|
4191
|
-
* Human-readable summary. No-op when the mode is undefined. Returns the new
|
|
4192
|
-
* Bumped values so callers can refresh their in-memory ExpoConfig.
|
|
4280
|
+
* Resolve a path that may be either absolute or relative to the project root.
|
|
4193
4281
|
*/
|
|
4194
|
-
const
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4282
|
+
const resolveCredentialPath = (projectRoot, candidate) => path.isAbsolute(candidate) ? candidate : path.join(projectRoot, candidate);
|
|
4283
|
+
|
|
4284
|
+
//#endregion
|
|
4285
|
+
//#region src/lib/local-credentials.ts
|
|
4286
|
+
const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.catchAll(() => Effect.succeed(false)), Effect.flatMap((exists) => exists ? Effect.void : Effect.fail(new MissingCredentialsError({
|
|
4287
|
+
message: `Local credentials.json: ${label} not found at ${absolutePath}.`,
|
|
4288
|
+
hint: "Run `better-update credentials sync pull` to materialize files, or fix the path in credentials.json."
|
|
4289
|
+
}))));
|
|
4290
|
+
const loadLocalIosCredentials = (options) => Effect.gen(function* () {
|
|
4291
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4292
|
+
const data = yield* readCredentialsJson(options.projectRoot).pipe(Effect.mapError((cause) => new MissingCredentialsError({
|
|
4293
|
+
message: `Local credentials.json: ${cause.message}`,
|
|
4294
|
+
hint: "Create credentials.json or switch the build profile's credentialsSource back to \"remote\"."
|
|
4295
|
+
})));
|
|
4296
|
+
if (!data.ios) return yield* new MissingCredentialsError({
|
|
4297
|
+
message: "credentials.json has no `ios` section but the build is for iOS.",
|
|
4298
|
+
hint: "Add an `ios` block to credentials.json or switch credentialsSource to remote."
|
|
4299
|
+
});
|
|
4300
|
+
const p12Path = resolveCredentialPath(options.projectRoot, data.ios.distributionCertificate.path);
|
|
4301
|
+
yield* requirePath(fs, p12Path, "distribution certificate (.p12)");
|
|
4302
|
+
const mainProfilePath = resolveCredentialPath(options.projectRoot, data.ios.provisioningProfilePath);
|
|
4303
|
+
yield* requirePath(fs, mainProfilePath, "provisioning profile (.mobileprovision)");
|
|
4304
|
+
const profiles = [{
|
|
4305
|
+
bundleIdentifier: options.mainBundleIdentifier,
|
|
4306
|
+
profilePath: mainProfilePath,
|
|
4307
|
+
profileFilename: path.basename(mainProfilePath)
|
|
4308
|
+
}];
|
|
4309
|
+
for (const extra of data.ios.additionalProvisioningProfiles ?? []) {
|
|
4310
|
+
const extraPath = resolveCredentialPath(options.projectRoot, extra.path);
|
|
4311
|
+
yield* requirePath(fs, extraPath, `provisioning profile for ${extra.bundleIdentifier} (.mobileprovision)`);
|
|
4312
|
+
profiles.push({
|
|
4313
|
+
bundleIdentifier: extra.bundleIdentifier,
|
|
4314
|
+
profilePath: extraPath,
|
|
4315
|
+
profileFilename: path.basename(extraPath)
|
|
4316
|
+
});
|
|
4202
4317
|
}
|
|
4203
|
-
|
|
4204
|
-
|
|
4318
|
+
return {
|
|
4319
|
+
p12Path,
|
|
4320
|
+
p12Password: data.ios.distributionCertificate.password,
|
|
4321
|
+
profiles
|
|
4322
|
+
};
|
|
4323
|
+
});
|
|
4324
|
+
const loadLocalAndroidCredentials = (options) => Effect.gen(function* () {
|
|
4325
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4326
|
+
const data = yield* readCredentialsJson(options.projectRoot).pipe(Effect.mapError((cause) => new MissingCredentialsError({
|
|
4327
|
+
message: `Local credentials.json: ${cause.message}`,
|
|
4328
|
+
hint: "Create credentials.json or switch the build profile's credentialsSource back to \"remote\"."
|
|
4329
|
+
})));
|
|
4330
|
+
if (!data.android) return yield* new MissingCredentialsError({
|
|
4331
|
+
message: "credentials.json has no `android` section but the build is for Android.",
|
|
4332
|
+
hint: "Add an `android` block to credentials.json or switch credentialsSource to remote."
|
|
4333
|
+
});
|
|
4334
|
+
const keystorePath = resolveCredentialPath(options.projectRoot, data.android.keystore.keystorePath);
|
|
4335
|
+
yield* requirePath(fs, keystorePath, "keystore");
|
|
4336
|
+
return {
|
|
4337
|
+
keystorePath,
|
|
4338
|
+
storePassword: data.android.keystore.keystorePassword,
|
|
4339
|
+
keyAlias: data.android.keystore.keyAlias,
|
|
4340
|
+
keyPassword: data.android.keystore.keyPassword
|
|
4341
|
+
};
|
|
4205
4342
|
});
|
|
4206
4343
|
|
|
4207
4344
|
//#endregion
|
|
4208
|
-
//#region src/lib/
|
|
4209
|
-
const
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
const
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
};
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
};
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
}
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
const
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
4253
|
-
};
|
|
4254
|
-
const asEasDistribution = (raw) => {
|
|
4255
|
-
const value = asStringValue(raw);
|
|
4256
|
-
return value === "internal" || value === "store" ? value : void 0;
|
|
4257
|
-
};
|
|
4258
|
-
const asCredentialsSource = (raw) => {
|
|
4259
|
-
const value = asStringValue(raw);
|
|
4260
|
-
return value === "remote" || value === "local" ? value : void 0;
|
|
4261
|
-
};
|
|
4262
|
-
const parseIosProfile = (raw) => {
|
|
4263
|
-
const record = asRecord(raw);
|
|
4264
|
-
if (!record) return;
|
|
4265
|
-
const distribution = asIosDistribution(record["distribution"]);
|
|
4266
|
-
const buildConfiguration = asStringValue(record["buildConfiguration"]);
|
|
4267
|
-
const scheme = asStringValue(record["scheme"]);
|
|
4268
|
-
const simulator = asBooleanValue(record["simulator"]);
|
|
4269
|
-
const enterpriseProvisioning = asEnterpriseProvisioning(record["enterpriseProvisioning"]);
|
|
4270
|
-
const autoIncrement = asIosAutoIncrement(record["autoIncrement"]);
|
|
4271
|
-
return {
|
|
4272
|
-
...distribution === void 0 ? {} : { distribution },
|
|
4273
|
-
...buildConfiguration === void 0 ? {} : { buildConfiguration },
|
|
4274
|
-
...scheme === void 0 ? {} : { scheme },
|
|
4275
|
-
...simulator === void 0 ? {} : { simulator },
|
|
4276
|
-
...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning },
|
|
4277
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4278
|
-
};
|
|
4279
|
-
};
|
|
4280
|
-
const parseAndroidProfile = (raw) => {
|
|
4281
|
-
const record = asRecord(raw);
|
|
4282
|
-
if (!record) return;
|
|
4283
|
-
const buildType = asAndroidBuildType(record["buildType"]);
|
|
4284
|
-
const flavor = asStringValue(record["flavor"]);
|
|
4285
|
-
const gradleCommand = asStringValue(record["gradleCommand"]);
|
|
4286
|
-
const format = asAndroidFormat(record["format"]);
|
|
4287
|
-
const distribution = asAndroidDistribution(record["distribution"]);
|
|
4288
|
-
const autoIncrement = asAndroidAutoIncrement(record["autoIncrement"]);
|
|
4289
|
-
return {
|
|
4290
|
-
...buildType === void 0 ? {} : { buildType },
|
|
4291
|
-
...flavor === void 0 ? {} : { flavor },
|
|
4292
|
-
...gradleCommand === void 0 ? {} : { gradleCommand },
|
|
4293
|
-
...format === void 0 ? {} : { format },
|
|
4294
|
-
...distribution === void 0 ? {} : { distribution },
|
|
4295
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4296
|
-
};
|
|
4297
|
-
};
|
|
4298
|
-
const parseBuildProfile = (raw) => {
|
|
4299
|
-
const record = asRecord(raw);
|
|
4300
|
-
if (!record) return;
|
|
4301
|
-
const extendsName = asStringValue(record["extends"]);
|
|
4302
|
-
const developmentClient = asBooleanValue(record["developmentClient"]);
|
|
4303
|
-
const distribution = asEasDistribution(record["distribution"]);
|
|
4304
|
-
const channel = asStringValue(record["channel"]);
|
|
4305
|
-
const environment = asStringValue(record["environment"]);
|
|
4306
|
-
const env = asEnv(record["env"]);
|
|
4307
|
-
const ios = parseIosProfile(record["ios"]);
|
|
4308
|
-
const android = parseAndroidProfile(record["android"]);
|
|
4309
|
-
const credentialsSource = asCredentialsSource(record["credentialsSource"]);
|
|
4310
|
-
const autoIncrement = asAutoIncrement(record["autoIncrement"]);
|
|
4311
|
-
return {
|
|
4312
|
-
...extendsName === void 0 ? {} : { extends: extendsName },
|
|
4313
|
-
...developmentClient === void 0 ? {} : { developmentClient },
|
|
4314
|
-
...distribution === void 0 ? {} : { distribution },
|
|
4315
|
-
...channel === void 0 ? {} : { channel },
|
|
4316
|
-
...environment === void 0 ? {} : { environment },
|
|
4317
|
-
...env === void 0 ? {} : { env },
|
|
4318
|
-
...ios === void 0 ? {} : { ios },
|
|
4319
|
-
...android === void 0 ? {} : { android },
|
|
4320
|
-
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
4321
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4322
|
-
};
|
|
4345
|
+
//#region src/lib/sha256.ts
|
|
4346
|
+
const hashReadError = (message) => new BuildFailedError({
|
|
4347
|
+
step: "sha256",
|
|
4348
|
+
exitCode: 1,
|
|
4349
|
+
message
|
|
4350
|
+
});
|
|
4351
|
+
const hashFile = (path, formatDigest) => Effect.async((resume) => {
|
|
4352
|
+
const hash = createHash("sha256");
|
|
4353
|
+
const stream = createReadStream(path);
|
|
4354
|
+
let byteSize = 0;
|
|
4355
|
+
stream.on("data", (chunk) => {
|
|
4356
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
4357
|
+
byteSize += buffer.byteLength;
|
|
4358
|
+
hash.update(buffer);
|
|
4359
|
+
});
|
|
4360
|
+
stream.on("error", (error) => {
|
|
4361
|
+
resume(Effect.fail(hashReadError(`Failed to read file for SHA-256: ${error.message}`)));
|
|
4362
|
+
});
|
|
4363
|
+
stream.on("end", () => {
|
|
4364
|
+
resume(Effect.succeed({
|
|
4365
|
+
digest: formatDigest(hash.digest()),
|
|
4366
|
+
byteSize
|
|
4367
|
+
}));
|
|
4368
|
+
});
|
|
4369
|
+
});
|
|
4370
|
+
/**
|
|
4371
|
+
* Compute the SHA-256 digest and byte size of a file using Node's streaming
|
|
4372
|
+
* hash API. The file is never fully loaded into memory — chunks flow through
|
|
4373
|
+
* `createReadStream` into `crypto.createHash("sha256")`.
|
|
4374
|
+
*/
|
|
4375
|
+
const sha256File = (path) => hashFile(path, (digest) => digest.toString("hex")).pipe(Effect.map(({ digest, byteSize }) => ({
|
|
4376
|
+
sha256: digest,
|
|
4377
|
+
byteSize
|
|
4378
|
+
})));
|
|
4379
|
+
/**
|
|
4380
|
+
* Compute a content-type-namespaced hash: `SHA-256(contentType + '\0' + SHA-256_hex(fileBytes))`.
|
|
4381
|
+
*
|
|
4382
|
+
* This prevents hash collisions when identical bytes are served with different
|
|
4383
|
+
* MIME types (e.g. same file used as both `application/javascript` and `text/plain`).
|
|
4384
|
+
* The raw content hash is still needed separately for R2 upload verification.
|
|
4385
|
+
*/
|
|
4386
|
+
const sha256Namespaced = (contentType, contentSha256Hex) => {
|
|
4387
|
+
const input = `${contentType}\0${contentSha256Hex}`;
|
|
4388
|
+
return toBase64Url(createHash("sha256").update(input).digest());
|
|
4323
4389
|
};
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4390
|
+
|
|
4391
|
+
//#endregion
|
|
4392
|
+
//#region src/commands/build/run-step.ts
|
|
4393
|
+
const runStep = (cmd, step) => Command.exitCode(cmd.pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
4394
|
+
step,
|
|
4395
|
+
exitCode: 1,
|
|
4396
|
+
message: `${step} failed to spawn: ${String(cause)}`
|
|
4397
|
+
})), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
|
|
4398
|
+
step,
|
|
4399
|
+
exitCode: code,
|
|
4400
|
+
message: `${step} exited with code ${code}`
|
|
4401
|
+
}))));
|
|
4402
|
+
/**
|
|
4403
|
+
* Run a build step with stdout piped through a formatter (e.g., xcpretty).
|
|
4404
|
+
* stderr passes through to the terminal directly.
|
|
4405
|
+
*/
|
|
4406
|
+
const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
|
|
4407
|
+
const proc = yield* Command.start(cmd.pipe(Command.stdout("pipe"), Command.stderr("pipe"))).pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
4408
|
+
step,
|
|
4409
|
+
exitCode: 1,
|
|
4410
|
+
message: `${step} failed to spawn: ${String(cause)}`
|
|
4411
|
+
})));
|
|
4412
|
+
const stdoutFiber = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => {
|
|
4413
|
+
const formatted = formatter.pipe(line);
|
|
4414
|
+
return formatted.length > 0 ? Effect.sync(() => {
|
|
4415
|
+
for (const output of formatted) process$1.stdout.write(`${output}\n`);
|
|
4416
|
+
}) : Effect.void;
|
|
4417
|
+
}), Effect.mapError((cause) => new BuildFailedError({
|
|
4418
|
+
step,
|
|
4419
|
+
exitCode: 1,
|
|
4420
|
+
message: `${step} stdout stream error: ${String(cause)}`
|
|
4421
|
+
})), Effect.fork);
|
|
4422
|
+
const stderrFiber = yield* proc.stderr.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => Effect.sync(() => process$1.stderr.write(`${line}\n`))), Effect.mapError((cause) => new BuildFailedError({
|
|
4423
|
+
step,
|
|
4424
|
+
exitCode: 1,
|
|
4425
|
+
message: `${step} stderr stream error: ${String(cause)}`
|
|
4426
|
+
})), Effect.fork);
|
|
4427
|
+
yield* Effect.all([Fiber.join(stdoutFiber), Fiber.join(stderrFiber)], { concurrency: 2 }).pipe(Effect.catchAll(() => Effect.void));
|
|
4428
|
+
const code = yield* proc.exitCode.pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
4429
|
+
step,
|
|
4430
|
+
exitCode: 1,
|
|
4431
|
+
message: `${step} exit code error: ${String(cause)}`
|
|
4432
|
+
})));
|
|
4433
|
+
if (code !== 0) {
|
|
4434
|
+
const summary = formatter.getBuildSummary();
|
|
4435
|
+
if (summary) process$1.stderr.write(`${summary}\n`);
|
|
4436
|
+
return yield* new BuildFailedError({
|
|
4437
|
+
step,
|
|
4438
|
+
exitCode: code,
|
|
4439
|
+
message: `${step} exited with code ${code}`
|
|
4440
|
+
});
|
|
4336
4441
|
}
|
|
4337
|
-
return {
|
|
4338
|
-
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
4339
|
-
build: profiles
|
|
4340
|
-
};
|
|
4341
4442
|
});
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4443
|
+
|
|
4444
|
+
//#endregion
|
|
4445
|
+
//#region src/commands/build/android.ts
|
|
4446
|
+
/**
|
|
4447
|
+
* Compose the Gradle task name from flavor, format, and buildType.
|
|
4448
|
+
*
|
|
4449
|
+
* Gradle naming convention: `<verb><Flavor><Variant>`, e.g.
|
|
4450
|
+
* - no flavor + apk + release → `assembleRelease`
|
|
4451
|
+
* - no flavor + aab + release → `bundleRelease`
|
|
4452
|
+
* - flavor=prod + aab + release → `bundleProdRelease`
|
|
4453
|
+
* - flavor=prod + apk + debug → `assembleProdDebug`
|
|
4454
|
+
*/
|
|
4455
|
+
const gradleTaskName = (format, flavor, buildType) => {
|
|
4456
|
+
const verb = format === "aab" ? "bundle" : "assemble";
|
|
4457
|
+
return flavor ? `${verb}${capitalize(flavor)}${capitalize(buildType)}` : `${verb}${capitalize(buildType)}`;
|
|
4347
4458
|
};
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
const
|
|
4459
|
+
const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
4460
|
+
const { api, tempDir, projectRoot, androidProfile, applicationIdentifier, envVars, projectId } = input;
|
|
4461
|
+
const runtime = yield* CliRuntime;
|
|
4462
|
+
const buildStartMs = Date.now();
|
|
4463
|
+
const { format } = androidProfile;
|
|
4464
|
+
const { flavor } = androidProfile;
|
|
4465
|
+
const buildType = androidProfile.buildType ?? "release";
|
|
4466
|
+
const androidDir = path.join(projectRoot, "android");
|
|
4467
|
+
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
4468
|
+
const credentials = input.credentialsSource === "local" ? yield* loadLocalAndroidCredentials({ projectRoot }) : yield* downloadAndroidCredentials(api, {
|
|
4469
|
+
projectId,
|
|
4470
|
+
applicationIdentifier,
|
|
4471
|
+
tempDir
|
|
4472
|
+
});
|
|
4473
|
+
yield* runStep(Command.make("bunx", "expo", "prebuild", "--platform", "android", "--clean").pipe(Command.workingDirectory(projectRoot), Command.env(commandEnv)), "expo prebuild android");
|
|
4352
4474
|
const fs = yield* FileSystem.FileSystem;
|
|
4353
|
-
const
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
};
|
|
4371
|
-
};
|
|
4372
|
-
const mergeEnv = (base, overlay) => {
|
|
4373
|
-
if (!base) return overlay;
|
|
4374
|
-
if (!overlay) return base;
|
|
4375
|
-
return {
|
|
4376
|
-
...base,
|
|
4377
|
-
...overlay
|
|
4378
|
-
};
|
|
4379
|
-
};
|
|
4380
|
-
const mergeProfile = (base, overlay) => {
|
|
4381
|
-
const ios = mergeIos(base.ios, overlay.ios);
|
|
4382
|
-
const android = mergeAndroid(base.android, overlay.android);
|
|
4383
|
-
const env = mergeEnv(base.env, overlay.env);
|
|
4384
|
-
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
4385
|
-
const distribution = overlay.distribution ?? base.distribution;
|
|
4386
|
-
const channel = overlay.channel ?? base.channel;
|
|
4387
|
-
const environment = overlay.environment ?? base.environment;
|
|
4388
|
-
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
4389
|
-
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
4475
|
+
const signingGradlePath = path.join(tempDir, "signing.gradle");
|
|
4476
|
+
yield* fs.writeFileString(signingGradlePath, renderSigningGradle({
|
|
4477
|
+
keystorePath: credentials.keystorePath,
|
|
4478
|
+
storePassword: credentials.storePassword,
|
|
4479
|
+
keyAlias: credentials.keyAlias,
|
|
4480
|
+
keyPassword: credentials.keyPassword
|
|
4481
|
+
}));
|
|
4482
|
+
const taskName = gradleTaskName(format, flavor, buildType);
|
|
4483
|
+
yield* runStep(Command.make("./gradlew", "--init-script", signingGradlePath, `:app:${taskName}`).pipe(Command.workingDirectory(androidDir), Command.env(commandEnv)), "gradlew");
|
|
4484
|
+
const artifactPath = yield* findAndroidArtifact({
|
|
4485
|
+
projectRoot,
|
|
4486
|
+
format,
|
|
4487
|
+
...flavor === void 0 ? {} : { flavor },
|
|
4488
|
+
buildType,
|
|
4489
|
+
minMtimeMs: buildStartMs
|
|
4490
|
+
});
|
|
4491
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
4390
4492
|
return {
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
...channel === void 0 ? {} : { channel },
|
|
4395
|
-
...environment === void 0 ? {} : { environment },
|
|
4396
|
-
...env === void 0 ? {} : { env },
|
|
4397
|
-
...ios === void 0 ? {} : { ios },
|
|
4398
|
-
...android === void 0 ? {} : { android },
|
|
4399
|
-
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
4400
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4493
|
+
artifactPath,
|
|
4494
|
+
byteSize,
|
|
4495
|
+
sha256
|
|
4401
4496
|
};
|
|
4402
|
-
};
|
|
4403
|
-
const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
|
|
4404
|
-
const chain = [];
|
|
4405
|
-
const visited = /* @__PURE__ */ new Set();
|
|
4406
|
-
let current = profileName;
|
|
4407
|
-
let depth = 0;
|
|
4408
|
-
while (current !== void 0) {
|
|
4409
|
-
if (visited.has(current)) return yield* new BuildProfileError({ message: `Cycle detected in eas.json build.${profileName} extends chain at "${current}".` });
|
|
4410
|
-
visited.add(current);
|
|
4411
|
-
const profile = profiles[current];
|
|
4412
|
-
if (!profile) return yield* new BuildProfileError({ message: current === profileName ? `Build profile "${profileName}" not found in eas.json.` : `Build profile "${profileName}" extends missing profile "${current}".` });
|
|
4413
|
-
chain.unshift(profile);
|
|
4414
|
-
current = profile.extends;
|
|
4415
|
-
depth += 1;
|
|
4416
|
-
if (depth > MAX_EXTENDS_DEPTH) return yield* new BuildProfileError({ message: `Too many "extends" levels (max ${String(MAX_EXTENDS_DEPTH)}) in eas.json build.${profileName}.` });
|
|
4417
|
-
}
|
|
4418
|
-
return chain;
|
|
4419
4497
|
});
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4498
|
+
|
|
4499
|
+
//#endregion
|
|
4500
|
+
//#region src/lib/ios-codesign-pbxproj.ts
|
|
4501
|
+
const loadXcodeModule$1 = () => __require("xcode");
|
|
4502
|
+
const findXcodeProjectDir$1 = (iosDir) => Effect.gen(function* () {
|
|
4503
|
+
const projectDir = (yield* (yield* FileSystem.FileSystem).readDirectory(iosDir).pipe(Effect.mapError((cause) => new XcodeProjectError({ message: `Failed to read ${iosDir}: ${String(cause)}` })))).find((entry) => entry.endsWith(".xcodeproj"));
|
|
4504
|
+
if (!projectDir) return yield* new XcodeProjectError({ message: `No .xcodeproj directory found under ${iosDir}.` });
|
|
4505
|
+
return path.join(iosDir, projectDir);
|
|
4506
|
+
});
|
|
4507
|
+
const parseProject$1 = (pbxprojPath) => Effect.try({
|
|
4508
|
+
try: () => loadXcodeModule$1().project(pbxprojPath).parseSync(),
|
|
4509
|
+
catch: (cause) => new XcodeProjectError({ message: `Failed to parse ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
4510
|
+
});
|
|
4511
|
+
/**
|
|
4512
|
+
* Always wrap a value in double quotes for safe pbxproj serialization. The
|
|
4513
|
+
* `xcode` writer emits values verbatim (e.g. `KEY = %s;`), so any string with
|
|
4514
|
+
* spaces, brackets or non-identifier characters needs explicit quoting.
|
|
4515
|
+
*/
|
|
4516
|
+
const quote = (value) => `"${value.replaceAll("\"", String.raw`\"`)}"`;
|
|
4517
|
+
const SDK_CONDITIONAL_IDENTITY_KEYS = ["\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"", "CODE_SIGN_IDENTITY[sdk=iphoneos*]"];
|
|
4518
|
+
const mutateConfig = (project, configUuid, settings) => {
|
|
4519
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
4520
|
+
if (!cfg || typeof cfg === "string") return false;
|
|
4521
|
+
cfg.buildSettings["CODE_SIGN_STYLE"] = "Manual";
|
|
4522
|
+
cfg.buildSettings["DEVELOPMENT_TEAM"] = quote(settings.teamId);
|
|
4523
|
+
cfg.buildSettings["CODE_SIGN_IDENTITY"] = quote(settings.signingIdentity);
|
|
4524
|
+
cfg.buildSettings["PROVISIONING_PROFILE_SPECIFIER"] = quote(settings.profileSpecifier);
|
|
4525
|
+
delete cfg.buildSettings["PROVISIONING_PROFILE"];
|
|
4526
|
+
for (const key of SDK_CONDITIONAL_IDENTITY_KEYS) delete cfg.buildSettings[key];
|
|
4527
|
+
return true;
|
|
4424
4528
|
};
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4529
|
+
/**
|
|
4530
|
+
* Write `CODE_SIGN_STYLE=Manual`, `DEVELOPMENT_TEAM`, `CODE_SIGN_IDENTITY`, and
|
|
4531
|
+
* `PROVISIONING_PROFILE_SPECIFIER` into the specified XCBuildConfiguration
|
|
4532
|
+
* entries of the project under `iosDir`, then serialize back to disk.
|
|
4533
|
+
*
|
|
4534
|
+
* Only mutates the main app project — `Pods.xcodeproj` is left untouched. The
|
|
4535
|
+
* caller is responsible for ensuring each entry's `buildConfigurationUuids`
|
|
4536
|
+
* only includes configurations that belong to a signed target (see
|
|
4537
|
+
* `discoverSignedTargets`).
|
|
4538
|
+
*/
|
|
4539
|
+
const applyTargetSigning = (options) => Effect.gen(function* () {
|
|
4540
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4541
|
+
const projectDir = yield* findXcodeProjectDir$1(options.iosDir);
|
|
4542
|
+
const pbxprojPath = path.join(projectDir, "project.pbxproj");
|
|
4543
|
+
const project = yield* parseProject$1(pbxprojPath);
|
|
4544
|
+
for (const entry of options.entries) for (const configUuid of entry.buildConfigurationUuids) if (!mutateConfig(project, configUuid, entry.settings)) yield* new XcodeProjectError({ message: `Build configuration ${configUuid} not found for target "${entry.targetName}" in ${pbxprojPath}.` });
|
|
4545
|
+
const serialized = yield* Effect.try({
|
|
4546
|
+
try: () => project.writeSync(),
|
|
4547
|
+
catch: (cause) => new XcodeProjectError({ message: `Failed to serialize ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
4548
|
+
});
|
|
4549
|
+
yield* fs.writeFileString(pbxprojPath, serialized).pipe(Effect.mapError((cause) => new XcodeProjectError({ message: `Failed to write ${pbxprojPath}: ${String(cause)}` })));
|
|
4429
4550
|
});
|
|
4430
4551
|
|
|
4431
4552
|
//#endregion
|
|
4432
|
-
//#region src/lib/
|
|
4433
|
-
const
|
|
4434
|
-
const
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4553
|
+
//#region src/lib/ios-export-options.ts
|
|
4554
|
+
const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
4555
|
+
const boolTag = (value) => value ? "<true/>" : "<false/>";
|
|
4556
|
+
/**
|
|
4557
|
+
* Render an Xcode `ExportOptions.plist` for `xcodebuild -exportArchive`.
|
|
4558
|
+
*
|
|
4559
|
+
* - `signingStyle` is always `manual` (ephemeral keychain + downloaded profile)
|
|
4560
|
+
* - `uploadSymbols` is emitted only for `app-store` exports
|
|
4561
|
+
* - `provisioningProfiles` dict maps each bundleId → profile name (one entry
|
|
4562
|
+
* per signed target: main app + any extensions like notification service)
|
|
4563
|
+
* - `compileBitcode` defaults to `false`
|
|
4564
|
+
*/
|
|
4565
|
+
const renderExportOptionsPlist = ({ method, teamId, provisioningProfiles, compileBitcode = false }) => {
|
|
4566
|
+
const lines = [
|
|
4567
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
4568
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
4569
|
+
"<plist version=\"1.0\">",
|
|
4570
|
+
"<dict>",
|
|
4571
|
+
" <key>method</key>",
|
|
4572
|
+
`\t<string>${escapeXml(method)}</string>`,
|
|
4573
|
+
" <key>teamID</key>",
|
|
4574
|
+
`\t<string>${escapeXml(teamId)}</string>`,
|
|
4575
|
+
" <key>signingStyle</key>",
|
|
4576
|
+
" <string>manual</string>",
|
|
4577
|
+
" <key>compileBitcode</key>",
|
|
4578
|
+
`\t${boolTag(compileBitcode)}`,
|
|
4579
|
+
" <key>provisioningProfiles</key>",
|
|
4580
|
+
" <dict>"
|
|
4581
|
+
];
|
|
4582
|
+
for (const { bundleId, profileName } of provisioningProfiles) lines.push(`\t\t<key>${escapeXml(bundleId)}</key>`, `\t\t<string>${escapeXml(profileName)}</string>`);
|
|
4583
|
+
lines.push(" </dict>");
|
|
4584
|
+
if (method === "app-store") lines.push(" <key>uploadSymbols</key>", " <true/>");
|
|
4585
|
+
lines.push("</dict>", "</plist>", "");
|
|
4586
|
+
return lines.join("\n");
|
|
4440
4587
|
};
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4588
|
+
|
|
4589
|
+
//#endregion
|
|
4590
|
+
//#region src/lib/ios-keychain.ts
|
|
4591
|
+
const runOrFail = (cmd, step) => Command.string(cmd).pipe(Effect.mapError((cause) => new KeychainError({ message: `keychain ${step} failed: ${String(cause)}` })));
|
|
4592
|
+
const listCurrentKeychains = Effect.gen(function* () {
|
|
4593
|
+
return (yield* runOrFail(Command.make("security", "list-keychains", "-d", "user"), "list-keychains")).split("\n").map((line) => line.trim().replace(/^"/u, "").replace(/"$/u, "")).filter((line) => line.length > 0);
|
|
4594
|
+
});
|
|
4595
|
+
const parseSigningIdentity = (output) => {
|
|
4596
|
+
const lines = output.split("\n");
|
|
4597
|
+
for (const line of lines) {
|
|
4598
|
+
const match = /"([^"]+)"/u.exec(line);
|
|
4599
|
+
if (match?.[1]) return match[1];
|
|
4600
|
+
}
|
|
4446
4601
|
};
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4602
|
+
/**
|
|
4603
|
+
* Acquire an ephemeral macOS keychain, import a `.p12` into it, add it to the
|
|
4604
|
+
* user search list, and tear it all down on scope close. The keychain name is
|
|
4605
|
+
* namespaced as `better-update-<uuid>` and lives in `$tempDir`, so cleanup is
|
|
4606
|
+
* guaranteed under all termination paths.
|
|
4607
|
+
*/
|
|
4608
|
+
const acquireKeychain = ({ tempDir, p12Path, p12Password }) => {
|
|
4609
|
+
const keychainName = `better-update-${randomUUID()}.keychain-db`;
|
|
4610
|
+
const keychainPath = path.join(tempDir, keychainName);
|
|
4611
|
+
const keychainPassword = randomBytes(32).toString("hex");
|
|
4612
|
+
return Effect.acquireRelease(Effect.gen(function* () {
|
|
4613
|
+
const priorKeychains = yield* listCurrentKeychains;
|
|
4614
|
+
yield* runOrFail(Command.make("security", "create-keychain", "-p", keychainPassword, keychainPath), "create-keychain");
|
|
4615
|
+
yield* runOrFail(Command.make("security", "unlock-keychain", "-p", keychainPassword, keychainPath), "unlock-keychain");
|
|
4616
|
+
yield* runOrFail(Command.make("security", "set-keychain-settings", "-t", "3600", "-l", keychainPath), "set-keychain-settings");
|
|
4617
|
+
yield* runOrFail(Command.make("security", "import", p12Path, "-k", keychainPath, "-P", p12Password, "-T", "/usr/bin/codesign"), "import");
|
|
4618
|
+
yield* runOrFail(Command.make("security", "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", keychainPassword, keychainPath), "set-key-partition-list");
|
|
4619
|
+
yield* runOrFail(Command.make("security", "list-keychains", "-d", "user", "-s", keychainPath, ...priorKeychains), "list-keychains -s (add)");
|
|
4620
|
+
const signingIdentity = parseSigningIdentity(yield* runOrFail(Command.make("security", "find-identity", "-v", "-p", "codesigning", keychainPath), "find-identity"));
|
|
4621
|
+
if (!signingIdentity) return yield* new KeychainError({ message: "No code signing identity found after importing .p12 into ephemeral keychain." });
|
|
4622
|
+
return {
|
|
4623
|
+
handle: {
|
|
4624
|
+
keychainName,
|
|
4625
|
+
keychainPath,
|
|
4626
|
+
signingIdentity
|
|
4627
|
+
},
|
|
4628
|
+
priorKeychains
|
|
4629
|
+
};
|
|
4630
|
+
}), ({ priorKeychains }) => Effect.gen(function* () {
|
|
4631
|
+
yield* Command.string(Command.make("security", "list-keychains", "-d", "user", "-s", ...priorKeychains)).pipe(Effect.catchAll(() => Effect.void));
|
|
4632
|
+
yield* Command.string(Command.make("security", "delete-keychain", keychainPath)).pipe(Effect.catchAll(() => Effect.void));
|
|
4633
|
+
})).pipe(Effect.map(({ handle }) => handle));
|
|
4451
4634
|
};
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4635
|
+
|
|
4636
|
+
//#endregion
|
|
4637
|
+
//#region src/lib/plist.ts
|
|
4638
|
+
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
4639
|
+
/**
|
|
4640
|
+
* Parse an XML plist string into a typed object.
|
|
4641
|
+
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
4642
|
+
*/
|
|
4643
|
+
const parsePlistXml = (xml) => plist.parse(xml);
|
|
4644
|
+
/**
|
|
4645
|
+
* Parse a binary plist buffer into a typed object.
|
|
4646
|
+
* Uses bplist-parser for Apple's binary plist format.
|
|
4647
|
+
*/
|
|
4648
|
+
const parsePlistBinary = (buffer) => {
|
|
4649
|
+
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
4650
|
+
return result;
|
|
4462
4651
|
};
|
|
4463
|
-
const
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4652
|
+
const BPLIST_MAGIC = Buffer.from("bplist00");
|
|
4653
|
+
/**
|
|
4654
|
+
* Auto-detect plist format (binary vs XML) and parse accordingly.
|
|
4655
|
+
*/
|
|
4656
|
+
const parsePlist = (data) => data.subarray(0, 8).equals(BPLIST_MAGIC) ? parsePlistBinary(data) : parsePlistXml(data.toString("utf8"));
|
|
4657
|
+
|
|
4658
|
+
//#endregion
|
|
4659
|
+
//#region src/lib/ios-provisioning.ts
|
|
4660
|
+
const getString = (obj, key) => {
|
|
4661
|
+
const value = obj[key];
|
|
4662
|
+
return typeof value === "string" ? value : void 0;
|
|
4471
4663
|
};
|
|
4472
|
-
const
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4664
|
+
const getFirstArrayString = (obj, key) => {
|
|
4665
|
+
const value = obj[key];
|
|
4666
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
4667
|
+
};
|
|
4668
|
+
/**
|
|
4669
|
+
* Extract `UUID`, `Name`, and the first `TeamIdentifier` from the XML plist
|
|
4670
|
+
* output of `security cms -D -i <path>`. Returns `ProvisioningError` when any
|
|
4671
|
+
* of the three fields are missing.
|
|
4672
|
+
*/
|
|
4673
|
+
const extractProvisioningInfo = (plistXml) => Effect.gen(function* () {
|
|
4674
|
+
const parsed = yield* Effect.try({
|
|
4675
|
+
try: () => parsePlistXml(plistXml),
|
|
4676
|
+
catch: (error) => new ProvisioningError({ message: `Failed to parse provisioning profile plist: ${error instanceof Error ? error.message : String(error)}` })
|
|
4677
|
+
});
|
|
4678
|
+
const uuid = getString(parsed, "UUID");
|
|
4679
|
+
const name = getString(parsed, "Name");
|
|
4680
|
+
const teamId = getFirstArrayString(parsed, "TeamIdentifier");
|
|
4681
|
+
if (!uuid || !name || !teamId) return yield* new ProvisioningError({ message: `Failed to parse provisioning profile: missing ${uuid ? "" : "UUID "}${name ? "" : "Name "}${teamId ? "" : "TeamIdentifier "}`.trim() });
|
|
4478
4682
|
return {
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
|
|
4483
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4683
|
+
uuid,
|
|
4684
|
+
name,
|
|
4685
|
+
teamId
|
|
4484
4686
|
};
|
|
4485
|
-
};
|
|
4486
|
-
const
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4687
|
+
});
|
|
4688
|
+
const userProvisioningProfilesDir = () => path.join(os.homedir(), "Library", "MobileDevice", "Provisioning Profiles");
|
|
4689
|
+
/**
|
|
4690
|
+
* Scoped installation of a provisioning profile: parses its metadata via
|
|
4691
|
+
* `security cms -D -i`, copies it into `~/Library/MobileDevice/Provisioning Profiles`
|
|
4692
|
+
* under `<uuid>.mobileprovision`, and removes the copy on scope close — but
|
|
4693
|
+
* only if we installed it. If the target file already existed when we arrived
|
|
4694
|
+
* (e.g., Xcode had it), we leave both the file and the contents untouched.
|
|
4695
|
+
*/
|
|
4696
|
+
const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Effect.gen(function* () {
|
|
4697
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4698
|
+
const info = yield* extractProvisioningInfo(yield* Command.string(Command.make("security", "cms", "-D", "-i", profilePath)).pipe(Effect.mapError((cause) => new ProvisioningError({ message: `security cms -D failed for ${profilePath}: ${String(cause)}` }))));
|
|
4699
|
+
const targetDir = userProvisioningProfilesDir();
|
|
4700
|
+
const installedPath = path.join(targetDir, `${info.uuid}.mobileprovision`);
|
|
4701
|
+
yield* fs.makeDirectory(targetDir, { recursive: true }).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to create provisioning profiles dir: ${String(cause)}` })));
|
|
4702
|
+
if (yield* fs.exists(installedPath).pipe(Effect.orElseSucceed(() => false))) return {
|
|
4703
|
+
...info,
|
|
4704
|
+
installedPath,
|
|
4705
|
+
ownsInstallation: false
|
|
4706
|
+
};
|
|
4707
|
+
yield* fs.copyFile(profilePath, installedPath).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to copy provisioning profile into ${installedPath}: ${String(cause)}` })));
|
|
4493
4708
|
return {
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
...android.flavor === void 0 ? {} : { flavor: android.flavor },
|
|
4498
|
-
...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
|
|
4499
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4709
|
+
...info,
|
|
4710
|
+
installedPath,
|
|
4711
|
+
ownsInstallation: true
|
|
4500
4712
|
};
|
|
4501
|
-
}
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4713
|
+
}), (acquired) => Effect.gen(function* () {
|
|
4714
|
+
if (!acquired.ownsInstallation) return;
|
|
4715
|
+
yield* (yield* FileSystem.FileSystem).remove(acquired.installedPath).pipe(Effect.catchAll(() => Effect.void));
|
|
4716
|
+
})).pipe(Effect.map(({ uuid, name, teamId, installedPath }) => ({
|
|
4717
|
+
uuid,
|
|
4718
|
+
name,
|
|
4719
|
+
teamId,
|
|
4720
|
+
installedPath
|
|
4721
|
+
})));
|
|
4722
|
+
|
|
4723
|
+
//#endregion
|
|
4724
|
+
//#region src/lib/post-build-validation.ts
|
|
4725
|
+
const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Effect.gen(function* () {
|
|
4726
|
+
const bundleId = yield* readBundleId(bundleDir).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
4727
|
+
if (!bundleId) return {
|
|
4728
|
+
bundleId: void 0,
|
|
4729
|
+
warnings: [`Missing CFBundleIdentifier in Info.plist at ${bundleDir}`]
|
|
4730
|
+
};
|
|
4731
|
+
const expected = expectedByBundleId.get(bundleId);
|
|
4732
|
+
if (!expected) return {
|
|
4733
|
+
bundleId,
|
|
4734
|
+
warnings: [`Unexpected signed bundle "${bundleId}" found in archive at ${bundleDir}`]
|
|
4735
|
+
};
|
|
4736
|
+
return {
|
|
4737
|
+
bundleId,
|
|
4738
|
+
warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.catchAll(() => Effect.succeed([])))
|
|
4739
|
+
};
|
|
4740
|
+
});
|
|
4741
|
+
const validateIosBuild = (params) => Effect.gen(function* () {
|
|
4742
|
+
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
4743
|
+
if (!appDir) return {
|
|
4744
|
+
passed: false,
|
|
4745
|
+
warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
|
|
4746
|
+
};
|
|
4747
|
+
const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.catchAll(() => Effect.succeed([appDir])));
|
|
4748
|
+
const expectedByBundleId = new Map(params.expectedTargets.map((target) => [target.bundleId, target]));
|
|
4749
|
+
const perBundle = yield* Effect.forEach(bundleDirs, (bundleDir) => validateOneBundle(bundleDir, expectedByBundleId, params.expectedTeamId));
|
|
4750
|
+
const warnings = perBundle.flatMap((entry) => [...entry.warnings]);
|
|
4751
|
+
const validatedBundleIds = new Set(perBundle.map((entry) => entry.bundleId).filter((id) => id !== void 0));
|
|
4752
|
+
for (const expected of params.expectedTargets) if (!validatedBundleIds.has(expected.bundleId)) warnings.push(`Expected signed target "${expected.bundleId}" was not found in the archive.`);
|
|
4753
|
+
if (warnings.length > 0) {
|
|
4754
|
+
yield* Console.warn("Post-build validation warnings:");
|
|
4755
|
+
for (const warning of warnings) yield* Console.warn(` - ${warning}`);
|
|
4756
|
+
}
|
|
4505
4757
|
return {
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
...eas.channel === void 0 ? {} : { channel: eas.channel },
|
|
4509
|
-
...eas.env === void 0 ? {} : { env: eas.env },
|
|
4510
|
-
...ios === void 0 ? {} : { ios },
|
|
4511
|
-
...android === void 0 ? {} : { android },
|
|
4512
|
-
...eas.credentialsSource === void 0 ? {} : { credentialsSource: eas.credentialsSource }
|
|
4758
|
+
passed: warnings.length === 0,
|
|
4759
|
+
warnings
|
|
4513
4760
|
};
|
|
4514
|
-
};
|
|
4515
|
-
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
4516
|
-
return fromEasProfile(yield* resolveEasBuildProfile(yield* readEasJson(projectRoot), profileName), profileName);
|
|
4517
4761
|
});
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4520
|
-
|
|
4762
|
+
const findAppDirectory$1 = (archivePath) => Effect.gen(function* () {
|
|
4763
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4764
|
+
const productsDir = path.join(archivePath, "Products", "Applications");
|
|
4765
|
+
const appEntry = (yield* fs.readDirectory(productsDir)).find((entry) => entry.endsWith(".app"));
|
|
4766
|
+
if (!appEntry) return yield* Effect.fail("No .app found");
|
|
4767
|
+
return path.join(productsDir, appEntry);
|
|
4521
4768
|
});
|
|
4522
|
-
const readRawRuntimeVersion = (value) => {
|
|
4523
|
-
if (typeof value === "string") return value;
|
|
4524
|
-
const policy = asString$1(asRecord(value)?.["policy"]);
|
|
4525
|
-
if (policy) return { policy };
|
|
4526
|
-
};
|
|
4527
|
-
|
|
4528
|
-
//#endregion
|
|
4529
|
-
//#region src/lib/clear-cache.ts
|
|
4530
4769
|
/**
|
|
4531
|
-
*
|
|
4532
|
-
*
|
|
4533
|
-
*
|
|
4770
|
+
* Return the main `.app` plus every `.appex` extension under `<app>/PlugIns/`.
|
|
4771
|
+
* Each returned path is a signed bundle that should carry its own embedded
|
|
4772
|
+
* provisioning profile + Info.plist with CFBundleIdentifier.
|
|
4534
4773
|
*/
|
|
4535
|
-
const
|
|
4536
|
-
"android/.gradle",
|
|
4537
|
-
"android/app/build",
|
|
4538
|
-
"android/build",
|
|
4539
|
-
"ios/build",
|
|
4540
|
-
".expo",
|
|
4541
|
-
"node_modules/.cache"
|
|
4542
|
-
];
|
|
4543
|
-
const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
4774
|
+
const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
|
|
4544
4775
|
const fs = yield* FileSystem.FileSystem;
|
|
4545
|
-
const
|
|
4546
|
-
yield* Effect.
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4776
|
+
const plugInsDir = path.join(appDir, "PlugIns");
|
|
4777
|
+
if (!(yield* fs.exists(plugInsDir).pipe(Effect.catchAll(() => Effect.succeed(false))))) return [appDir];
|
|
4778
|
+
return [appDir, ...(yield* fs.readDirectory(plugInsDir)).filter((entry) => entry.endsWith(".appex")).map((entry) => path.join(plugInsDir, entry))];
|
|
4779
|
+
});
|
|
4780
|
+
const readBundleId = (bundleDir) => Effect.gen(function* () {
|
|
4781
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4782
|
+
const plistPath = path.join(bundleDir, "Info.plist");
|
|
4783
|
+
const data = yield* fs.readFile(plistPath);
|
|
4784
|
+
const bundleId = parsePlist(Buffer.from(data))["CFBundleIdentifier"];
|
|
4785
|
+
return typeof bundleId === "string" ? bundleId : void 0;
|
|
4786
|
+
});
|
|
4787
|
+
const validateEmbeddedProfile = (bundleDir, expectedUuid, expectedTeamId, bundleId) => Effect.gen(function* () {
|
|
4788
|
+
const warnings = [];
|
|
4789
|
+
const profilePath = path.join(bundleDir, "embedded.mobileprovision");
|
|
4790
|
+
const parsed = parsePlistXml(yield* Command.string(Command.make("security", "cms", "-D", "-i", profilePath)));
|
|
4791
|
+
const actualUuid = parsed["UUID"];
|
|
4792
|
+
if (typeof actualUuid === "string" && actualUuid !== expectedUuid) warnings.push(`[${bundleId}] Profile UUID mismatch: expected "${expectedUuid}", got "${actualUuid}"`);
|
|
4793
|
+
const teamIdentifiers = parsed["TeamIdentifier"];
|
|
4794
|
+
if (Array.isArray(teamIdentifiers)) {
|
|
4795
|
+
const [actualTeamId] = teamIdentifiers;
|
|
4796
|
+
if (typeof actualTeamId === "string" && actualTeamId !== expectedTeamId) warnings.push(`[${bundleId}] Team ID mismatch: expected "${expectedTeamId}", got "${actualTeamId}"`);
|
|
4797
|
+
}
|
|
4798
|
+
const expirationDate = parsed["ExpirationDate"];
|
|
4799
|
+
if (expirationDate instanceof Date && expirationDate.getTime() < Date.now()) warnings.push(`[${bundleId}] Embedded provisioning profile expired on ${expirationDate.toISOString()}`);
|
|
4800
|
+
return warnings;
|
|
4553
4801
|
});
|
|
4554
4802
|
|
|
4555
4803
|
//#endregion
|
|
4556
|
-
//#region src/lib/
|
|
4557
|
-
|
|
4804
|
+
//#region src/lib/xcode-targets.ts
|
|
4805
|
+
/** Product types whose targets require code-signing with a provisioning profile. */
|
|
4806
|
+
const SIGNED_PRODUCT_TYPES = new Set([
|
|
4807
|
+
"com.apple.product-type.application",
|
|
4808
|
+
"com.apple.product-type.app-extension",
|
|
4809
|
+
"com.apple.product-type.messages-extension",
|
|
4810
|
+
"com.apple.product-type.tv-app-extension",
|
|
4811
|
+
"com.apple.product-type.watchapp2",
|
|
4812
|
+
"com.apple.product-type.watchkit2-extension"
|
|
4813
|
+
]);
|
|
4814
|
+
const loadXcodeModule = () => __require("xcode");
|
|
4558
4815
|
/**
|
|
4559
|
-
*
|
|
4560
|
-
*
|
|
4816
|
+
* Strip surrounding quotes from a pbxproj string value. `xcode` returns values
|
|
4817
|
+
* verbatim from the project file, so identifiers like productType are usually
|
|
4818
|
+
* wrapped in double quotes (e.g. `"com.apple.product-type.application"`).
|
|
4561
4819
|
*/
|
|
4562
|
-
const
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
return
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4820
|
+
const unquote$1 = (value) => value.length >= 2 && value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value;
|
|
4821
|
+
const findXcodeProjectDir = (iosDir) => Effect.gen(function* () {
|
|
4822
|
+
const projectDir = (yield* (yield* FileSystem.FileSystem).readDirectory(iosDir).pipe(Effect.mapError((cause) => new XcodeProjectError({ message: `Failed to read ${iosDir}: ${String(cause)}` })))).find((entry) => entry.endsWith(".xcodeproj"));
|
|
4823
|
+
if (!projectDir) return yield* new XcodeProjectError({ message: `No .xcodeproj directory found under ${iosDir}. Did "expo prebuild" run?` });
|
|
4824
|
+
return path.join(iosDir, projectDir);
|
|
4825
|
+
});
|
|
4826
|
+
const parseProject = (pbxprojPath) => Effect.try({
|
|
4827
|
+
try: () => loadXcodeModule().project(pbxprojPath).parseSync(),
|
|
4828
|
+
catch: (cause) => new XcodeProjectError({ message: `Failed to parse ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
4829
|
+
});
|
|
4830
|
+
const collectConfigUuidsForTarget = (project, target, configurationName) => {
|
|
4831
|
+
const configList = project.pbxXCConfigurationList()[target.buildConfigurationList];
|
|
4832
|
+
if (!configList || typeof configList === "string") return [];
|
|
4833
|
+
const buildConfigSection = project.pbxXCBuildConfigurationSection();
|
|
4834
|
+
return configList.buildConfigurations.map((entry) => entry.value).filter((uuid) => {
|
|
4835
|
+
const cfg = buildConfigSection[uuid];
|
|
4836
|
+
if (!cfg || typeof cfg === "string") return false;
|
|
4837
|
+
return unquote$1(cfg.name) === configurationName;
|
|
4838
|
+
});
|
|
4839
|
+
};
|
|
4840
|
+
const extractBundleIdForConfig = (project, configUuid) => {
|
|
4841
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
4842
|
+
if (!cfg || typeof cfg === "string") return;
|
|
4843
|
+
const raw = cfg.buildSettings["PRODUCT_BUNDLE_IDENTIFIER"];
|
|
4844
|
+
if (typeof raw !== "string") return;
|
|
4845
|
+
return unquote$1(raw);
|
|
4846
|
+
};
|
|
4847
|
+
const collectSignedTargets = (project, pbxprojPath, configurationName) => Effect.gen(function* () {
|
|
4848
|
+
const results = [];
|
|
4849
|
+
const nativeTargets = project.pbxNativeTargetSection();
|
|
4850
|
+
for (const [uuid, entry] of Object.entries(nativeTargets)) {
|
|
4851
|
+
if (uuid.endsWith("_comment") || typeof entry === "string") continue;
|
|
4852
|
+
const productType = unquote$1(entry.productType);
|
|
4853
|
+
if (!SIGNED_PRODUCT_TYPES.has(productType)) continue;
|
|
4854
|
+
const configUuids = collectConfigUuidsForTarget(project, entry, configurationName);
|
|
4855
|
+
const [firstConfigUuid] = configUuids;
|
|
4856
|
+
if (!firstConfigUuid) return yield* new XcodeProjectError({ message: `Target "${unquote$1(entry.name)}" has no "${configurationName}" build configuration in ${pbxprojPath}.` });
|
|
4857
|
+
const bundleId = extractBundleIdForConfig(project, firstConfigUuid);
|
|
4858
|
+
if (!bundleId) return yield* new XcodeProjectError({ message: `Target "${unquote$1(entry.name)}" is missing PRODUCT_BUNDLE_IDENTIFIER in the "${configurationName}" configuration.` });
|
|
4859
|
+
results.push({
|
|
4860
|
+
targetName: unquote$1(entry.name),
|
|
4861
|
+
bundleId,
|
|
4862
|
+
productType,
|
|
4863
|
+
buildConfigurationUuids: configUuids
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
return results;
|
|
4867
|
+
});
|
|
4868
|
+
/**
|
|
4869
|
+
* Enumerate code-signed native targets (main app + extensions) declared in the
|
|
4870
|
+
* single `.xcodeproj` under `iosDir`, restricted to a given build configuration
|
|
4871
|
+
* (e.g. "Release"). Pod targets and other library product types are excluded.
|
|
4872
|
+
*
|
|
4873
|
+
* The returned `buildConfigurationUuids` list is the set of XCBuildConfiguration
|
|
4874
|
+
* UUIDs that belong to this target *and* match `configurationName` — the
|
|
4875
|
+
* per-target signing mutator writes settings into exactly those configurations.
|
|
4876
|
+
*/
|
|
4877
|
+
const discoverSignedTargets = (options) => Effect.gen(function* () {
|
|
4878
|
+
const projectDir = yield* findXcodeProjectDir(options.iosDir);
|
|
4879
|
+
const pbxprojPath = path.join(projectDir, "project.pbxproj");
|
|
4880
|
+
const results = yield* collectSignedTargets(yield* parseProject(pbxprojPath), pbxprojPath, options.configurationName);
|
|
4881
|
+
if (results.length === 0) return yield* new XcodeProjectError({ message: `No signed native targets found in ${pbxprojPath} for configuration "${options.configurationName}".` });
|
|
4882
|
+
return results;
|
|
4883
|
+
});
|
|
4570
4884
|
|
|
4571
4885
|
//#endregion
|
|
4572
|
-
//#region src/lib/
|
|
4573
|
-
const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd));
|
|
4886
|
+
//#region src/lib/xcpretty-formatter.ts
|
|
4574
4887
|
/**
|
|
4575
|
-
*
|
|
4576
|
-
*
|
|
4577
|
-
*
|
|
4578
|
-
* not a requirement.
|
|
4888
|
+
* Create a stateful xcodebuild output formatter backed by `@expo/xcpretty`.
|
|
4889
|
+
* Each `pipe(line)` call may return zero or more formatted lines — zero means
|
|
4890
|
+
* the line was suppressed (e.g., intermediate compiler invocations).
|
|
4579
4891
|
*/
|
|
4580
|
-
const
|
|
4581
|
-
const
|
|
4582
|
-
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
4583
|
-
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
4584
|
-
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
4585
|
-
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.catchAll(() => Effect.succeed("")))
|
|
4586
|
-
], { concurrency: "unbounded" });
|
|
4892
|
+
const createXcodebuildFormatter = (projectRoot) => {
|
|
4893
|
+
const formatter = ExpoRunFormatter.create(projectRoot);
|
|
4587
4894
|
return {
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
commitMessage: commitMessage.length > 0 ? commitMessage : void 0,
|
|
4591
|
-
dirty: status.trim().length > 0
|
|
4895
|
+
pipe: (line) => formatter.pipe(line),
|
|
4896
|
+
getBuildSummary: () => formatter.getBuildSummary()
|
|
4592
4897
|
};
|
|
4593
|
-
}
|
|
4898
|
+
};
|
|
4594
4899
|
|
|
4595
4900
|
//#endregion
|
|
4596
|
-
//#region src/
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
*
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
const
|
|
4901
|
+
//#region src/commands/build/ios.ts
|
|
4902
|
+
const findXcworkspace = (iosDir) => Effect.gen(function* () {
|
|
4903
|
+
const workspace = (yield* (yield* FileSystem.FileSystem).readDirectory(iosDir)).find((entry) => entry.endsWith(".xcworkspace"));
|
|
4904
|
+
if (!workspace) return yield* new BuildFailedError({
|
|
4905
|
+
step: "detect xcworkspace",
|
|
4906
|
+
exitCode: 1,
|
|
4907
|
+
message: `No .xcworkspace found under ${iosDir}. Did "pod install" run?`
|
|
4908
|
+
});
|
|
4909
|
+
return workspace;
|
|
4910
|
+
});
|
|
4911
|
+
const prebuildAndPods = (params) => Effect.gen(function* () {
|
|
4912
|
+
yield* runStep(Command.make("bunx", "expo", "prebuild", "--platform", "ios", "--clean").pipe(Command.workingDirectory(params.projectRoot), Command.env(params.commandEnv)), "expo prebuild ios");
|
|
4913
|
+
yield* runStep(Command.make("pod", "install").pipe(Command.workingDirectory(params.iosDir), Command.env(params.commandEnv)), "pod install");
|
|
4914
|
+
});
|
|
4915
|
+
const findAppDirectory = (root) => Effect.gen(function* () {
|
|
4607
4916
|
const fs = yield* FileSystem.FileSystem;
|
|
4608
|
-
const
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4917
|
+
const stack = [root];
|
|
4918
|
+
let depth = 0;
|
|
4919
|
+
while (stack.length > 0 && depth < 6) {
|
|
4920
|
+
const layer = stack.splice(0);
|
|
4921
|
+
depth += 1;
|
|
4922
|
+
for (const dir of layer) {
|
|
4923
|
+
const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
|
|
4924
|
+
for (const entry of entries) {
|
|
4925
|
+
const full = path.join(dir, entry);
|
|
4926
|
+
if (entry.endsWith(".app")) return full;
|
|
4927
|
+
const stat = yield* fs.stat(full).pipe(Effect.option);
|
|
4928
|
+
if (stat._tag === "Some" && stat.value.type === "Directory") stack.push(full);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
4622
4933
|
});
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
const
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
const
|
|
4636
|
-
const
|
|
4637
|
-
|
|
4638
|
-
|
|
4934
|
+
const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
4935
|
+
const { projectRoot, iosProfile, envVars, tempDir } = input;
|
|
4936
|
+
const runtime = yield* CliRuntime;
|
|
4937
|
+
const iosDir = path.join(projectRoot, "ios");
|
|
4938
|
+
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
4939
|
+
yield* prebuildAndPods({
|
|
4940
|
+
projectRoot,
|
|
4941
|
+
iosDir,
|
|
4942
|
+
commandEnv
|
|
4943
|
+
});
|
|
4944
|
+
const workspaceFilename = yield* findXcworkspace(iosDir);
|
|
4945
|
+
const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
|
|
4946
|
+
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
4947
|
+
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
4948
|
+
const buildCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-sdk", "iphonesimulator", "-destination", "generic/platform=iOS Simulator", "-derivedDataPath", derivedDataPath, "build", "CODE_SIGNING_ALLOWED=NO", "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY=").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
4949
|
+
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
4950
|
+
yield* formatter ? runStepFormatted(buildCmd, "xcodebuild build (simulator)", formatter) : runStep(buildCmd, "xcodebuild build (simulator)");
|
|
4951
|
+
const appDir = yield* findAppDirectory(path.join(derivedDataPath, "Build", "Products", `${configuration}-iphonesimulator`));
|
|
4952
|
+
const archiveName = `${path.basename(appDir, ".app")}-simulator.tar.gz`;
|
|
4953
|
+
const archivePath = path.join(tempDir, archiveName);
|
|
4954
|
+
yield* runStep(Command.make("tar", "-czf", archivePath, "-C", path.dirname(appDir), path.basename(appDir)).pipe(Command.env(commandEnv)), "tar simulator .app");
|
|
4955
|
+
const { sha256, byteSize } = yield* sha256File(archivePath);
|
|
4956
|
+
return {
|
|
4957
|
+
artifactPath: archivePath,
|
|
4958
|
+
byteSize,
|
|
4959
|
+
sha256
|
|
4960
|
+
};
|
|
4961
|
+
});
|
|
4962
|
+
const fetchAllCredentials = (params) => params.input.credentialsSource === "local" ? loadLocalIosCredentials({
|
|
4963
|
+
projectRoot: params.input.projectRoot,
|
|
4964
|
+
mainBundleIdentifier: params.mainBundleIdentifier
|
|
4965
|
+
}) : downloadIosCredentials(params.api, {
|
|
4966
|
+
projectId: params.input.projectId,
|
|
4967
|
+
mainBundleIdentifier: params.mainBundleIdentifier,
|
|
4968
|
+
bundleIdentifiers: params.allBundleIdentifiers,
|
|
4969
|
+
distribution: params.input.iosProfile.distribution,
|
|
4970
|
+
tempDir: params.input.tempDir
|
|
4971
|
+
});
|
|
4972
|
+
const installPerTarget = (signedTargets, credentials, credentialsSource) => Effect.gen(function* () {
|
|
4973
|
+
const profileByBundle = new Map(credentials.profiles.map((profile) => [profile.bundleIdentifier, profile]));
|
|
4974
|
+
const missing = signedTargets.filter((target) => !profileByBundle.has(target.bundleId));
|
|
4975
|
+
if (missing.length > 0) {
|
|
4976
|
+
const list = missing.map((target) => `"${target.targetName}" (${target.bundleId})`).join(", ");
|
|
4977
|
+
const hint = credentialsSource === "local" ? "Add the missing entries to credentials.json under ios.additionalProvisioningProfiles." : "Register the bundle identifier(s) in the dashboard and bind a provisioning profile.";
|
|
4978
|
+
return yield* new MissingCredentialsError({
|
|
4979
|
+
message: `Missing provisioning profile for signed target(s): ${list}.`,
|
|
4980
|
+
hint
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
return yield* Effect.forEach(signedTargets, (target) => installProfileForTarget(target, profileByBundle));
|
|
4984
|
+
});
|
|
4985
|
+
const installProfileForTarget = (target, profileByBundle) => {
|
|
4986
|
+
const profile = profileByBundle.get(target.bundleId);
|
|
4987
|
+
if (!profile) return Effect.fail(new ProvisioningError({ message: `Internal: no profile for ${target.bundleId} after pre-check.` }));
|
|
4988
|
+
return installProvisioningProfile({ profilePath: profile.profilePath }).pipe(Effect.map((installed) => ({
|
|
4989
|
+
target,
|
|
4990
|
+
profile,
|
|
4991
|
+
installed
|
|
4992
|
+
})));
|
|
4639
4993
|
};
|
|
4640
|
-
const
|
|
4641
|
-
|
|
4642
|
-
const
|
|
4643
|
-
const
|
|
4644
|
-
const
|
|
4994
|
+
const pickMainTarget = (signedTargets) => signedTargets.find((target) => target.productType === "com.apple.product-type.application") ?? signedTargets[0];
|
|
4995
|
+
const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
4996
|
+
const { api, tempDir, projectRoot, iosProfile, envVars } = input;
|
|
4997
|
+
const runtime = yield* CliRuntime;
|
|
4998
|
+
const fs = yield* FileSystem.FileSystem;
|
|
4999
|
+
const iosDir = path.join(projectRoot, "ios");
|
|
5000
|
+
const { distribution } = iosProfile;
|
|
5001
|
+
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
5002
|
+
yield* prebuildAndPods({
|
|
5003
|
+
projectRoot,
|
|
5004
|
+
iosDir,
|
|
5005
|
+
commandEnv
|
|
5006
|
+
});
|
|
5007
|
+
const workspaceFilename = yield* findXcworkspace(iosDir);
|
|
5008
|
+
const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
|
|
5009
|
+
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
5010
|
+
const signedTargets = yield* discoverSignedTargets({
|
|
5011
|
+
iosDir,
|
|
5012
|
+
configurationName: configuration
|
|
5013
|
+
});
|
|
5014
|
+
const mainTarget = pickMainTarget(signedTargets);
|
|
5015
|
+
if (!mainTarget) return yield* new BuildFailedError({
|
|
5016
|
+
step: "discover signed targets",
|
|
5017
|
+
exitCode: 1,
|
|
5018
|
+
message: `No signed iOS targets found in the Xcode project for configuration "${configuration}".`
|
|
5019
|
+
});
|
|
5020
|
+
const credentials = yield* fetchAllCredentials({
|
|
5021
|
+
api,
|
|
5022
|
+
input,
|
|
5023
|
+
mainBundleIdentifier: mainTarget.bundleId,
|
|
5024
|
+
allBundleIdentifiers: signedTargets.map((target) => target.bundleId)
|
|
5025
|
+
});
|
|
5026
|
+
const keychain = yield* acquireKeychain({
|
|
5027
|
+
tempDir,
|
|
5028
|
+
p12Path: credentials.p12Path,
|
|
5029
|
+
p12Password: credentials.p12Password
|
|
5030
|
+
});
|
|
5031
|
+
const installedTargets = yield* installPerTarget(signedTargets, credentials, input.credentialsSource);
|
|
5032
|
+
yield* applyTargetSigning({
|
|
5033
|
+
iosDir,
|
|
5034
|
+
entries: installedTargets.map(({ target, installed }) => ({
|
|
5035
|
+
targetName: target.targetName,
|
|
5036
|
+
buildConfigurationUuids: target.buildConfigurationUuids,
|
|
5037
|
+
settings: {
|
|
5038
|
+
teamId: installed.teamId,
|
|
5039
|
+
signingIdentity: keychain.signingIdentity,
|
|
5040
|
+
profileSpecifier: installed.name
|
|
5041
|
+
}
|
|
5042
|
+
}))
|
|
5043
|
+
});
|
|
5044
|
+
const archivePath = path.join(tempDir, "build.xcarchive");
|
|
5045
|
+
const archiveCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-archivePath", archivePath, "-allowProvisioningUpdates", "archive").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
5046
|
+
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
5047
|
+
yield* formatter ? runStepFormatted(archiveCmd, "xcodebuild archive", formatter) : runStep(archiveCmd, "xcodebuild archive");
|
|
5048
|
+
const exportOptionsPath = path.join(tempDir, "ExportOptions.plist");
|
|
5049
|
+
const mainInstall = installedTargets.find((entry) => entry.target.targetName === mainTarget.targetName);
|
|
5050
|
+
if (!mainInstall) return yield* new BuildFailedError({
|
|
5051
|
+
step: "resolve main target signing",
|
|
5052
|
+
exitCode: 1,
|
|
5053
|
+
message: `Internal: main target "${mainTarget.targetName}" was not in the installed list.`
|
|
5054
|
+
});
|
|
5055
|
+
const { teamId } = mainInstall.installed;
|
|
5056
|
+
yield* fs.writeFileString(exportOptionsPath, renderExportOptionsPlist({
|
|
5057
|
+
method: distribution,
|
|
5058
|
+
teamId,
|
|
5059
|
+
provisioningProfiles: installedTargets.map(({ target, installed }) => ({
|
|
5060
|
+
bundleId: target.bundleId,
|
|
5061
|
+
profileName: installed.name
|
|
5062
|
+
}))
|
|
5063
|
+
}));
|
|
5064
|
+
const exportPath = path.join(tempDir, "export");
|
|
5065
|
+
const exportCmd = Command.make("xcodebuild", "-exportArchive", "-archivePath", archivePath, "-exportPath", exportPath, "-exportOptionsPlist", exportOptionsPath, "-allowProvisioningUpdates").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
5066
|
+
yield* formatter ? runStepFormatted(exportCmd, "xcodebuild exportArchive", formatter) : runStep(exportCmd, "xcodebuild exportArchive");
|
|
5067
|
+
yield* validateIosBuild({
|
|
5068
|
+
archivePath,
|
|
5069
|
+
expectedTeamId: teamId,
|
|
5070
|
+
expectedTargets: installedTargets.map(({ target, installed }) => ({
|
|
5071
|
+
bundleId: target.bundleId,
|
|
5072
|
+
profileUuid: installed.uuid
|
|
5073
|
+
}))
|
|
5074
|
+
});
|
|
5075
|
+
const artifactPath = yield* findIosArtifact({ exportPath });
|
|
5076
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
4645
5077
|
return {
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
5078
|
+
artifactPath,
|
|
5079
|
+
byteSize,
|
|
5080
|
+
sha256
|
|
4649
5081
|
};
|
|
4650
|
-
};
|
|
4651
|
-
const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? input.slice(1, -1) : input;
|
|
4652
|
-
|
|
4653
|
-
//#endregion
|
|
4654
|
-
//#region src/lib/platform-detect.ts
|
|
4655
|
-
const PLATFORMS = ["ios", "android"];
|
|
4656
|
-
const inferPlatforms = (config) => {
|
|
4657
|
-
const fromConfig = config["platforms"];
|
|
4658
|
-
if (Array.isArray(fromConfig)) return fromConfig.filter((entry) => entry === "ios" || entry === "android");
|
|
4659
|
-
const present = [];
|
|
4660
|
-
if (config.ios !== void 0) present.push("ios");
|
|
4661
|
-
if (config.android !== void 0) present.push("android");
|
|
4662
|
-
return present;
|
|
4663
|
-
};
|
|
4664
|
-
/**
|
|
4665
|
-
* Resolve a build platform from an explicit flag, or fall back to the Expo
|
|
4666
|
-
* config (`expo.platforms` or the presence of `ios`/`android` sections). Prompts
|
|
4667
|
-
* when the config declares both platforms; fails when ambiguous and prompts are
|
|
4668
|
-
* disallowed.
|
|
4669
|
-
*/
|
|
4670
|
-
const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
4671
|
-
if (explicit !== void 0) return explicit;
|
|
4672
|
-
const candidates = inferPlatforms(config);
|
|
4673
|
-
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to your Expo config, or pass --platform." });
|
|
4674
|
-
if (candidates.length === 1) {
|
|
4675
|
-
const [only] = candidates;
|
|
4676
|
-
if (only === void 0) return yield* new BuildProfileError({ message: "Internal: empty platform candidate list." });
|
|
4677
|
-
return only;
|
|
4678
|
-
}
|
|
4679
|
-
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms detected (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
4680
|
-
return yield* promptSelect("Which platform to build?", PLATFORMS.filter((entry) => candidates.includes(entry)).map((entry) => ({
|
|
4681
|
-
value: entry,
|
|
4682
|
-
label: entry
|
|
4683
|
-
})));
|
|
4684
5082
|
});
|
|
5083
|
+
const runIosBuild = (input) => input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
4685
5084
|
|
|
4686
5085
|
//#endregion
|
|
4687
|
-
//#region src/
|
|
4688
|
-
const
|
|
4689
|
-
|
|
5086
|
+
//#region src/commands/build/reserve-and-upload.ts
|
|
5087
|
+
const buildReserveCommon = (input) => ({
|
|
5088
|
+
projectId: input.projectId,
|
|
5089
|
+
profile: input.profileName,
|
|
5090
|
+
runtimeVersion: input.runtimeVersion,
|
|
5091
|
+
bundleId: input.bundleId,
|
|
5092
|
+
sha256: input.sha256,
|
|
5093
|
+
byteSize: input.byteSize,
|
|
5094
|
+
...input.appVersion === void 0 ? {} : { appVersion: input.appVersion },
|
|
5095
|
+
...input.buildNumber === void 0 ? {} : { buildNumber: input.buildNumber },
|
|
5096
|
+
...input.gitContext.ref === void 0 ? {} : { gitRef: input.gitContext.ref },
|
|
5097
|
+
...input.gitContext.commit === void 0 ? {} : { gitCommit: input.gitContext.commit },
|
|
5098
|
+
...input.message === void 0 ? {} : { message: input.message }
|
|
5099
|
+
});
|
|
5100
|
+
const callReserve = (api, input) => {
|
|
5101
|
+
const common = buildReserveCommon(input);
|
|
5102
|
+
const { target } = input;
|
|
5103
|
+
if (target.platform === "ios") return target.distribution === "simulator" ? api.builds.reserve({ payload: {
|
|
5104
|
+
...common,
|
|
5105
|
+
platform: "ios",
|
|
5106
|
+
distribution: "simulator",
|
|
5107
|
+
artifactFormat: "tar.gz"
|
|
5108
|
+
} }) : api.builds.reserve({ payload: {
|
|
5109
|
+
...common,
|
|
5110
|
+
platform: "ios",
|
|
5111
|
+
distribution: target.distribution,
|
|
5112
|
+
artifactFormat: "ipa"
|
|
5113
|
+
} });
|
|
5114
|
+
return target.distribution === "play-store" ? api.builds.reserve({ payload: {
|
|
5115
|
+
...common,
|
|
5116
|
+
platform: "android",
|
|
5117
|
+
distribution: "play-store",
|
|
5118
|
+
artifactFormat: "aab"
|
|
5119
|
+
} }) : api.builds.reserve({ payload: {
|
|
5120
|
+
...common,
|
|
5121
|
+
platform: "android",
|
|
5122
|
+
distribution: "direct",
|
|
5123
|
+
artifactFormat: "apk"
|
|
5124
|
+
} });
|
|
5125
|
+
};
|
|
4690
5126
|
/**
|
|
4691
|
-
*
|
|
4692
|
-
*
|
|
4693
|
-
* non-interactive mode, fails with `DirtyRepoError`.
|
|
5127
|
+
* Reserve a build record on the server, upload the artifact to the returned
|
|
5128
|
+
* presigned URL, and finalize the build with its sha256 + byteSize.
|
|
4694
5129
|
*/
|
|
4695
|
-
const
|
|
4696
|
-
|
|
4697
|
-
const
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
return;
|
|
4705
|
-
}
|
|
4706
|
-
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
4707
|
-
});
|
|
4708
|
-
|
|
4709
|
-
//#endregion
|
|
4710
|
-
//#region src/lib/fingerprint.ts
|
|
4711
|
-
var FingerprintError = class extends Data.TaggedError("FingerprintError") {};
|
|
4712
|
-
const runFingerprintFull = (projectRoot) => Effect.gen(function* () {
|
|
4713
|
-
const cmd = Command.make("bunx", "@expo/fingerprint", projectRoot).pipe(Command.workingDirectory(projectRoot));
|
|
4714
|
-
const stdout = yield* Command.string(cmd).pipe(Effect.mapError((cause) => new FingerprintError({ message: `Failed to run "@expo/fingerprint": ${cause.message}` })));
|
|
4715
|
-
const parsed = yield* Effect.try({
|
|
4716
|
-
try: () => JSON.parse(stdout),
|
|
4717
|
-
catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
|
|
5130
|
+
const reserveAndUpload = (api, input) => Effect.gen(function* () {
|
|
5131
|
+
const presignedUploadClient = yield* PresignedUploadClient;
|
|
5132
|
+
const reserveResult = yield* callReserve(api, input).pipe(Effect.mapError((cause) => new ReserveError({ message: `Failed to reserve build: ${formatCause(cause)}` })));
|
|
5133
|
+
yield* presignedUploadClient.putToPresignedUrl({
|
|
5134
|
+
url: reserveResult.uploadUrl,
|
|
5135
|
+
filePath: input.artifactPath,
|
|
5136
|
+
byteSize: input.byteSize,
|
|
5137
|
+
expiresAt: reserveResult.uploadExpiresAt,
|
|
5138
|
+
headers: reserveResult.uploadHeaders
|
|
4718
5139
|
});
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
5140
|
+
const completed = yield* api.builds.complete({
|
|
5141
|
+
path: { id: reserveResult.id },
|
|
5142
|
+
payload: {
|
|
5143
|
+
sha256: input.sha256,
|
|
5144
|
+
byteSize: input.byteSize
|
|
5145
|
+
}
|
|
5146
|
+
}).pipe(Effect.mapError((cause) => new CompleteError({ message: `Failed to complete build ${reserveResult.id}: ${formatCause(cause)}` })));
|
|
5147
|
+
if (!completed.artifact) return yield* new CompleteError({ message: `Build ${completed.id} completed but server returned no artifact record.` });
|
|
4723
5148
|
return {
|
|
4724
|
-
|
|
4725
|
-
|
|
5149
|
+
id: completed.id,
|
|
5150
|
+
status: "uploaded"
|
|
4726
5151
|
};
|
|
4727
5152
|
});
|
|
4728
5153
|
|
|
4729
5154
|
//#endregion
|
|
4730
|
-
//#region src/lib/
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
if (appVersion === void 0) return yield* new RuntimeVersionError({ message: "runtimeVersion policy is \"appVersion\" but expo.version is missing in app.json." });
|
|
4737
|
-
return appVersion;
|
|
4738
|
-
}
|
|
4739
|
-
if (policy === "fingerprint") return yield* runFingerprintFull(projectRoot).pipe(Effect.map((result) => result.hash), Effect.mapError((cause) => new RuntimeVersionError({ message: cause.message })));
|
|
4740
|
-
if (policy === "nativeVersion") return yield* new RuntimeVersionError({ message: "runtimeVersion policy \"nativeVersion\" is not supported. Set a static runtimeVersion string in your Expo config." });
|
|
4741
|
-
return yield* new RuntimeVersionError({ message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".` });
|
|
5155
|
+
//#region src/lib/auto-increment.ts
|
|
5156
|
+
const bumpBuildNumber = (current) => Effect.gen(function* () {
|
|
5157
|
+
const raw = current ?? "0";
|
|
5158
|
+
const parsed = Number.parseInt(raw, 10);
|
|
5159
|
+
if (Number.isNaN(parsed)) return yield* new BuildProfileError({ message: `Cannot autoIncrement ios.buildNumber: current value "${raw}" is not a base-10 integer.` });
|
|
5160
|
+
return String(parsed + 1);
|
|
4742
5161
|
});
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
* Create a scoped temp directory prefixed with "better-update-" and `chmod 0o700`
|
|
4748
|
-
* it so only the current user can read its contents. The directory and all files
|
|
4749
|
-
* inside it are removed when the enclosing scope closes.
|
|
4750
|
-
*/
|
|
4751
|
-
const acquireBuildTempDir = Effect.gen(function* () {
|
|
4752
|
-
const fs = yield* FileSystem.FileSystem;
|
|
4753
|
-
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "better-update-" });
|
|
4754
|
-
yield* fs.chmod(dir, 448);
|
|
4755
|
-
return dir;
|
|
5162
|
+
const bumpVersionCode = (current) => Effect.gen(function* () {
|
|
5163
|
+
const value = current ?? 0;
|
|
5164
|
+
if (!Number.isInteger(value) || value < 0) return yield* new BuildProfileError({ message: `Cannot autoIncrement android.versionCode: current value ${String(value)} is not a non-negative integer.` });
|
|
5165
|
+
return value + 1;
|
|
4756
5166
|
});
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
const
|
|
4761
|
-
|
|
4762
|
-
const
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
})
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
const
|
|
4782
|
-
|
|
4783
|
-
if (
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
}
|
|
5167
|
+
const SEMVER_PATCH = /^(\d+)\.(\d+)\.(\d+)(.*)$/u;
|
|
5168
|
+
const bumpVersion = (current) => Effect.gen(function* () {
|
|
5169
|
+
if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
|
|
5170
|
+
const match = SEMVER_PATCH.exec(current);
|
|
5171
|
+
if (!match) return yield* new BuildProfileError({ message: `Cannot autoIncrement version: "${current}" is not a semver string like "1.2.3".` });
|
|
5172
|
+
const [, major, minor, patch, suffix] = match;
|
|
5173
|
+
const nextPatch = Number.parseInt(patch ?? "0", 10) + 1;
|
|
5174
|
+
return `${major ?? "0"}.${minor ?? "0"}.${String(nextPatch)}${suffix ?? ""}`;
|
|
5175
|
+
});
|
|
5176
|
+
const computeIosBumps = (config, mode) => Effect.gen(function* () {
|
|
5177
|
+
if (mode === "buildNumber") return { nextBuildNumber: yield* bumpBuildNumber(config.ios?.buildNumber) };
|
|
5178
|
+
return {
|
|
5179
|
+
nextVersion: yield* bumpVersion(config.version),
|
|
5180
|
+
nextBuildNumber: yield* bumpBuildNumber(config.ios?.buildNumber)
|
|
5181
|
+
};
|
|
5182
|
+
});
|
|
5183
|
+
const computeAndroidBumps = (config, mode) => Effect.gen(function* () {
|
|
5184
|
+
if (mode === "versionCode") return { nextVersionCode: yield* bumpVersionCode(config.android?.versionCode) };
|
|
5185
|
+
return {
|
|
5186
|
+
nextVersion: yield* bumpVersion(config.version),
|
|
5187
|
+
nextVersionCode: yield* bumpVersionCode(config.android?.versionCode)
|
|
5188
|
+
};
|
|
5189
|
+
});
|
|
5190
|
+
const buildPatch = (platform, bumps) => {
|
|
5191
|
+
const patch = {};
|
|
5192
|
+
if (bumps.nextVersion !== void 0) patch["version"] = bumps.nextVersion;
|
|
5193
|
+
if (platform === "ios" && bumps.nextBuildNumber !== void 0) patch["ios"] = { buildNumber: bumps.nextBuildNumber };
|
|
5194
|
+
if (platform === "android" && bumps.nextVersionCode !== void 0) patch["android"] = { versionCode: bumps.nextVersionCode };
|
|
5195
|
+
return patch;
|
|
5196
|
+
};
|
|
5197
|
+
const describeBumps = (platform, bumps) => {
|
|
5198
|
+
const parts = [];
|
|
5199
|
+
if (bumps.nextVersion !== void 0) parts.push(`version=${bumps.nextVersion}`);
|
|
5200
|
+
if (platform === "ios" && bumps.nextBuildNumber !== void 0) parts.push(`ios.buildNumber=${bumps.nextBuildNumber}`);
|
|
5201
|
+
if (platform === "android" && bumps.nextVersionCode !== void 0) parts.push(`android.versionCode=${String(bumps.nextVersionCode)}`);
|
|
5202
|
+
return parts.join(", ");
|
|
5203
|
+
};
|
|
5204
|
+
const computeBumps = (input) => {
|
|
5205
|
+
if (input.platform === "ios") return input.iosMode === void 0 ? Effect.succeed({}) : computeIosBumps(input.config, input.iosMode);
|
|
5206
|
+
return input.androidMode === void 0 ? Effect.succeed({}) : computeAndroidBumps(input.config, input.androidMode);
|
|
4791
5207
|
};
|
|
5208
|
+
const hasAnyBump = (bumps) => bumps.nextVersion !== void 0 || bumps.nextBuildNumber !== void 0 || bumps.nextVersionCode !== void 0;
|
|
5209
|
+
/**
|
|
5210
|
+
* Bump `version` / `ios.buildNumber` / `android.versionCode` per the resolved
|
|
5211
|
+
* autoIncrement mode, persist via `@expo/config.modifyConfigAsync`, and log a
|
|
5212
|
+
* Human-readable summary. No-op when the mode is undefined. Returns the new
|
|
5213
|
+
* Bumped values so callers can refresh their in-memory ExpoConfig.
|
|
5214
|
+
*/
|
|
5215
|
+
const applyAutoIncrement = (input) => Effect.gen(function* () {
|
|
5216
|
+
const bumps = yield* computeBumps(input);
|
|
5217
|
+
if (!hasAnyBump(bumps)) return bumps;
|
|
5218
|
+
const patch = buildPatch(input.platform, bumps);
|
|
5219
|
+
const result = yield* writeExpoConfigPatch(input.projectRoot, patch).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to persist autoIncrement: ${cause.message}` })));
|
|
5220
|
+
if (result.type === "warn" && result.configPath === null) {
|
|
5221
|
+
yield* Console.log(`autoIncrement: dynamic Expo config detected, cannot write back. Update manually: ${describeBumps(input.platform, bumps)}`);
|
|
5222
|
+
return bumps;
|
|
5223
|
+
}
|
|
5224
|
+
yield* Console.log(`autoIncrement: bumped ${describeBumps(input.platform, bumps)}`);
|
|
5225
|
+
return bumps;
|
|
5226
|
+
});
|
|
4792
5227
|
|
|
4793
5228
|
//#endregion
|
|
4794
|
-
//#region src/lib/
|
|
4795
|
-
|
|
4796
|
-
const
|
|
4797
|
-
const
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
return
|
|
5229
|
+
//#region src/lib/eas-config.ts
|
|
5230
|
+
const MAX_EXTENDS_DEPTH = 10;
|
|
5231
|
+
const asStringValue = (value) => typeof value === "string" ? value : void 0;
|
|
5232
|
+
const asBooleanValue = (value) => typeof value === "boolean" ? value : void 0;
|
|
5233
|
+
const asEnv = (value) => {
|
|
5234
|
+
const record = asRecord(value);
|
|
5235
|
+
if (!record) return;
|
|
5236
|
+
const env = {};
|
|
5237
|
+
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") env[key] = raw;
|
|
5238
|
+
return Object.keys(env).length === 0 ? void 0 : env;
|
|
4801
5239
|
};
|
|
4802
|
-
const
|
|
4803
|
-
const
|
|
4804
|
-
if (
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
5240
|
+
const asIosDistribution = (raw) => {
|
|
5241
|
+
const value = asStringValue(raw);
|
|
5242
|
+
if (value === "app-store" || value === "ad-hoc" || value === "development" || value === "enterprise") return value;
|
|
5243
|
+
};
|
|
5244
|
+
const asEnterpriseProvisioning = (raw) => {
|
|
5245
|
+
const value = asStringValue(raw);
|
|
5246
|
+
return value === "adhoc" || value === "universal" ? value : void 0;
|
|
5247
|
+
};
|
|
5248
|
+
const asAndroidBuildType = (raw) => {
|
|
5249
|
+
const value = asStringValue(raw);
|
|
5250
|
+
return value === "debug" || value === "release" ? value : void 0;
|
|
5251
|
+
};
|
|
5252
|
+
const asAndroidFormat = (raw) => {
|
|
5253
|
+
const value = asStringValue(raw);
|
|
5254
|
+
return value === "apk" || value === "aab" ? value : void 0;
|
|
5255
|
+
};
|
|
5256
|
+
const asAndroidDistribution = (raw) => {
|
|
5257
|
+
const value = asStringValue(raw);
|
|
5258
|
+
return value === "play-store" || value === "direct" ? value : void 0;
|
|
5259
|
+
};
|
|
5260
|
+
const asIosAutoIncrement = (raw) => {
|
|
5261
|
+
if (typeof raw === "boolean") return raw;
|
|
5262
|
+
const value = asStringValue(raw);
|
|
5263
|
+
return value === "buildNumber" || value === "version" ? value : void 0;
|
|
5264
|
+
};
|
|
5265
|
+
const asAndroidAutoIncrement = (raw) => {
|
|
5266
|
+
if (typeof raw === "boolean") return raw;
|
|
5267
|
+
const value = asStringValue(raw);
|
|
5268
|
+
return value === "versionCode" || value === "version" ? value : void 0;
|
|
5269
|
+
};
|
|
5270
|
+
const asAutoIncrement = (raw) => {
|
|
5271
|
+
if (typeof raw === "boolean") return raw;
|
|
5272
|
+
const value = asStringValue(raw);
|
|
5273
|
+
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
5274
|
+
};
|
|
5275
|
+
const asEasDistribution = (raw) => {
|
|
5276
|
+
const value = asStringValue(raw);
|
|
5277
|
+
return value === "internal" || value === "store" ? value : void 0;
|
|
5278
|
+
};
|
|
5279
|
+
const asCredentialsSource = (raw) => {
|
|
5280
|
+
const value = asStringValue(raw);
|
|
5281
|
+
return value === "remote" || value === "local" ? value : void 0;
|
|
5282
|
+
};
|
|
5283
|
+
const parseIosProfile = (raw) => {
|
|
5284
|
+
const record = asRecord(raw);
|
|
5285
|
+
if (!record) return;
|
|
5286
|
+
const distribution = asIosDistribution(record["distribution"]);
|
|
5287
|
+
const buildConfiguration = asStringValue(record["buildConfiguration"]);
|
|
5288
|
+
const scheme = asStringValue(record["scheme"]);
|
|
5289
|
+
const simulator = asBooleanValue(record["simulator"]);
|
|
5290
|
+
const enterpriseProvisioning = asEnterpriseProvisioning(record["enterpriseProvisioning"]);
|
|
5291
|
+
const autoIncrement = asIosAutoIncrement(record["autoIncrement"]);
|
|
5292
|
+
return {
|
|
5293
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5294
|
+
...buildConfiguration === void 0 ? {} : { buildConfiguration },
|
|
5295
|
+
...scheme === void 0 ? {} : { scheme },
|
|
5296
|
+
...simulator === void 0 ? {} : { simulator },
|
|
5297
|
+
...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning },
|
|
5298
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4809
5299
|
};
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5300
|
+
};
|
|
5301
|
+
const parseAndroidProfile = (raw) => {
|
|
5302
|
+
const record = asRecord(raw);
|
|
5303
|
+
if (!record) return;
|
|
5304
|
+
const buildType = asAndroidBuildType(record["buildType"]);
|
|
5305
|
+
const flavor = asStringValue(record["flavor"]);
|
|
5306
|
+
const gradleCommand = asStringValue(record["gradleCommand"]);
|
|
5307
|
+
const format = asAndroidFormat(record["format"]);
|
|
5308
|
+
const distribution = asAndroidDistribution(record["distribution"]);
|
|
5309
|
+
const autoIncrement = asAndroidAutoIncrement(record["autoIncrement"]);
|
|
5310
|
+
return {
|
|
5311
|
+
...buildType === void 0 ? {} : { buildType },
|
|
5312
|
+
...flavor === void 0 ? {} : { flavor },
|
|
5313
|
+
...gradleCommand === void 0 ? {} : { gradleCommand },
|
|
5314
|
+
...format === void 0 ? {} : { format },
|
|
5315
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5316
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4816
5317
|
};
|
|
4817
|
-
const signingInput = `${toBase64Url(new TextEncoder().encode(JSON.stringify(header)))}.${toBase64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
|
|
4818
|
-
const key = yield* Effect.tryPromise({
|
|
4819
|
-
try: async () => crypto.subtle.importKey("pkcs8", asArrayBuffer(der), {
|
|
4820
|
-
name: "ECDSA",
|
|
4821
|
-
namedCurve: "P-256"
|
|
4822
|
-
}, false, ["sign"]),
|
|
4823
|
-
catch: (cause) => new AppleAuthError({ cause })
|
|
4824
|
-
});
|
|
4825
|
-
const signature = yield* Effect.tryPromise({
|
|
4826
|
-
try: async () => crypto.subtle.sign({
|
|
4827
|
-
name: "ECDSA",
|
|
4828
|
-
hash: "SHA-256"
|
|
4829
|
-
}, key, new TextEncoder().encode(signingInput)),
|
|
4830
|
-
catch: (cause) => new AppleAuthError({ cause })
|
|
4831
|
-
});
|
|
4832
|
-
return `${signingInput}.${toBase64Url(new Uint8Array(signature))}`;
|
|
4833
|
-
});
|
|
4834
|
-
|
|
4835
|
-
//#endregion
|
|
4836
|
-
//#region src/lib/apple-asc-client.ts
|
|
4837
|
-
var AscApiError = class extends Data.TaggedError("AscApiError") {};
|
|
4838
|
-
var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
|
|
4839
|
-
const API_BASE = "https://api.appstoreconnect.apple.com";
|
|
4840
|
-
const extractErrors = (body) => {
|
|
4841
|
-
if (!isRecord(body) || !Array.isArray(body["errors"])) return [];
|
|
4842
|
-
return body["errors"].filter((value) => isRecord(value));
|
|
4843
5318
|
};
|
|
4844
|
-
const
|
|
4845
|
-
const
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
5319
|
+
const parseBuildProfile = (raw) => {
|
|
5320
|
+
const record = asRecord(raw);
|
|
5321
|
+
if (!record) return;
|
|
5322
|
+
const extendsName = asStringValue(record["extends"]);
|
|
5323
|
+
const developmentClient = asBooleanValue(record["developmentClient"]);
|
|
5324
|
+
const distribution = asEasDistribution(record["distribution"]);
|
|
5325
|
+
const channel = asStringValue(record["channel"]);
|
|
5326
|
+
const environment = asStringValue(record["environment"]);
|
|
5327
|
+
const env = asEnv(record["env"]);
|
|
5328
|
+
const ios = parseIosProfile(record["ios"]);
|
|
5329
|
+
const android = parseAndroidProfile(record["android"]);
|
|
5330
|
+
const credentialsSource = asCredentialsSource(record["credentialsSource"]);
|
|
5331
|
+
const autoIncrement = asAutoIncrement(record["autoIncrement"]);
|
|
5332
|
+
return {
|
|
5333
|
+
...extendsName === void 0 ? {} : { extends: extendsName },
|
|
5334
|
+
...developmentClient === void 0 ? {} : { developmentClient },
|
|
5335
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5336
|
+
...channel === void 0 ? {} : { channel },
|
|
5337
|
+
...environment === void 0 ? {} : { environment },
|
|
5338
|
+
...env === void 0 ? {} : { env },
|
|
5339
|
+
...ios === void 0 ? {} : { ios },
|
|
5340
|
+
...android === void 0 ? {} : { android },
|
|
5341
|
+
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
5342
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
5343
|
+
};
|
|
4852
5344
|
};
|
|
4853
|
-
const
|
|
4854
|
-
const
|
|
4855
|
-
try:
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
}
|
|
4866
|
-
const text = yield* Effect.tryPromise({
|
|
4867
|
-
try: async () => response.text(),
|
|
4868
|
-
catch: (cause) => new AscNetworkError({ cause })
|
|
4869
|
-
});
|
|
4870
|
-
const body = text.length === 0 ? {} : JSON.parse(text);
|
|
4871
|
-
if (!response.ok) return yield* Effect.fail(parseApiError(response, body, text));
|
|
4872
|
-
return body;
|
|
4873
|
-
});
|
|
4874
|
-
const toAscCertificate = (value) => {
|
|
4875
|
-
if (!isRecord(value)) return null;
|
|
4876
|
-
const { id, attributes } = value;
|
|
4877
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
4878
|
-
const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
|
|
4879
|
-
if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
|
|
5345
|
+
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
5346
|
+
const root = asRecord(yield* Effect.try({
|
|
5347
|
+
try: () => JSON.parse(text),
|
|
5348
|
+
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
5349
|
+
}));
|
|
5350
|
+
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
5351
|
+
const buildRecord = asRecord(root["build"]);
|
|
5352
|
+
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
5353
|
+
const profiles = {};
|
|
5354
|
+
for (const [name, value] of Object.entries(buildRecord)) {
|
|
5355
|
+
const profile = parseBuildProfile(value);
|
|
5356
|
+
if (profile) profiles[name] = profile;
|
|
5357
|
+
}
|
|
4880
5358
|
return {
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
5359
|
+
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
5360
|
+
build: profiles
|
|
5361
|
+
};
|
|
5362
|
+
});
|
|
5363
|
+
const parseCli = (raw) => {
|
|
5364
|
+
const record = asRecord(raw);
|
|
5365
|
+
if (!record) return {};
|
|
5366
|
+
const version = asStringValue(record["version"]);
|
|
5367
|
+
return version === void 0 ? {} : { version };
|
|
5368
|
+
};
|
|
5369
|
+
const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
5370
|
+
return (yield* Path.Path).join(projectRoot, "eas.json");
|
|
5371
|
+
});
|
|
5372
|
+
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
5373
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5374
|
+
const filePath = yield* easJsonPath(projectRoot);
|
|
5375
|
+
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.catchAll((cause) => Effect.fail(new BuildProfileError({ message: cause._tag === "SystemError" && cause.reason === "NotFound" ? `No eas.json found at ${filePath}. Create one with a "build" section.` : `Failed to read eas.json: ${cause.message}` })))));
|
|
5376
|
+
});
|
|
5377
|
+
const mergeIos = (base, overlay) => {
|
|
5378
|
+
if (!base) return overlay;
|
|
5379
|
+
if (!overlay) return base;
|
|
5380
|
+
return {
|
|
5381
|
+
...base,
|
|
5382
|
+
...overlay
|
|
4887
5383
|
};
|
|
4888
5384
|
};
|
|
4889
|
-
const
|
|
4890
|
-
if (!
|
|
4891
|
-
|
|
4892
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
4893
|
-
const { identifier, name } = attributes;
|
|
4894
|
-
if (typeof identifier !== "string" || typeof name !== "string") return null;
|
|
5385
|
+
const mergeAndroid = (base, overlay) => {
|
|
5386
|
+
if (!base) return overlay;
|
|
5387
|
+
if (!overlay) return base;
|
|
4895
5388
|
return {
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
name
|
|
5389
|
+
...base,
|
|
5390
|
+
...overlay
|
|
4899
5391
|
};
|
|
4900
5392
|
};
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
"IOS_APP_STORE",
|
|
4905
|
-
"IOS_APP_INHOUSE"
|
|
4906
|
-
];
|
|
4907
|
-
const asProfileType = (value) => {
|
|
4908
|
-
const match = PROFILE_TYPES.find((entry) => entry === value);
|
|
4909
|
-
return match === void 0 ? null : match;
|
|
4910
|
-
};
|
|
4911
|
-
const toAscProfile = (value) => {
|
|
4912
|
-
if (!isRecord(value)) return null;
|
|
4913
|
-
const { id, attributes } = value;
|
|
4914
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
4915
|
-
const { name, uuid, expirationDate, profileContent } = attributes;
|
|
4916
|
-
const profileType = asProfileType(attributes["profileType"]);
|
|
4917
|
-
if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
|
|
5393
|
+
const mergeEnv = (base, overlay) => {
|
|
5394
|
+
if (!base) return overlay;
|
|
5395
|
+
if (!overlay) return base;
|
|
4918
5396
|
return {
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
uuid,
|
|
4922
|
-
expirationDate,
|
|
4923
|
-
profileContent,
|
|
4924
|
-
profileType
|
|
5397
|
+
...base,
|
|
5398
|
+
...overlay
|
|
4925
5399
|
};
|
|
4926
5400
|
};
|
|
4927
|
-
const
|
|
4928
|
-
|
|
4929
|
-
const
|
|
4930
|
-
|
|
4931
|
-
const
|
|
4932
|
-
|
|
5401
|
+
const mergeProfile = (base, overlay) => {
|
|
5402
|
+
const ios = mergeIos(base.ios, overlay.ios);
|
|
5403
|
+
const android = mergeAndroid(base.android, overlay.android);
|
|
5404
|
+
const env = mergeEnv(base.env, overlay.env);
|
|
5405
|
+
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
5406
|
+
const distribution = overlay.distribution ?? base.distribution;
|
|
5407
|
+
const channel = overlay.channel ?? base.channel;
|
|
5408
|
+
const environment = overlay.environment ?? base.environment;
|
|
5409
|
+
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
5410
|
+
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
4933
5411
|
return {
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
5412
|
+
...overlay.extends === void 0 ? {} : { extends: overlay.extends },
|
|
5413
|
+
...developmentClient === void 0 ? {} : { developmentClient },
|
|
5414
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5415
|
+
...channel === void 0 ? {} : { channel },
|
|
5416
|
+
...environment === void 0 ? {} : { environment },
|
|
5417
|
+
...env === void 0 ? {} : { env },
|
|
5418
|
+
...ios === void 0 ? {} : { ios },
|
|
5419
|
+
...android === void 0 ? {} : { android },
|
|
5420
|
+
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
5421
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4937
5422
|
};
|
|
4938
5423
|
};
|
|
4939
|
-
const
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
const
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
});
|
|
4953
|
-
|
|
4954
|
-
return
|
|
5424
|
+
const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
|
|
5425
|
+
const chain = [];
|
|
5426
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5427
|
+
let current = profileName;
|
|
5428
|
+
let depth = 0;
|
|
5429
|
+
while (current !== void 0) {
|
|
5430
|
+
if (visited.has(current)) return yield* new BuildProfileError({ message: `Cycle detected in eas.json build.${profileName} extends chain at "${current}".` });
|
|
5431
|
+
visited.add(current);
|
|
5432
|
+
const profile = profiles[current];
|
|
5433
|
+
if (!profile) return yield* new BuildProfileError({ message: current === profileName ? `Build profile "${profileName}" not found in eas.json.` : `Build profile "${profileName}" extends missing profile "${current}".` });
|
|
5434
|
+
chain.unshift(profile);
|
|
5435
|
+
current = profile.extends;
|
|
5436
|
+
depth += 1;
|
|
5437
|
+
if (depth > MAX_EXTENDS_DEPTH) return yield* new BuildProfileError({ message: `Too many "extends" levels (max ${String(MAX_EXTENDS_DEPTH)}) in eas.json build.${profileName}.` });
|
|
5438
|
+
}
|
|
5439
|
+
return chain;
|
|
4955
5440
|
});
|
|
4956
|
-
const
|
|
4957
|
-
|
|
4958
|
-
}
|
|
4959
|
-
|
|
4960
|
-
const csrContent = params.csrPem.replaceAll("-----BEGIN CERTIFICATE REQUEST-----", "").replaceAll("-----END CERTIFICATE REQUEST-----", "").replaceAll(/\s+/gu, "");
|
|
4961
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/certificates", {
|
|
4962
|
-
method: "POST",
|
|
4963
|
-
body: JSON.stringify({ data: {
|
|
4964
|
-
type: "certificates",
|
|
4965
|
-
attributes: {
|
|
4966
|
-
csrContent,
|
|
4967
|
-
certificateType: params.certificateType
|
|
4968
|
-
}
|
|
4969
|
-
} })
|
|
4970
|
-
}), toAscCertificate);
|
|
4971
|
-
if (resource === null) return yield* Effect.fail(malformed("certificate"));
|
|
4972
|
-
return resource;
|
|
4973
|
-
}));
|
|
4974
|
-
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4975
|
-
yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
4976
|
-
}));
|
|
4977
|
-
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4978
|
-
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
4979
|
-
}));
|
|
4980
|
-
const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4981
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/bundleIds", {
|
|
4982
|
-
method: "POST",
|
|
4983
|
-
body: JSON.stringify({ data: {
|
|
4984
|
-
type: "bundleIds",
|
|
4985
|
-
attributes: {
|
|
4986
|
-
identifier: params.identifier,
|
|
4987
|
-
name: params.name,
|
|
4988
|
-
platform: "IOS"
|
|
4989
|
-
}
|
|
4990
|
-
} })
|
|
4991
|
-
}), toAscBundleId);
|
|
4992
|
-
if (resource === null) return yield* Effect.fail(malformed("bundleId"));
|
|
4993
|
-
return resource;
|
|
4994
|
-
}));
|
|
4995
|
-
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4996
|
-
return extractList(yield* fetchRaw(jwt, "/v1/devices?limit=200"), toAscDevice);
|
|
4997
|
-
}));
|
|
4998
|
-
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4999
|
-
const relationships = {
|
|
5000
|
-
bundleId: { data: {
|
|
5001
|
-
type: "bundleIds",
|
|
5002
|
-
id: params.bundleIdAscId
|
|
5003
|
-
} },
|
|
5004
|
-
certificates: { data: params.certificateAscIds.map((id) => ({
|
|
5005
|
-
type: "certificates",
|
|
5006
|
-
id
|
|
5007
|
-
})) },
|
|
5008
|
-
...params.deviceAscIds.length > 0 ? { devices: { data: params.deviceAscIds.map((id) => ({
|
|
5009
|
-
type: "devices",
|
|
5010
|
-
id
|
|
5011
|
-
})) } } : {}
|
|
5012
|
-
};
|
|
5013
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/profiles", {
|
|
5014
|
-
method: "POST",
|
|
5015
|
-
body: JSON.stringify({ data: {
|
|
5016
|
-
type: "profiles",
|
|
5017
|
-
attributes: {
|
|
5018
|
-
name: params.profileName,
|
|
5019
|
-
profileType: params.profileType
|
|
5020
|
-
},
|
|
5021
|
-
relationships
|
|
5022
|
-
} })
|
|
5023
|
-
}), toAscProfile);
|
|
5024
|
-
if (resource === null) return yield* Effect.fail(malformed("profile"));
|
|
5025
|
-
return resource;
|
|
5026
|
-
}));
|
|
5027
|
-
const isCertificateLimitError = (error) => {
|
|
5028
|
-
if (error._tag !== "AscApiError") return false;
|
|
5029
|
-
return /already have a current.*certificate|pending certificate request/iu.test(error.message);
|
|
5441
|
+
const stripExtends = (profile) => {
|
|
5442
|
+
if (profile.extends === void 0) return profile;
|
|
5443
|
+
const { extends: _omit, ...rest } = profile;
|
|
5444
|
+
return rest;
|
|
5030
5445
|
};
|
|
5446
|
+
const resolveEasBuildProfile = (config, profileName) => Effect.gen(function* () {
|
|
5447
|
+
const profiles = config.build;
|
|
5448
|
+
if (!profiles) return yield* new BuildProfileError({ message: "eas.json has no \"build\" section. Add at least one profile." });
|
|
5449
|
+
return stripExtends((yield* collectExtendsChain(profiles, profileName)).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
5450
|
+
});
|
|
5031
5451
|
|
|
5032
5452
|
//#endregion
|
|
5033
|
-
//#region src/lib/
|
|
5034
|
-
|
|
5035
|
-
const
|
|
5036
|
-
const
|
|
5037
|
-
|
|
5038
|
-
|
|
5453
|
+
//#region src/lib/build-profile.ts
|
|
5454
|
+
const asString$1 = (value) => typeof value === "string" ? value : void 0;
|
|
5455
|
+
const deriveIosDistribution = (eas) => {
|
|
5456
|
+
const override = eas.ios?.distribution;
|
|
5457
|
+
if (override) return override;
|
|
5458
|
+
if (eas.developmentClient === true) return "development";
|
|
5459
|
+
if (eas.distribution === "internal") return "ad-hoc";
|
|
5460
|
+
if (eas.distribution === "store") return "app-store";
|
|
5039
5461
|
};
|
|
5040
|
-
const
|
|
5041
|
-
|
|
5042
|
-
if (
|
|
5043
|
-
|
|
5044
|
-
|
|
5462
|
+
const deriveAndroidFormat = (eas) => {
|
|
5463
|
+
if (eas.android?.format) return eas.android.format;
|
|
5464
|
+
if (eas.distribution === "store") return "aab";
|
|
5465
|
+
if (eas.distribution === "internal") return "apk";
|
|
5466
|
+
if (eas.developmentClient === true) return "apk";
|
|
5045
5467
|
};
|
|
5046
|
-
const
|
|
5047
|
-
|
|
5048
|
-
if (
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5468
|
+
const deriveAndroidDistribution = (eas, format) => {
|
|
5469
|
+
if (eas.android?.distribution) return eas.android.distribution;
|
|
5470
|
+
if (format === "aab") return "play-store";
|
|
5471
|
+
return "direct";
|
|
5472
|
+
};
|
|
5473
|
+
const hasIosIntent = (eas) => eas.ios !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
|
|
5474
|
+
const hasAndroidIntent = (eas) => eas.android !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
|
|
5475
|
+
const resolveIosAutoIncrement = (eas) => {
|
|
5476
|
+
const override = eas.ios?.autoIncrement;
|
|
5477
|
+
if (override === false) return;
|
|
5478
|
+
if (override === true) return "buildNumber";
|
|
5479
|
+
if (override === "buildNumber" || override === "version") return override;
|
|
5480
|
+
const top = eas.autoIncrement;
|
|
5481
|
+
if (top === true || top === "buildNumber") return "buildNumber";
|
|
5482
|
+
if (top === "version") return "version";
|
|
5483
|
+
};
|
|
5484
|
+
const resolveAndroidAutoIncrement = (eas) => {
|
|
5485
|
+
const override = eas.android?.autoIncrement;
|
|
5486
|
+
if (override === false) return;
|
|
5487
|
+
if (override === true) return "versionCode";
|
|
5488
|
+
if (override === "versionCode" || override === "version") return override;
|
|
5489
|
+
const top = eas.autoIncrement;
|
|
5490
|
+
if (top === true || top === "versionCode") return "versionCode";
|
|
5491
|
+
if (top === "version") return "version";
|
|
5492
|
+
};
|
|
5493
|
+
const toIosProfile = (eas) => {
|
|
5494
|
+
if (!hasIosIntent(eas)) return;
|
|
5495
|
+
const distribution = deriveIosDistribution(eas);
|
|
5496
|
+
if (!distribution) return;
|
|
5497
|
+
const ios = eas.ios ?? {};
|
|
5498
|
+
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
5499
|
+
return {
|
|
5500
|
+
distribution,
|
|
5501
|
+
...ios.buildConfiguration === void 0 ? {} : { buildConfiguration: ios.buildConfiguration },
|
|
5502
|
+
...ios.scheme === void 0 ? {} : { scheme: ios.scheme },
|
|
5503
|
+
...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
|
|
5504
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
5505
|
+
};
|
|
5052
5506
|
};
|
|
5053
|
-
const
|
|
5054
|
-
|
|
5055
|
-
|
|
5507
|
+
const toAndroidProfile = (eas) => {
|
|
5508
|
+
if (!hasAndroidIntent(eas)) return;
|
|
5509
|
+
const format = deriveAndroidFormat(eas);
|
|
5510
|
+
if (!format) return;
|
|
5511
|
+
const android = eas.android ?? {};
|
|
5512
|
+
const distribution = deriveAndroidDistribution(eas, format);
|
|
5513
|
+
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
5514
|
+
return {
|
|
5515
|
+
format,
|
|
5516
|
+
distribution,
|
|
5517
|
+
...android.buildType === void 0 ? {} : { buildType: android.buildType },
|
|
5518
|
+
...android.flavor === void 0 ? {} : { flavor: android.flavor },
|
|
5519
|
+
...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
|
|
5520
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
5521
|
+
};
|
|
5056
5522
|
};
|
|
5057
|
-
const
|
|
5058
|
-
const
|
|
5059
|
-
const
|
|
5060
|
-
if (appleTeamId === null) return yield* Effect.fail(new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" }));
|
|
5523
|
+
const fromEasProfile = (eas, profileName) => {
|
|
5524
|
+
const ios = toIosProfile(eas);
|
|
5525
|
+
const android = toAndroidProfile(eas);
|
|
5061
5526
|
return {
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5527
|
+
name: profileName,
|
|
5528
|
+
environment: eas.environment ?? "production",
|
|
5529
|
+
...eas.channel === void 0 ? {} : { channel: eas.channel },
|
|
5530
|
+
...eas.env === void 0 ? {} : { env: eas.env },
|
|
5531
|
+
...ios === void 0 ? {} : { ios },
|
|
5532
|
+
...android === void 0 ? {} : { android },
|
|
5533
|
+
...eas.credentialsSource === void 0 ? {} : { credentialsSource: eas.credentialsSource }
|
|
5069
5534
|
};
|
|
5535
|
+
};
|
|
5536
|
+
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
5537
|
+
return fromEasProfile(yield* resolveEasBuildProfile(yield* readEasJson(projectRoot), profileName), profileName);
|
|
5538
|
+
});
|
|
5539
|
+
const readRuntimeVersionMeta = (config) => ({
|
|
5540
|
+
appVersion: config.version,
|
|
5541
|
+
rawRuntimeVersion: readRawRuntimeVersion(config.runtimeVersion)
|
|
5070
5542
|
});
|
|
5543
|
+
const readRawRuntimeVersion = (value) => {
|
|
5544
|
+
if (typeof value === "string") return value;
|
|
5545
|
+
const policy = asString$1(asRecord(value)?.["policy"]);
|
|
5546
|
+
if (policy) return { policy };
|
|
5547
|
+
};
|
|
5548
|
+
|
|
5549
|
+
//#endregion
|
|
5550
|
+
//#region src/lib/clear-cache.ts
|
|
5071
5551
|
/**
|
|
5072
|
-
*
|
|
5073
|
-
*
|
|
5074
|
-
*
|
|
5552
|
+
* Project-scoped build cache directories to remove when --clear-cache is passed.
|
|
5553
|
+
* Intentionally avoids `~/.gradle/caches` (global) and `ios/Pods/` (requires
|
|
5554
|
+
* pod install rebuild — leave to the user).
|
|
5075
5555
|
*/
|
|
5076
|
-
const
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
const password = generatePassword();
|
|
5095
|
-
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(params.privateKey, [cert], password, {
|
|
5096
|
-
friendlyName: "key",
|
|
5097
|
-
algorithm: "3des"
|
|
5098
|
-
});
|
|
5099
|
-
return {
|
|
5100
|
-
cert,
|
|
5101
|
-
p12Base64: forge.util.encode64(forge.asn1.toDer(p12Asn1).getBytes()),
|
|
5102
|
-
password
|
|
5103
|
-
};
|
|
5104
|
-
},
|
|
5105
|
-
catch: (error) => new CertParseError({ message: `Failed to assemble .p12: ${error instanceof Error ? error.message : String(error)}` })
|
|
5106
|
-
});
|
|
5107
|
-
const metadata = yield* extractCertMetadata(result.cert);
|
|
5108
|
-
return {
|
|
5109
|
-
p12Base64: result.p12Base64,
|
|
5110
|
-
password: result.password,
|
|
5111
|
-
metadata
|
|
5112
|
-
};
|
|
5556
|
+
const CACHE_DIRS = [
|
|
5557
|
+
"android/.gradle",
|
|
5558
|
+
"android/app/build",
|
|
5559
|
+
"android/build",
|
|
5560
|
+
"ios/build",
|
|
5561
|
+
".expo",
|
|
5562
|
+
"node_modules/.cache"
|
|
5563
|
+
];
|
|
5564
|
+
const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
5565
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5566
|
+
const removed = [];
|
|
5567
|
+
yield* Effect.forEach(CACHE_DIRS, (rel) => Effect.gen(function* () {
|
|
5568
|
+
const target = path.join(projectRoot, rel);
|
|
5569
|
+
if (!(yield* fs.exists(target).pipe(Effect.catchAll(() => Effect.succeed(false))))) return;
|
|
5570
|
+
yield* fs.remove(target, { recursive: true }).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
5571
|
+
removed.push(rel);
|
|
5572
|
+
}), { concurrency: 4 });
|
|
5573
|
+
if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
|
|
5113
5574
|
});
|
|
5114
5575
|
|
|
5115
5576
|
//#endregion
|
|
5116
|
-
//#region src/lib/
|
|
5117
|
-
const
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
});
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
csr.setSubject([{
|
|
5130
|
-
name: "commonName",
|
|
5131
|
-
shortName: "CN",
|
|
5132
|
-
value: "PEM"
|
|
5133
|
-
}]);
|
|
5134
|
-
csr.sign(keyPair.privateKey, forge.md.sha1.create());
|
|
5135
|
-
return {
|
|
5136
|
-
csrPem: forge.pki.certificationRequestToPem(csr),
|
|
5137
|
-
privateKeyPem: forge.pki.privateKeyToPem(keyPair.privateKey),
|
|
5138
|
-
privateKey: keyPair.privateKey
|
|
5139
|
-
};
|
|
5577
|
+
//#region src/lib/env-exporter.ts
|
|
5578
|
+
const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
|
|
5579
|
+
/**
|
|
5580
|
+
* Pull environment variables for a project + environment and flatten them into
|
|
5581
|
+
* a key/value map. Returns an empty map when the project has no variables.
|
|
5582
|
+
*/
|
|
5583
|
+
const pullEnvVars = (api, { projectId, environment }) => {
|
|
5584
|
+
const validated = coerceEnvironment(environment);
|
|
5585
|
+
if (!validated) return Effect.fail(new EnvExportError({ message: `Invalid environment "${environment}". Must be one of: development, preview, production.` }));
|
|
5586
|
+
return api["env-vars"].export({ urlParams: {
|
|
5587
|
+
projectId,
|
|
5588
|
+
environment: validated
|
|
5589
|
+
} }).pipe(Effect.map((result) => Object.fromEntries(result.items.map((item) => [item.key, item.value]))), Effect.mapError((cause) => new EnvExportError({ message: `Failed to export environment variables for "${environment}": ${String(cause)}` })));
|
|
5140
5590
|
};
|
|
5141
5591
|
|
|
5142
5592
|
//#endregion
|
|
5143
|
-
//#region src/lib/
|
|
5144
|
-
const
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
if (cause._tag === "AppleAuthError") return "Apple JWT signing failed";
|
|
5159
|
-
return "Network error talking to Apple";
|
|
5160
|
-
};
|
|
5161
|
-
const wrapAscError = (step) => (cause) => {
|
|
5162
|
-
if (cause._tag === "AscApiError" && isCertificateLimitError(cause)) return new CertificateLimitError({ message: cause.message });
|
|
5163
|
-
return new GenerateFailedError({
|
|
5164
|
-
step,
|
|
5165
|
-
message: messageForAscCause(cause)
|
|
5166
|
-
});
|
|
5167
|
-
};
|
|
5168
|
-
const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(function* () {
|
|
5169
|
-
const fs = yield* FileSystem.FileSystem;
|
|
5170
|
-
const tempDir = yield* acquireBuildTempDir;
|
|
5171
|
-
const keystorePath = path.join(tempDir, "release.keystore");
|
|
5172
|
-
yield* generateAndroidKeystore({
|
|
5173
|
-
outputPath: keystorePath,
|
|
5174
|
-
keyAlias: input.keyAlias,
|
|
5175
|
-
storePassword: input.storePassword,
|
|
5176
|
-
keyPassword: input.keyPassword,
|
|
5177
|
-
commonName: input.commonName,
|
|
5178
|
-
organization: input.organization,
|
|
5179
|
-
...input.validityDays === void 0 ? {} : { validityDays: input.validityDays }
|
|
5180
|
-
});
|
|
5181
|
-
const bytes = yield* fs.readFile(keystorePath);
|
|
5182
|
-
const created = yield* api.androidUploadKeystores.upload({ payload: {
|
|
5183
|
-
keystoreBase64: toBase64(bytes),
|
|
5184
|
-
keyAlias: input.keyAlias,
|
|
5185
|
-
keystorePassword: input.storePassword,
|
|
5186
|
-
keyPassword: input.keyPassword
|
|
5187
|
-
} });
|
|
5188
|
-
return {
|
|
5189
|
-
id: created.id,
|
|
5190
|
-
keyAlias: created.keyAlias
|
|
5191
|
-
};
|
|
5192
|
-
}));
|
|
5193
|
-
const fetchAscCredentials = (api, ascApiKeyId) => api.ascApiKeys.getCredentials({ path: { id: ascApiKeyId } });
|
|
5194
|
-
const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(function* () {
|
|
5195
|
-
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
5196
|
-
const ascCreds = {
|
|
5197
|
-
keyId: creds.keyId,
|
|
5198
|
-
issuerId: creds.issuerId,
|
|
5199
|
-
p8Pem: creds.p8Pem
|
|
5200
|
-
};
|
|
5201
|
-
const csrResult = yield* Effect.tryPromise({
|
|
5202
|
-
try: generateCertificateSigningRequest,
|
|
5203
|
-
catch: (cause) => new GenerateFailedError({
|
|
5204
|
-
step: "csr",
|
|
5205
|
-
message: `CSR generation failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
5206
|
-
})
|
|
5207
|
-
});
|
|
5208
|
-
const certificateType = input.certificateType ?? "IOS_DISTRIBUTION";
|
|
5209
|
-
const apple = yield* createCertificate(ascCreds, {
|
|
5210
|
-
csrPem: csrResult.csrPem,
|
|
5211
|
-
certificateType
|
|
5212
|
-
}).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
|
|
5213
|
-
if (apple.certificateContent === null) return yield* Effect.fail(new GenerateFailedError({
|
|
5214
|
-
step: "apple-create-certificate",
|
|
5215
|
-
message: "Apple response missing certificateContent"
|
|
5216
|
-
}));
|
|
5217
|
-
const bundle = yield* buildDistributionCertP12({
|
|
5218
|
-
certificateContentBase64: apple.certificateContent,
|
|
5219
|
-
privateKey: csrResult.privateKey
|
|
5220
|
-
}).pipe(Effect.mapError((cause) => new GenerateFailedError({
|
|
5221
|
-
step: "p12-build",
|
|
5222
|
-
message: cause.message
|
|
5223
|
-
})));
|
|
5224
|
-
const created = yield* api.appleDistributionCertificates.upload({ payload: {
|
|
5225
|
-
p12Base64: bundle.p12Base64,
|
|
5226
|
-
p12Password: bundle.password,
|
|
5227
|
-
serialNumber: bundle.metadata.serialNumber,
|
|
5228
|
-
appleTeamIdentifier: bundle.metadata.appleTeamId,
|
|
5229
|
-
...bundle.metadata.appleTeamName === null ? {} : { appleTeamName: bundle.metadata.appleTeamName },
|
|
5230
|
-
...bundle.metadata.developerIdIdentifier === null ? {} : { developerIdIdentifier: bundle.metadata.developerIdIdentifier },
|
|
5231
|
-
validFrom: bundle.metadata.validFrom,
|
|
5232
|
-
validUntil: bundle.metadata.validUntil
|
|
5233
|
-
} });
|
|
5593
|
+
//#region src/lib/git-context.ts
|
|
5594
|
+
const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd));
|
|
5595
|
+
/**
|
|
5596
|
+
* Best-effort git context extraction. If git is missing, the directory isn't
|
|
5597
|
+
* a repo, or any command fails, we silently return undefined fields so the
|
|
5598
|
+
* build can still proceed. This is intentional — git context is metadata,
|
|
5599
|
+
* not a requirement.
|
|
5600
|
+
*/
|
|
5601
|
+
const readGitContext = (projectRoot) => Effect.gen(function* () {
|
|
5602
|
+
const [commit, ref, commitMessage, status] = yield* Effect.all([
|
|
5603
|
+
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
5604
|
+
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
5605
|
+
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
5606
|
+
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.catchAll(() => Effect.succeed("")))
|
|
5607
|
+
], { concurrency: "unbounded" });
|
|
5234
5608
|
return {
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
developerPortalIdentifier: apple.id
|
|
5609
|
+
ref: ref.length > 0 ? ref : void 0,
|
|
5610
|
+
commit: commit.length > 0 ? commit : void 0,
|
|
5611
|
+
commitMessage: commitMessage.length > 0 ? commitMessage : void 0,
|
|
5612
|
+
dirty: status.trim().length > 0
|
|
5240
5613
|
};
|
|
5241
5614
|
});
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5615
|
+
|
|
5616
|
+
//#endregion
|
|
5617
|
+
//#region src/lib/gradle-config.ts
|
|
5618
|
+
/**
|
|
5619
|
+
* Parse Groovy `build.gradle` to extract key Android config values.
|
|
5620
|
+
* Returns `undefined` if:
|
|
5621
|
+
* - Only `build.gradle.kts` exists (Kotlin DSL not supported by gradle-to-js)
|
|
5622
|
+
* - No build.gradle found at all
|
|
5623
|
+
* - Parse fails
|
|
5624
|
+
*
|
|
5625
|
+
* Informational only — never blocks the build.
|
|
5626
|
+
*/
|
|
5627
|
+
const readGradleConfig = (androidDir) => Effect.gen(function* () {
|
|
5628
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5629
|
+
const gradlePath = path.join(androidDir, "app", "build.gradle");
|
|
5630
|
+
const ktsPath = path.join(androidDir, "app", "build.gradle.kts");
|
|
5631
|
+
const hasGroovy = yield* fs.exists(gradlePath).pipe(Effect.orElseSucceed(() => false));
|
|
5632
|
+
const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
|
|
5633
|
+
if (!hasGroovy && hasKts) return;
|
|
5634
|
+
if (!hasGroovy) return;
|
|
5635
|
+
const content = yield* fs.readFileString(gradlePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
5636
|
+
if (!content) return;
|
|
5637
|
+
return yield* Effect.tryPromise({
|
|
5638
|
+
try: async () => {
|
|
5639
|
+
return __require("gradle-to-js").parseText(stripGroovyComments(content));
|
|
5640
|
+
},
|
|
5641
|
+
catch: () => void 0
|
|
5642
|
+
}).pipe(Effect.map(extractGradleConfig), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
5249
5643
|
});
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
yield* api.appleDistributionCertificates.delete({ path: { id: input.distributionCertificateId } });
|
|
5273
|
-
deletedLocally = true;
|
|
5274
|
-
}
|
|
5644
|
+
/**
|
|
5645
|
+
* Log a warning if Gradle applicationId differs from app.json package name.
|
|
5646
|
+
*/
|
|
5647
|
+
const warnOnGradleMismatch = (gradleConfig, expectedPackage) => {
|
|
5648
|
+
if (!gradleConfig?.applicationId) return Effect.void;
|
|
5649
|
+
if (gradleConfig.applicationId === expectedPackage) return Effect.void;
|
|
5650
|
+
return Console.warn(`Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". The Gradle value will be used in the built APK/AAB.`);
|
|
5651
|
+
};
|
|
5652
|
+
/**
|
|
5653
|
+
* Strip Groovy single-line and block comments.
|
|
5654
|
+
* gradle-to-js chokes on comments — EAS CLI does this same pre-processing.
|
|
5655
|
+
*/
|
|
5656
|
+
const stripGroovyComments = (text) => text.replaceAll(/\/\/.*$/gmu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
|
|
5657
|
+
const parseVersionCode = (raw) => {
|
|
5658
|
+
if (typeof raw === "number") return raw;
|
|
5659
|
+
if (typeof raw === "string") return Number.parseInt(raw, 10) || void 0;
|
|
5660
|
+
};
|
|
5661
|
+
const extractGradleConfig = (parsed) => {
|
|
5662
|
+
const defaultConfig = asRecord(asRecord(parsed["android"])?.["defaultConfig"]);
|
|
5663
|
+
const applicationId = typeof defaultConfig?.["applicationId"] === "string" ? unquote(defaultConfig["applicationId"]) : void 0;
|
|
5664
|
+
const versionCode = parseVersionCode(defaultConfig?.["versionCode"]);
|
|
5665
|
+
const versionName = typeof defaultConfig?.["versionName"] === "string" ? unquote(defaultConfig["versionName"]) : void 0;
|
|
5275
5666
|
return {
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
deletedLocally
|
|
5667
|
+
...applicationId === void 0 ? {} : { applicationId },
|
|
5668
|
+
...versionCode === void 0 ? {} : { versionCode },
|
|
5669
|
+
...versionName === void 0 ? {} : { versionName }
|
|
5280
5670
|
};
|
|
5671
|
+
};
|
|
5672
|
+
const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? input.slice(1, -1) : input;
|
|
5673
|
+
|
|
5674
|
+
//#endregion
|
|
5675
|
+
//#region src/lib/platform-detect.ts
|
|
5676
|
+
const PLATFORMS = ["ios", "android"];
|
|
5677
|
+
const inferPlatforms = (config) => {
|
|
5678
|
+
const fromConfig = config["platforms"];
|
|
5679
|
+
if (Array.isArray(fromConfig)) return fromConfig.filter((entry) => entry === "ios" || entry === "android");
|
|
5680
|
+
const present = [];
|
|
5681
|
+
if (config.ios !== void 0) present.push("ios");
|
|
5682
|
+
if (config.android !== void 0) present.push("android");
|
|
5683
|
+
return present;
|
|
5684
|
+
};
|
|
5685
|
+
/**
|
|
5686
|
+
* Resolve a build platform from an explicit flag, or fall back to the Expo
|
|
5687
|
+
* config (`expo.platforms` or the presence of `ios`/`android` sections). Prompts
|
|
5688
|
+
* when the config declares both platforms; fails when ambiguous and prompts are
|
|
5689
|
+
* disallowed.
|
|
5690
|
+
*/
|
|
5691
|
+
const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
5692
|
+
if (explicit !== void 0) return explicit;
|
|
5693
|
+
const candidates = inferPlatforms(config);
|
|
5694
|
+
if (candidates.length === 0) return yield* new BuildProfileError({ message: "Cannot infer build platform. Add an `ios` or `android` section to your Expo config, or pass --platform." });
|
|
5695
|
+
if (candidates.length === 1) {
|
|
5696
|
+
const [only] = candidates;
|
|
5697
|
+
if (only === void 0) return yield* new BuildProfileError({ message: "Internal: empty platform candidate list." });
|
|
5698
|
+
return only;
|
|
5699
|
+
}
|
|
5700
|
+
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms detected (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
5701
|
+
return yield* promptSelect("Which platform to build?", PLATFORMS.filter((entry) => candidates.includes(entry)).map((entry) => ({
|
|
5702
|
+
value: entry,
|
|
5703
|
+
label: entry
|
|
5704
|
+
})));
|
|
5281
5705
|
});
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
}
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
}).pipe(Effect.mapError(wrapAscError("apple-create-bundle-id")))).id;
|
|
5706
|
+
|
|
5707
|
+
//#endregion
|
|
5708
|
+
//#region src/lib/repo-clean.ts
|
|
5709
|
+
const MAX_FILES_SHOWN = 10;
|
|
5710
|
+
const readPorcelain = (projectRoot) => Command.make("git", "status", "--porcelain").pipe(Command.workingDirectory(projectRoot), Command.string, Effect.map((output) => output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0)), Effect.catchAll(() => Effect.succeed([])));
|
|
5711
|
+
/**
|
|
5712
|
+
* Refuse to proceed when the working tree has uncommitted changes. Skipped when
|
|
5713
|
+
* `allowDirty` is true. In interactive mode, prompts the user to confirm; in
|
|
5714
|
+
* non-interactive mode, fails with `DirtyRepoError`.
|
|
5715
|
+
*/
|
|
5716
|
+
const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(function* () {
|
|
5717
|
+
if (allowDirty) return;
|
|
5718
|
+
const dirty = yield* readPorcelain(projectRoot);
|
|
5719
|
+
if (dirty.length === 0) return;
|
|
5720
|
+
const preview = dirty.slice(0, MAX_FILES_SHOWN).join("\n ");
|
|
5721
|
+
const overflow = dirty.length > MAX_FILES_SHOWN ? `\n ... and ${String(dirty.length - MAX_FILES_SHOWN)} more` : "";
|
|
5722
|
+
yield* Console.error(`Uncommitted changes (${String(dirty.length)} file(s)):\n ${preview}${overflow}`);
|
|
5723
|
+
if (!(yield* InteractiveMode).allow) {
|
|
5724
|
+
yield* new DirtyRepoError({ message: `Refusing to ${label} with a dirty working tree. Commit your changes or pass --allow-dirty.` });
|
|
5725
|
+
return;
|
|
5726
|
+
}
|
|
5727
|
+
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
5305
5728
|
});
|
|
5306
|
-
|
|
5307
|
-
|
|
5729
|
+
|
|
5730
|
+
//#endregion
|
|
5731
|
+
//#region src/lib/fingerprint.ts
|
|
5732
|
+
var FingerprintError = class extends Data.TaggedError("FingerprintError") {};
|
|
5733
|
+
const runFingerprintFull = (projectRoot) => Effect.gen(function* () {
|
|
5734
|
+
const cmd = Command.make("bunx", "@expo/fingerprint", projectRoot).pipe(Command.workingDirectory(projectRoot));
|
|
5735
|
+
const stdout = yield* Command.string(cmd).pipe(Effect.mapError((cause) => new FingerprintError({ message: `Failed to run "@expo/fingerprint": ${cause.message}` })));
|
|
5736
|
+
const parsed = yield* Effect.try({
|
|
5737
|
+
try: () => JSON.parse(stdout),
|
|
5738
|
+
catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
|
|
5739
|
+
});
|
|
5740
|
+
if (!isRecord(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
|
|
5741
|
+
const { hash } = parsed;
|
|
5742
|
+
if (typeof hash !== "string" || hash.length === 0) return yield* new FingerprintError({ message: "@expo/fingerprint output did not contain a \"hash\" string field." });
|
|
5743
|
+
const sourcesRaw = parsed["sources"];
|
|
5308
5744
|
return {
|
|
5309
|
-
|
|
5310
|
-
|
|
5745
|
+
hash,
|
|
5746
|
+
sources: Array.isArray(sourcesRaw) ? sourcesRaw : []
|
|
5311
5747
|
};
|
|
5312
5748
|
});
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
};
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
|
|
5328
|
-
if (useDevices && deviceAscIds.length === 0) return yield* Effect.fail(new GenerateFailedError({
|
|
5329
|
-
step: "collect-devices",
|
|
5330
|
-
message: "No registered devices to attach to the provisioning profile"
|
|
5331
|
-
}));
|
|
5332
|
-
const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
|
|
5333
|
-
profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
|
|
5334
|
-
profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
|
|
5335
|
-
bundleIdAscId,
|
|
5336
|
-
certificateAscIds: [certAscId],
|
|
5337
|
-
deviceAscIds
|
|
5338
|
-
}).pipe(Effect.mapError(wrapAscError("apple-create-profile")))).profileContent);
|
|
5339
|
-
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceAscIds) : void 0;
|
|
5340
|
-
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
5341
|
-
profileBase64: toBase64(profileBytes),
|
|
5342
|
-
appleDistributionCertificateId: input.distributionCertificateId,
|
|
5343
|
-
isManaged: true,
|
|
5344
|
-
...rosterHash === void 0 ? {} : { deviceRosterHash: rosterHash }
|
|
5345
|
-
} });
|
|
5346
|
-
return {
|
|
5347
|
-
id: created.id,
|
|
5348
|
-
bundleIdentifier: created.bundleIdentifier,
|
|
5349
|
-
distributionType: created.distributionType,
|
|
5350
|
-
profileName: created.profileName,
|
|
5351
|
-
validUntil: created.validUntil,
|
|
5352
|
-
developerPortalIdentifier: created.developerPortalIdentifier
|
|
5353
|
-
};
|
|
5749
|
+
|
|
5750
|
+
//#endregion
|
|
5751
|
+
//#region src/lib/runtime-version.ts
|
|
5752
|
+
const resolveRuntimeVersion = ({ raw, appVersion, projectRoot }) => Effect.gen(function* () {
|
|
5753
|
+
if (typeof raw === "string") return raw;
|
|
5754
|
+
if (raw === void 0) return yield* new RuntimeVersionError({ message: "No runtimeVersion configured in expo section of app.json." });
|
|
5755
|
+
const { policy } = raw;
|
|
5756
|
+
if (policy === "appVersion") {
|
|
5757
|
+
if (appVersion === void 0) return yield* new RuntimeVersionError({ message: "runtimeVersion policy is \"appVersion\" but expo.version is missing in app.json." });
|
|
5758
|
+
return appVersion;
|
|
5759
|
+
}
|
|
5760
|
+
if (policy === "fingerprint") return yield* runFingerprintFull(projectRoot).pipe(Effect.map((result) => result.hash), Effect.mapError((cause) => new RuntimeVersionError({ message: cause.message })));
|
|
5761
|
+
if (policy === "nativeVersion") return yield* new RuntimeVersionError({ message: "runtimeVersion policy \"nativeVersion\" is not supported. Set a static runtimeVersion string in your Expo config." });
|
|
5762
|
+
return yield* new RuntimeVersionError({ message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".` });
|
|
5354
5763
|
});
|
|
5355
5764
|
|
|
5356
5765
|
//#endregion
|