@better-update/cli 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +2243 -1833
- 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
|
-
import
|
|
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.0";
|
|
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,2046 +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
|
-
/**
|
|
3788
|
-
* Parse an XML plist string into a typed object.
|
|
3789
|
-
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
3790
|
-
*/
|
|
3791
|
-
const parsePlistXml = (xml) => plist.parse(xml);
|
|
3792
|
-
/**
|
|
3793
|
-
* Parse a binary plist buffer into a typed object.
|
|
3794
|
-
* Uses bplist-parser for Apple's binary plist format.
|
|
3795
|
-
*/
|
|
3796
|
-
const parsePlistBinary = (buffer) => {
|
|
3797
|
-
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
3798
|
-
return result;
|
|
3799
|
-
};
|
|
3800
|
-
const BPLIST_MAGIC = Buffer.from("bplist00");
|
|
3801
|
-
/**
|
|
3802
|
-
* Auto-detect plist format (binary vs XML) and parse accordingly.
|
|
3803
|
-
*/
|
|
3804
|
-
const parsePlist = (data) => data.subarray(0, 8).equals(BPLIST_MAGIC) ? parsePlistBinary(data) : parsePlistXml(data.toString("utf8"));
|
|
3805
|
-
|
|
3806
|
-
//#endregion
|
|
3807
|
-
//#region src/lib/ios-provisioning.ts
|
|
3808
|
-
const getString = (obj, key) => {
|
|
3809
|
-
const value = obj[key];
|
|
3810
|
-
return typeof value === "string" ? value : void 0;
|
|
3811
|
-
};
|
|
3812
|
-
const getFirstArrayString = (obj, key) => {
|
|
3813
|
-
const value = obj[key];
|
|
3814
|
-
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
3815
|
-
};
|
|
3816
|
-
/**
|
|
3817
|
-
* Extract `UUID`, `Name`, and the first `TeamIdentifier` from the XML plist
|
|
3818
|
-
* output of `security cms -D -i <path>`. Returns `ProvisioningError` when any
|
|
3819
|
-
* of the three fields are missing.
|
|
3820
|
-
*/
|
|
3821
|
-
const extractProvisioningInfo = (plistXml) => Effect.gen(function* () {
|
|
3822
|
-
const parsed = yield* Effect.try({
|
|
3823
|
-
try: () => parsePlistXml(plistXml),
|
|
3824
|
-
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
|
+
})
|
|
3825
3790
|
});
|
|
3826
|
-
const
|
|
3827
|
-
const
|
|
3828
|
-
|
|
3829
|
-
|
|
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
|
+
} });
|
|
3830
3817
|
return {
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3818
|
+
id: created.id,
|
|
3819
|
+
serialNumber: bundle.metadata.serialNumber,
|
|
3820
|
+
appleTeamId: created.appleTeamId,
|
|
3821
|
+
appleTeamIdentifier: bundle.metadata.appleTeamId,
|
|
3822
|
+
developerPortalIdentifier: apple.id
|
|
3834
3823
|
};
|
|
3835
3824
|
});
|
|
3836
|
-
const
|
|
3837
|
-
|
|
3838
|
-
*
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
const
|
|
3845
|
-
const
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
yield* fs.copyFile(profilePath, installedPath).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to copy provisioning profile into ${installedPath}: ${String(cause)}` })));
|
|
3856
|
-
return {
|
|
3857
|
-
...info,
|
|
3858
|
-
installedPath,
|
|
3859
|
-
ownsInstallation: true
|
|
3860
|
-
};
|
|
3861
|
-
}), (acquired) => Effect.gen(function* () {
|
|
3862
|
-
if (!acquired.ownsInstallation) return;
|
|
3863
|
-
yield* (yield* FileSystem.FileSystem).remove(acquired.installedPath).pipe(Effect.catchAll(() => Effect.void));
|
|
3864
|
-
})).pipe(Effect.map(({ uuid, name, teamId, installedPath }) => ({
|
|
3865
|
-
uuid,
|
|
3866
|
-
name,
|
|
3867
|
-
teamId,
|
|
3868
|
-
installedPath
|
|
3869
|
-
})));
|
|
3870
|
-
|
|
3871
|
-
//#endregion
|
|
3872
|
-
//#region src/lib/post-build-validation.ts
|
|
3873
|
-
/**
|
|
3874
|
-
* Validate an iOS build after xcodebuild completes. Checks:
|
|
3875
|
-
* 1. Bundle ID matches expected value
|
|
3876
|
-
* 2. Provisioning profile UUID matches
|
|
3877
|
-
* 3. Team ID matches
|
|
3878
|
-
*
|
|
3879
|
-
* All checks are non-blocking — returns warnings, never fails the build.
|
|
3880
|
-
*/
|
|
3881
|
-
const validateIosBuild = (params) => Effect.gen(function* () {
|
|
3882
|
-
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
3883
|
-
if (!appDir) return {
|
|
3884
|
-
passed: false,
|
|
3885
|
-
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
|
|
3886
3844
|
};
|
|
3887
|
-
const
|
|
3888
|
-
const
|
|
3889
|
-
const
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
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;
|
|
3893
3857
|
}
|
|
3894
3858
|
return {
|
|
3895
|
-
|
|
3896
|
-
|
|
3859
|
+
localId: input.distributionCertificateId,
|
|
3860
|
+
serialNumber: local.serialNumber,
|
|
3861
|
+
revokedOnApple,
|
|
3862
|
+
deletedLocally
|
|
3897
3863
|
};
|
|
3898
3864
|
});
|
|
3899
|
-
const
|
|
3900
|
-
const
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
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")));
|
|
3905
3872
|
});
|
|
3906
|
-
const
|
|
3907
|
-
const
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
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;
|
|
3912
3880
|
});
|
|
3913
|
-
const
|
|
3914
|
-
const
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
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")));
|
|
3891
|
+
return {
|
|
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
|
|
3894
|
+
};
|
|
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
|
+
};
|
|
3927
3940
|
});
|
|
3928
3941
|
|
|
3929
3942
|
//#endregion
|
|
3930
|
-
//#region src/lib/
|
|
3943
|
+
//#region src/lib/auto-provision-extension-profiles.ts
|
|
3931
3944
|
/**
|
|
3932
|
-
*
|
|
3933
|
-
*
|
|
3934
|
-
* the
|
|
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.
|
|
3935
3953
|
*/
|
|
3936
|
-
const
|
|
3937
|
-
const
|
|
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
|
|
3960
|
+
});
|
|
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
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3938
3972
|
return {
|
|
3939
|
-
|
|
3940
|
-
|
|
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
|
|
3941
3979
|
};
|
|
3942
|
-
};
|
|
3980
|
+
});
|
|
3943
3981
|
|
|
3944
3982
|
//#endregion
|
|
3945
|
-
//#region src/
|
|
3946
|
-
const
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
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
|
|
3952
4003
|
});
|
|
3953
|
-
return
|
|
3954
|
-
}
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
if (stat._tag === "Some" && stat.value.type === "Directory") stack.push(full);
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
4004
|
+
if (tag === "NotFound") return new MissingCredentialsError({
|
|
4005
|
+
message: message ?? `No ${platformLabel} build credentials configured${bundleSuffix}`,
|
|
4006
|
+
hint: bind
|
|
4007
|
+
});
|
|
4008
|
+
if (tag === "BadRequest") return new MissingCredentialsError({
|
|
4009
|
+
message: message ?? `${platformLabel} build credentials are misconfigured${bundleSuffix}`,
|
|
4010
|
+
hint: bind
|
|
4011
|
+
});
|
|
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]
|
|
3975
4023
|
}
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
projectRoot,
|
|
3985
|
-
iosDir,
|
|
3986
|
-
commandEnv
|
|
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
|
+
})
|
|
3987
4032
|
});
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
const { sha256, byteSize } = yield* sha256File(archivePath);
|
|
4000
|
-
return {
|
|
4001
|
-
artifactPath: archivePath,
|
|
4002
|
-
byteSize,
|
|
4003
|
-
sha256
|
|
4004
|
-
};
|
|
4005
|
-
});
|
|
4006
|
-
const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
4007
|
-
const { api, tempDir, projectRoot, iosProfile, bundleId, envVars, projectId } = input;
|
|
4008
|
-
const runtime = yield* CliRuntime;
|
|
4009
|
-
const iosDir = path.join(projectRoot, "ios");
|
|
4010
|
-
const { distribution } = iosProfile;
|
|
4011
|
-
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
4012
|
-
const credentials = input.credentialsSource === "local" ? yield* loadLocalIosCredentials({ projectRoot }) : yield* downloadIosCredentials(api, {
|
|
4013
|
-
projectId,
|
|
4014
|
-
bundleIdentifier: bundleId,
|
|
4015
|
-
distribution,
|
|
4016
|
-
tempDir
|
|
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
|
+
}
|
|
4017
4044
|
});
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4045
|
+
}), Effect.catchAll((cause) => {
|
|
4046
|
+
if ((hasTag$1(cause) ? cause._tag : null) === "NotFound") return Effect.succeed({
|
|
4047
|
+
status: "not-registered",
|
|
4048
|
+
bundleIdentifier: options.bundleIdentifier
|
|
4022
4049
|
});
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4050
|
+
return Effect.succeed({
|
|
4051
|
+
status: "failed",
|
|
4052
|
+
bundleIdentifier: options.bundleIdentifier,
|
|
4053
|
+
error: resolveErrorToMissingCredentials(cause, "ios", options.bundleIdentifier)
|
|
4027
4054
|
});
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
4032
|
-
const archivePath = path.join(tempDir, "build.xcarchive");
|
|
4033
|
-
const archiveCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-archivePath", archivePath, "-allowProvisioningUpdates", "archive", "CODE_SIGN_STYLE=Manual", `DEVELOPMENT_TEAM=${provisioning.teamId}`, `CODE_SIGN_IDENTITY=${keychain.signingIdentity}`, `PROVISIONING_PROFILE_SPECIFIER=${provisioning.name}`).pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
4034
|
-
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
4035
|
-
yield* formatter ? runStepFormatted(archiveCmd, "xcodebuild archive", formatter) : runStep(archiveCmd, "xcodebuild archive");
|
|
4055
|
+
}));
|
|
4056
|
+
const autoProvisionHint = "Bind an ASC API key to the main app bundle in the dashboard so missing extension bundles can be auto-provisioned, or register them manually.";
|
|
4057
|
+
const downloadIosCredentials = (api, options) => Effect.gen(function* () {
|
|
4036
4058
|
const fs = yield* FileSystem.FileSystem;
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
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
|
|
4043
4094
|
}));
|
|
4044
|
-
const
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
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 the main bundle has no ASC API key bound for auto-provisioning.`,
|
|
4111
|
+
hint: autoProvisionHint
|
|
4052
4112
|
});
|
|
4053
|
-
|
|
4054
|
-
|
|
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));
|
|
4055
4137
|
return {
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
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
|
|
4059
4163
|
};
|
|
4060
4164
|
});
|
|
4061
|
-
const runIosBuild = (input) => input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
4062
4165
|
|
|
4063
4166
|
//#endregion
|
|
4064
|
-
//#region src/
|
|
4065
|
-
const
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
sha256: input.sha256,
|
|
4071
|
-
byteSize: input.byteSize,
|
|
4072
|
-
...input.appVersion === void 0 ? {} : { appVersion: input.appVersion },
|
|
4073
|
-
...input.buildNumber === void 0 ? {} : { buildNumber: input.buildNumber },
|
|
4074
|
-
...input.gitContext.ref === void 0 ? {} : { gitRef: input.gitContext.ref },
|
|
4075
|
-
...input.gitContext.commit === void 0 ? {} : { gitCommit: input.gitContext.commit },
|
|
4076
|
-
...input.message === void 0 ? {} : { message: input.message }
|
|
4077
|
-
});
|
|
4078
|
-
const callReserve = (api, input) => {
|
|
4079
|
-
const common = buildReserveCommon(input);
|
|
4080
|
-
const { target } = input;
|
|
4081
|
-
if (target.platform === "ios") return target.distribution === "simulator" ? api.builds.reserve({ payload: {
|
|
4082
|
-
...common,
|
|
4083
|
-
platform: "ios",
|
|
4084
|
-
distribution: "simulator",
|
|
4085
|
-
artifactFormat: "tar.gz"
|
|
4086
|
-
} }) : api.builds.reserve({ payload: {
|
|
4087
|
-
...common,
|
|
4088
|
-
platform: "ios",
|
|
4089
|
-
distribution: target.distribution,
|
|
4090
|
-
artifactFormat: "ipa"
|
|
4091
|
-
} });
|
|
4092
|
-
return target.distribution === "play-store" ? api.builds.reserve({ payload: {
|
|
4093
|
-
...common,
|
|
4094
|
-
platform: "android",
|
|
4095
|
-
distribution: "play-store",
|
|
4096
|
-
artifactFormat: "aab"
|
|
4097
|
-
} }) : api.builds.reserve({ payload: {
|
|
4098
|
-
...common,
|
|
4099
|
-
platform: "android",
|
|
4100
|
-
distribution: "direct",
|
|
4101
|
-
artifactFormat: "apk"
|
|
4102
|
-
} });
|
|
4103
|
-
};
|
|
4104
|
-
/**
|
|
4105
|
-
* Reserve a build record on the server, upload the artifact to the returned
|
|
4106
|
-
* presigned URL, and finalize the build with its sha256 + byteSize.
|
|
4107
|
-
*/
|
|
4108
|
-
const reserveAndUpload = (api, input) => Effect.gen(function* () {
|
|
4109
|
-
const presignedUploadClient = yield* PresignedUploadClient;
|
|
4110
|
-
const reserveResult = yield* callReserve(api, input).pipe(Effect.mapError((cause) => new ReserveError({ message: `Failed to reserve build: ${formatCause(cause)}` })));
|
|
4111
|
-
yield* presignedUploadClient.putToPresignedUrl({
|
|
4112
|
-
url: reserveResult.uploadUrl,
|
|
4113
|
-
filePath: input.artifactPath,
|
|
4114
|
-
byteSize: input.byteSize,
|
|
4115
|
-
expiresAt: reserveResult.uploadExpiresAt,
|
|
4116
|
-
headers: reserveResult.uploadHeaders
|
|
4117
|
-
});
|
|
4118
|
-
const completed = yield* api.builds.complete({
|
|
4119
|
-
path: { id: reserveResult.id },
|
|
4120
|
-
payload: {
|
|
4121
|
-
sha256: input.sha256,
|
|
4122
|
-
byteSize: input.byteSize
|
|
4123
|
-
}
|
|
4124
|
-
}).pipe(Effect.mapError((cause) => new CompleteError({ message: `Failed to complete build ${reserveResult.id}: ${formatCause(cause)}` })));
|
|
4125
|
-
if (!completed.artifact) return yield* new CompleteError({ message: `Build ${completed.id} completed but server returned no artifact record.` });
|
|
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." });
|
|
4126
4173
|
return {
|
|
4127
|
-
|
|
4128
|
-
|
|
4174
|
+
path: yield* asString$2(record["path"], "ios.distributionCertificate.path"),
|
|
4175
|
+
password: yield* asString$2(record["password"], "ios.distributionCertificate.password")
|
|
4129
4176
|
};
|
|
4130
4177
|
});
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
});
|
|
4140
|
-
const bumpVersionCode = (current) => Effect.gen(function* () {
|
|
4141
|
-
const value = current ?? 0;
|
|
4142
|
-
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.` });
|
|
4143
|
-
return value + 1;
|
|
4144
|
-
});
|
|
4145
|
-
const SEMVER_PATCH = /^(\d+)\.(\d+)\.(\d+)(.*)$/u;
|
|
4146
|
-
const bumpVersion = (current) => Effect.gen(function* () {
|
|
4147
|
-
if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
|
|
4148
|
-
const match = SEMVER_PATCH.exec(current);
|
|
4149
|
-
if (!match) return yield* new BuildProfileError({ message: `Cannot autoIncrement version: "${current}" is not a semver string like "1.2.3".` });
|
|
4150
|
-
const [, major, minor, patch, suffix] = match;
|
|
4151
|
-
const nextPatch = Number.parseInt(patch ?? "0", 10) + 1;
|
|
4152
|
-
return `${major ?? "0"}.${minor ?? "0"}.${String(nextPatch)}${suffix ?? ""}`;
|
|
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." });
|
|
4181
|
+
return {
|
|
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")
|
|
4185
|
+
};
|
|
4153
4186
|
});
|
|
4154
|
-
const
|
|
4155
|
-
|
|
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." });
|
|
4156
4190
|
return {
|
|
4157
|
-
|
|
4158
|
-
|
|
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")
|
|
4159
4194
|
};
|
|
4160
4195
|
});
|
|
4161
|
-
const
|
|
4162
|
-
|
|
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.` });
|
|
4163
4199
|
return {
|
|
4164
|
-
|
|
4165
|
-
|
|
4200
|
+
bundleIdentifier: yield* asString$2(record["bundleIdentifier"], `ios.additionalProvisioningProfiles[${index}].bundleIdentifier`),
|
|
4201
|
+
path: yield* asString$2(record["path"], `ios.additionalProvisioningProfiles[${index}].path`)
|
|
4166
4202
|
};
|
|
4167
4203
|
});
|
|
4168
|
-
const
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
return patch;
|
|
4174
|
-
};
|
|
4175
|
-
const describeBumps = (platform, bumps) => {
|
|
4176
|
-
const parts = [];
|
|
4177
|
-
if (bumps.nextVersion !== void 0) parts.push(`version=${bumps.nextVersion}`);
|
|
4178
|
-
if (platform === "ios" && bumps.nextBuildNumber !== void 0) parts.push(`ios.buildNumber=${bumps.nextBuildNumber}`);
|
|
4179
|
-
if (platform === "android" && bumps.nextVersionCode !== void 0) parts.push(`android.versionCode=${String(bumps.nextVersionCode)}`);
|
|
4180
|
-
return parts.join(", ");
|
|
4181
|
-
};
|
|
4182
|
-
const computeBumps = (input) => {
|
|
4183
|
-
if (input.platform === "ios") return input.iosMode === void 0 ? Effect.succeed({}) : computeIosBumps(input.config, input.iosMode);
|
|
4184
|
-
return input.androidMode === void 0 ? Effect.succeed({}) : computeAndroidBumps(input.config, input.androidMode);
|
|
4185
|
-
};
|
|
4186
|
-
const hasAnyBump = (bumps) => bumps.nextVersion !== void 0 || bumps.nextBuildNumber !== void 0 || bumps.nextVersionCode !== void 0;
|
|
4187
|
-
/**
|
|
4188
|
-
* Bump `version` / `ios.buildNumber` / `android.versionCode` per the resolved
|
|
4189
|
-
* autoIncrement mode, persist via `@expo/config.modifyConfigAsync`, and log a
|
|
4190
|
-
* Human-readable summary. No-op when the mode is undefined. Returns the new
|
|
4191
|
-
* Bumped values so callers can refresh their in-memory ExpoConfig.
|
|
4192
|
-
*/
|
|
4193
|
-
const applyAutoIncrement = (input) => Effect.gen(function* () {
|
|
4194
|
-
const bumps = yield* computeBumps(input);
|
|
4195
|
-
if (!hasAnyBump(bumps)) return bumps;
|
|
4196
|
-
const patch = buildPatch(input.platform, bumps);
|
|
4197
|
-
const result = yield* writeExpoConfigPatch(input.projectRoot, patch).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to persist autoIncrement: ${cause.message}` })));
|
|
4198
|
-
if (result.type === "warn" && result.configPath === null) {
|
|
4199
|
-
yield* Console.log(`autoIncrement: dynamic Expo config detected, cannot write back. Update manually: ${describeBumps(input.platform, bumps)}`);
|
|
4200
|
-
return bumps;
|
|
4201
|
-
}
|
|
4202
|
-
yield* Console.log(`autoIncrement: bumped ${describeBumps(input.platform, bumps)}`);
|
|
4203
|
-
return bumps;
|
|
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;
|
|
4204
4209
|
});
|
|
4205
|
-
|
|
4206
|
-
//#endregion
|
|
4207
|
-
//#region src/lib/eas-config.ts
|
|
4208
|
-
const MAX_EXTENDS_DEPTH = 10;
|
|
4209
|
-
const asStringValue = (value) => typeof value === "string" ? value : void 0;
|
|
4210
|
-
const asBooleanValue = (value) => typeof value === "boolean" ? value : void 0;
|
|
4211
|
-
const asEnv = (value) => {
|
|
4212
|
-
const record = asRecord(value);
|
|
4213
|
-
if (!record) return;
|
|
4214
|
-
const env = {};
|
|
4215
|
-
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") env[key] = raw;
|
|
4216
|
-
return Object.keys(env).length === 0 ? void 0 : env;
|
|
4217
|
-
};
|
|
4218
|
-
const asIosDistribution = (raw) => {
|
|
4219
|
-
const value = asStringValue(raw);
|
|
4220
|
-
if (value === "app-store" || value === "ad-hoc" || value === "development" || value === "enterprise") return value;
|
|
4221
|
-
};
|
|
4222
|
-
const asEnterpriseProvisioning = (raw) => {
|
|
4223
|
-
const value = asStringValue(raw);
|
|
4224
|
-
return value === "adhoc" || value === "universal" ? value : void 0;
|
|
4225
|
-
};
|
|
4226
|
-
const asAndroidBuildType = (raw) => {
|
|
4227
|
-
const value = asStringValue(raw);
|
|
4228
|
-
return value === "debug" || value === "release" ? value : void 0;
|
|
4229
|
-
};
|
|
4230
|
-
const asAndroidFormat = (raw) => {
|
|
4231
|
-
const value = asStringValue(raw);
|
|
4232
|
-
return value === "apk" || value === "aab" ? value : void 0;
|
|
4233
|
-
};
|
|
4234
|
-
const asAndroidDistribution = (raw) => {
|
|
4235
|
-
const value = asStringValue(raw);
|
|
4236
|
-
return value === "play-store" || value === "direct" ? value : void 0;
|
|
4237
|
-
};
|
|
4238
|
-
const asIosAutoIncrement = (raw) => {
|
|
4239
|
-
if (typeof raw === "boolean") return raw;
|
|
4240
|
-
const value = asStringValue(raw);
|
|
4241
|
-
return value === "buildNumber" || value === "version" ? value : void 0;
|
|
4242
|
-
};
|
|
4243
|
-
const asAndroidAutoIncrement = (raw) => {
|
|
4244
|
-
if (typeof raw === "boolean") return raw;
|
|
4245
|
-
const value = asStringValue(raw);
|
|
4246
|
-
return value === "versionCode" || value === "version" ? value : void 0;
|
|
4247
|
-
};
|
|
4248
|
-
const asAutoIncrement = (raw) => {
|
|
4249
|
-
if (typeof raw === "boolean") return raw;
|
|
4250
|
-
const value = asStringValue(raw);
|
|
4251
|
-
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
4252
|
-
};
|
|
4253
|
-
const asEasDistribution = (raw) => {
|
|
4254
|
-
const value = asStringValue(raw);
|
|
4255
|
-
return value === "internal" || value === "store" ? value : void 0;
|
|
4256
|
-
};
|
|
4257
|
-
const asCredentialsSource = (raw) => {
|
|
4258
|
-
const value = asStringValue(raw);
|
|
4259
|
-
return value === "remote" || value === "local" ? value : void 0;
|
|
4260
|
-
};
|
|
4261
|
-
const parseIosProfile = (raw) => {
|
|
4210
|
+
const parseIos = (raw) => Effect.gen(function* () {
|
|
4262
4211
|
const record = asRecord(raw);
|
|
4263
|
-
if (!record) return;
|
|
4264
|
-
const
|
|
4265
|
-
const
|
|
4266
|
-
const
|
|
4267
|
-
const
|
|
4268
|
-
const
|
|
4269
|
-
const autoIncrement = asIosAutoIncrement(record["autoIncrement"]);
|
|
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"]);
|
|
4270
4218
|
return {
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
...
|
|
4274
|
-
...
|
|
4275
|
-
...
|
|
4276
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4219
|
+
provisioningProfilePath,
|
|
4220
|
+
distributionCertificate,
|
|
4221
|
+
...additionalProvisioningProfiles === void 0 ? {} : { additionalProvisioningProfiles },
|
|
4222
|
+
...pushKey === void 0 ? {} : { pushKey },
|
|
4223
|
+
...ascApiKey === void 0 ? {} : { ascApiKey }
|
|
4277
4224
|
};
|
|
4278
|
-
};
|
|
4279
|
-
const
|
|
4225
|
+
});
|
|
4226
|
+
const parseAndroidKeystore = (raw) => Effect.gen(function* () {
|
|
4280
4227
|
const record = asRecord(raw);
|
|
4281
|
-
if (!record) return;
|
|
4282
|
-
const buildType = asAndroidBuildType(record["buildType"]);
|
|
4283
|
-
const flavor = asStringValue(record["flavor"]);
|
|
4284
|
-
const gradleCommand = asStringValue(record["gradleCommand"]);
|
|
4285
|
-
const format = asAndroidFormat(record["format"]);
|
|
4286
|
-
const distribution = asAndroidDistribution(record["distribution"]);
|
|
4287
|
-
const autoIncrement = asAndroidAutoIncrement(record["autoIncrement"]);
|
|
4228
|
+
if (!record) return yield* new CredentialsJsonError({ message: "credentials.json: android.keystore must be an object." });
|
|
4288
4229
|
return {
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
...distribution === void 0 ? {} : { distribution },
|
|
4294
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
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")
|
|
4295
4234
|
};
|
|
4296
|
-
};
|
|
4297
|
-
const
|
|
4235
|
+
});
|
|
4236
|
+
const parseGoogleServiceAccountKey = (raw) => Effect.gen(function* () {
|
|
4298
4237
|
const record = asRecord(raw);
|
|
4299
|
-
if (!record) return;
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
const
|
|
4304
|
-
|
|
4305
|
-
const
|
|
4306
|
-
const
|
|
4307
|
-
const android = parseAndroidProfile(record["android"]);
|
|
4308
|
-
const credentialsSource = asCredentialsSource(record["credentialsSource"]);
|
|
4309
|
-
const autoIncrement = asAutoIncrement(record["autoIncrement"]);
|
|
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"]);
|
|
4310
4246
|
return {
|
|
4311
|
-
|
|
4312
|
-
...
|
|
4313
|
-
...distribution === void 0 ? {} : { distribution },
|
|
4314
|
-
...channel === void 0 ? {} : { channel },
|
|
4315
|
-
...environment === void 0 ? {} : { environment },
|
|
4316
|
-
...env === void 0 ? {} : { env },
|
|
4317
|
-
...ios === void 0 ? {} : { ios },
|
|
4318
|
-
...android === void 0 ? {} : { android },
|
|
4319
|
-
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
4320
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4247
|
+
keystore,
|
|
4248
|
+
...googleServiceAccountKey === void 0 ? {} : { googleServiceAccountKey }
|
|
4321
4249
|
};
|
|
4322
|
-
};
|
|
4323
|
-
const
|
|
4250
|
+
});
|
|
4251
|
+
const parseCredentialsJson = (raw) => Effect.gen(function* () {
|
|
4324
4252
|
const root = asRecord(yield* Effect.try({
|
|
4325
|
-
try: () => JSON.parse(
|
|
4326
|
-
catch: (
|
|
4253
|
+
try: () => JSON.parse(raw),
|
|
4254
|
+
catch: () => new CredentialsJsonError({ message: "credentials.json is not valid JSON." })
|
|
4327
4255
|
}));
|
|
4328
|
-
if (!root) return yield* new
|
|
4329
|
-
const
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
for (const [name, value] of Object.entries(buildRecord)) {
|
|
4333
|
-
const profile = parseBuildProfile(value);
|
|
4334
|
-
if (profile) profiles[name] = profile;
|
|
4335
|
-
}
|
|
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\"." });
|
|
4336
4260
|
return {
|
|
4337
|
-
...
|
|
4338
|
-
|
|
4261
|
+
...ios === void 0 ? {} : { ios },
|
|
4262
|
+
...android === void 0 ? {} : { android }
|
|
4339
4263
|
};
|
|
4340
4264
|
});
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
const
|
|
4345
|
-
|
|
4346
|
-
};
|
|
4347
|
-
const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
4348
|
-
return (yield* Path.Path).join(projectRoot, "eas.json");
|
|
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)}` }))));
|
|
4349
4271
|
});
|
|
4350
|
-
const
|
|
4272
|
+
const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
|
|
4351
4273
|
const fs = yield* FileSystem.FileSystem;
|
|
4352
|
-
const filePath =
|
|
4353
|
-
|
|
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;
|
|
4354
4278
|
});
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
if (!
|
|
4373
|
-
|
|
4279
|
+
/**
|
|
4280
|
+
* Resolve a path that may be either absolute or relative to the project root.
|
|
4281
|
+
*/
|
|
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
|
+
});
|
|
4317
|
+
}
|
|
4374
4318
|
return {
|
|
4375
|
-
|
|
4376
|
-
|
|
4319
|
+
p12Path,
|
|
4320
|
+
p12Password: data.ios.distributionCertificate.password,
|
|
4321
|
+
profiles
|
|
4377
4322
|
};
|
|
4378
|
-
};
|
|
4379
|
-
const
|
|
4380
|
-
const
|
|
4381
|
-
const
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
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");
|
|
4389
4336
|
return {
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
...environment === void 0 ? {} : { environment },
|
|
4395
|
-
...env === void 0 ? {} : { env },
|
|
4396
|
-
...ios === void 0 ? {} : { ios },
|
|
4397
|
-
...android === void 0 ? {} : { android },
|
|
4398
|
-
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
4399
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4337
|
+
keystorePath,
|
|
4338
|
+
storePassword: data.android.keystore.keystorePassword,
|
|
4339
|
+
keyAlias: data.android.keystore.keyAlias,
|
|
4340
|
+
keyPassword: data.android.keystore.keyPassword
|
|
4400
4341
|
};
|
|
4401
|
-
};
|
|
4402
|
-
const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
|
|
4403
|
-
const chain = [];
|
|
4404
|
-
const visited = /* @__PURE__ */ new Set();
|
|
4405
|
-
let current = profileName;
|
|
4406
|
-
let depth = 0;
|
|
4407
|
-
while (current !== void 0) {
|
|
4408
|
-
if (visited.has(current)) return yield* new BuildProfileError({ message: `Cycle detected in eas.json build.${profileName} extends chain at "${current}".` });
|
|
4409
|
-
visited.add(current);
|
|
4410
|
-
const profile = profiles[current];
|
|
4411
|
-
if (!profile) return yield* new BuildProfileError({ message: current === profileName ? `Build profile "${profileName}" not found in eas.json.` : `Build profile "${profileName}" extends missing profile "${current}".` });
|
|
4412
|
-
chain.unshift(profile);
|
|
4413
|
-
current = profile.extends;
|
|
4414
|
-
depth += 1;
|
|
4415
|
-
if (depth > MAX_EXTENDS_DEPTH) return yield* new BuildProfileError({ message: `Too many "extends" levels (max ${String(MAX_EXTENDS_DEPTH)}) in eas.json build.${profileName}.` });
|
|
4416
|
-
}
|
|
4417
|
-
return chain;
|
|
4418
|
-
});
|
|
4419
|
-
const stripExtends = (profile) => {
|
|
4420
|
-
if (profile.extends === void 0) return profile;
|
|
4421
|
-
const { extends: _omit, ...rest } = profile;
|
|
4422
|
-
return rest;
|
|
4423
|
-
};
|
|
4424
|
-
const resolveEasBuildProfile = (config, profileName) => Effect.gen(function* () {
|
|
4425
|
-
const profiles = config.build;
|
|
4426
|
-
if (!profiles) return yield* new BuildProfileError({ message: "eas.json has no \"build\" section. Add at least one profile." });
|
|
4427
|
-
return stripExtends((yield* collectExtendsChain(profiles, profileName)).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
4428
4342
|
});
|
|
4429
4343
|
|
|
4430
4344
|
//#endregion
|
|
4431
|
-
//#region src/lib/
|
|
4432
|
-
const
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
};
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
const
|
|
4474
|
-
|
|
4475
|
-
const ios = eas.ios ?? {};
|
|
4476
|
-
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
4477
|
-
return {
|
|
4478
|
-
distribution,
|
|
4479
|
-
...ios.buildConfiguration === void 0 ? {} : { buildConfiguration: ios.buildConfiguration },
|
|
4480
|
-
...ios.scheme === void 0 ? {} : { scheme: ios.scheme },
|
|
4481
|
-
...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
|
|
4482
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4483
|
-
};
|
|
4484
|
-
};
|
|
4485
|
-
const toAndroidProfile = (eas) => {
|
|
4486
|
-
if (!hasAndroidIntent(eas)) return;
|
|
4487
|
-
const format = deriveAndroidFormat(eas);
|
|
4488
|
-
if (!format) return;
|
|
4489
|
-
const android = eas.android ?? {};
|
|
4490
|
-
const distribution = deriveAndroidDistribution(eas, format);
|
|
4491
|
-
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
4492
|
-
return {
|
|
4493
|
-
format,
|
|
4494
|
-
distribution,
|
|
4495
|
-
...android.buildType === void 0 ? {} : { buildType: android.buildType },
|
|
4496
|
-
...android.flavor === void 0 ? {} : { flavor: android.flavor },
|
|
4497
|
-
...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
|
|
4498
|
-
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
4499
|
-
};
|
|
4500
|
-
};
|
|
4501
|
-
const fromEasProfile = (eas, profileName) => {
|
|
4502
|
-
const ios = toIosProfile(eas);
|
|
4503
|
-
const android = toAndroidProfile(eas);
|
|
4504
|
-
return {
|
|
4505
|
-
name: profileName,
|
|
4506
|
-
environment: eas.environment ?? "production",
|
|
4507
|
-
...eas.channel === void 0 ? {} : { channel: eas.channel },
|
|
4508
|
-
...eas.env === void 0 ? {} : { env: eas.env },
|
|
4509
|
-
...ios === void 0 ? {} : { ios },
|
|
4510
|
-
...android === void 0 ? {} : { android },
|
|
4511
|
-
...eas.credentialsSource === void 0 ? {} : { credentialsSource: eas.credentialsSource }
|
|
4512
|
-
};
|
|
4513
|
-
};
|
|
4514
|
-
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
4515
|
-
return fromEasProfile(yield* resolveEasBuildProfile(yield* readEasJson(projectRoot), profileName), profileName);
|
|
4516
|
-
});
|
|
4517
|
-
const readRuntimeVersionMeta = (config) => ({
|
|
4518
|
-
appVersion: config.version,
|
|
4519
|
-
rawRuntimeVersion: readRawRuntimeVersion(config.runtimeVersion)
|
|
4520
|
-
});
|
|
4521
|
-
const readRawRuntimeVersion = (value) => {
|
|
4522
|
-
if (typeof value === "string") return value;
|
|
4523
|
-
const policy = asString$1(asRecord(value)?.["policy"]);
|
|
4524
|
-
if (policy) return { policy };
|
|
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());
|
|
4525
4389
|
};
|
|
4526
4390
|
|
|
4527
4391
|
//#endregion
|
|
4528
|
-
//#region src/
|
|
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
|
+
}))));
|
|
4529
4402
|
/**
|
|
4530
|
-
*
|
|
4531
|
-
*
|
|
4532
|
-
* pod install rebuild — leave to the user).
|
|
4403
|
+
* Run a build step with stdout piped through a formatter (e.g., xcpretty).
|
|
4404
|
+
* stderr passes through to the terminal directly.
|
|
4533
4405
|
*/
|
|
4534
|
-
const
|
|
4535
|
-
"
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
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
|
+
});
|
|
4441
|
+
}
|
|
4552
4442
|
});
|
|
4553
4443
|
|
|
4554
4444
|
//#endregion
|
|
4555
|
-
//#region src/
|
|
4556
|
-
const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
|
|
4445
|
+
//#region src/commands/build/android.ts
|
|
4557
4446
|
/**
|
|
4558
|
-
*
|
|
4559
|
-
*
|
|
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`
|
|
4560
4454
|
*/
|
|
4561
|
-
const
|
|
4562
|
-
const
|
|
4563
|
-
|
|
4564
|
-
return api["env-vars"].export({ urlParams: {
|
|
4565
|
-
projectId,
|
|
4566
|
-
environment: validated
|
|
4567
|
-
} }).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)}` })));
|
|
4455
|
+
const gradleTaskName = (format, flavor, buildType) => {
|
|
4456
|
+
const verb = format === "aab" ? "bundle" : "assemble";
|
|
4457
|
+
return flavor ? `${verb}${capitalize(flavor)}${capitalize(buildType)}` : `${verb}${capitalize(buildType)}`;
|
|
4568
4458
|
};
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
const
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
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");
|
|
4474
|
+
const fs = yield* FileSystem.FileSystem;
|
|
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);
|
|
4586
4492
|
return {
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
dirty: status.trim().length > 0
|
|
4493
|
+
artifactPath,
|
|
4494
|
+
byteSize,
|
|
4495
|
+
sha256
|
|
4591
4496
|
};
|
|
4592
4497
|
});
|
|
4593
4498
|
|
|
4594
4499
|
//#endregion
|
|
4595
|
-
//#region src/lib/
|
|
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
|
+
});
|
|
4596
4511
|
/**
|
|
4597
|
-
*
|
|
4598
|
-
*
|
|
4599
|
-
*
|
|
4600
|
-
|
|
4601
|
-
|
|
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;
|
|
4528
|
+
};
|
|
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.
|
|
4602
4533
|
*
|
|
4603
|
-
*
|
|
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`).
|
|
4604
4538
|
*/
|
|
4605
|
-
const
|
|
4539
|
+
const applyTargetSigning = (options) => Effect.gen(function* () {
|
|
4606
4540
|
const fs = yield* FileSystem.FileSystem;
|
|
4607
|
-
const
|
|
4608
|
-
const
|
|
4609
|
-
const
|
|
4610
|
-
const
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
try: async () => {
|
|
4617
|
-
return __require("gradle-to-js").parseText(stripGroovyComments(content));
|
|
4618
|
-
},
|
|
4619
|
-
catch: () => void 0
|
|
4620
|
-
}).pipe(Effect.map(extractGradleConfig), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
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)}` })));
|
|
4621
4550
|
});
|
|
4551
|
+
|
|
4552
|
+
//#endregion
|
|
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/>";
|
|
4622
4556
|
/**
|
|
4623
|
-
*
|
|
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`
|
|
4624
4564
|
*/
|
|
4625
|
-
const
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
}
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
const
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
...versionName === void 0 ? {} : { versionName }
|
|
4648
|
-
};
|
|
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");
|
|
4649
4587
|
};
|
|
4650
|
-
const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? input.slice(1, -1) : input;
|
|
4651
4588
|
|
|
4652
4589
|
//#endregion
|
|
4653
|
-
//#region src/lib/
|
|
4654
|
-
const
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
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
|
+
}
|
|
4662
4601
|
};
|
|
4663
4602
|
/**
|
|
4664
|
-
*
|
|
4665
|
-
*
|
|
4666
|
-
*
|
|
4667
|
-
*
|
|
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.
|
|
4668
4607
|
*/
|
|
4669
|
-
const
|
|
4670
|
-
|
|
4671
|
-
const
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
const
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
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));
|
|
4634
|
+
};
|
|
4684
4635
|
|
|
4685
4636
|
//#endregion
|
|
4686
|
-
//#region src/lib/
|
|
4687
|
-
const
|
|
4688
|
-
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([])));
|
|
4637
|
+
//#region src/lib/plist.ts
|
|
4638
|
+
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
4689
4639
|
/**
|
|
4690
|
-
*
|
|
4691
|
-
*
|
|
4692
|
-
* non-interactive mode, fails with `DirtyRepoError`.
|
|
4640
|
+
* Parse an XML plist string into a typed object.
|
|
4641
|
+
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
4693
4642
|
*/
|
|
4694
|
-
const
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
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;
|
|
4651
|
+
};
|
|
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"));
|
|
4707
4657
|
|
|
4708
4658
|
//#endregion
|
|
4709
|
-
//#region src/lib/
|
|
4710
|
-
|
|
4711
|
-
const
|
|
4712
|
-
|
|
4713
|
-
|
|
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;
|
|
4663
|
+
};
|
|
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* () {
|
|
4714
4674
|
const parsed = yield* Effect.try({
|
|
4715
|
-
try: () =>
|
|
4716
|
-
catch: () => new
|
|
4675
|
+
try: () => parsePlistXml(plistXml),
|
|
4676
|
+
catch: (error) => new ProvisioningError({ message: `Failed to parse provisioning profile plist: ${error instanceof Error ? error.message : String(error)}` })
|
|
4717
4677
|
});
|
|
4718
|
-
|
|
4719
|
-
const
|
|
4720
|
-
|
|
4721
|
-
|
|
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() });
|
|
4722
4682
|
return {
|
|
4723
|
-
|
|
4724
|
-
|
|
4683
|
+
uuid,
|
|
4684
|
+
name,
|
|
4685
|
+
teamId
|
|
4725
4686
|
};
|
|
4726
4687
|
});
|
|
4727
|
-
|
|
4728
|
-
//#endregion
|
|
4729
|
-
//#region src/lib/runtime-version.ts
|
|
4730
|
-
const resolveRuntimeVersion = ({ raw, appVersion, projectRoot }) => Effect.gen(function* () {
|
|
4731
|
-
if (typeof raw === "string") return raw;
|
|
4732
|
-
if (raw === void 0) return yield* new RuntimeVersionError({ message: "No runtimeVersion configured in expo section of app.json." });
|
|
4733
|
-
const { policy } = raw;
|
|
4734
|
-
if (policy === "appVersion") {
|
|
4735
|
-
if (appVersion === void 0) return yield* new RuntimeVersionError({ message: "runtimeVersion policy is \"appVersion\" but expo.version is missing in app.json." });
|
|
4736
|
-
return appVersion;
|
|
4737
|
-
}
|
|
4738
|
-
if (policy === "fingerprint") return yield* runFingerprintFull(projectRoot).pipe(Effect.map((result) => result.hash), Effect.mapError((cause) => new RuntimeVersionError({ message: cause.message })));
|
|
4739
|
-
if (policy === "nativeVersion") return yield* new RuntimeVersionError({ message: "runtimeVersion policy \"nativeVersion\" is not supported. Set a static runtimeVersion string in your Expo config." });
|
|
4740
|
-
return yield* new RuntimeVersionError({ message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".` });
|
|
4741
|
-
});
|
|
4742
|
-
|
|
4743
|
-
//#endregion
|
|
4744
|
-
//#region src/lib/temp-dir.ts
|
|
4688
|
+
const userProvisioningProfilesDir = () => path.join(os.homedir(), "Library", "MobileDevice", "Provisioning Profiles");
|
|
4745
4689
|
/**
|
|
4746
|
-
*
|
|
4747
|
-
*
|
|
4748
|
-
*
|
|
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.
|
|
4749
4695
|
*/
|
|
4750
|
-
const
|
|
4696
|
+
const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Effect.gen(function* () {
|
|
4751
4697
|
const fs = yield* FileSystem.FileSystem;
|
|
4752
|
-
const
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
});
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
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)}` })));
|
|
4708
|
+
return {
|
|
4709
|
+
...info,
|
|
4710
|
+
installedPath,
|
|
4711
|
+
ownsInstallation: true
|
|
4712
|
+
};
|
|
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
|
+
})));
|
|
4773
4722
|
|
|
4774
4723
|
//#endregion
|
|
4775
|
-
//#region src/lib/
|
|
4776
|
-
const
|
|
4777
|
-
const
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
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}`);
|
|
4789
4756
|
}
|
|
4790
|
-
|
|
4757
|
+
return {
|
|
4758
|
+
passed: warnings.length === 0,
|
|
4759
|
+
warnings
|
|
4760
|
+
};
|
|
4761
|
+
});
|
|
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);
|
|
4768
|
+
});
|
|
4769
|
+
/**
|
|
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.
|
|
4773
|
+
*/
|
|
4774
|
+
const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
|
|
4775
|
+
const fs = yield* FileSystem.FileSystem;
|
|
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;
|
|
4801
|
+
});
|
|
4791
4802
|
|
|
4792
4803
|
//#endregion
|
|
4793
|
-
//#region src/lib/
|
|
4794
|
-
|
|
4795
|
-
const
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
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");
|
|
4815
|
+
/**
|
|
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"`).
|
|
4819
|
+
*/
|
|
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;
|
|
4832
4883
|
});
|
|
4833
4884
|
|
|
4834
4885
|
//#endregion
|
|
4835
|
-
//#region src/lib/
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4886
|
+
//#region src/lib/xcpretty-formatter.ts
|
|
4887
|
+
/**
|
|
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).
|
|
4891
|
+
*/
|
|
4892
|
+
const createXcodebuildFormatter = (projectRoot) => {
|
|
4893
|
+
const formatter = ExpoRunFormatter.create(projectRoot);
|
|
4894
|
+
return {
|
|
4895
|
+
pipe: (line) => formatter.pipe(line),
|
|
4896
|
+
getBuildSummary: () => formatter.getBuildSummary()
|
|
4897
|
+
};
|
|
4842
4898
|
};
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4899
|
+
|
|
4900
|
+
//#endregion
|
|
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?`
|
|
4850
4908
|
});
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
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* () {
|
|
4916
|
+
const fs = yield* FileSystem.FileSystem;
|
|
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);
|
|
4861
4929
|
}
|
|
4862
|
-
}
|
|
4863
|
-
|
|
4864
|
-
});
|
|
4865
|
-
const text = yield* Effect.tryPromise({
|
|
4866
|
-
try: async () => response.text(),
|
|
4867
|
-
catch: (cause) => new AscNetworkError({ cause })
|
|
4868
|
-
});
|
|
4869
|
-
const body = text.length === 0 ? {} : JSON.parse(text);
|
|
4870
|
-
if (!response.ok) return yield* Effect.fail(parseApiError(response, body, text));
|
|
4871
|
-
return body;
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
4872
4933
|
});
|
|
4873
|
-
const
|
|
4874
|
-
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
const
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
const
|
|
4889
|
-
|
|
4890
|
-
const
|
|
4891
|
-
|
|
4892
|
-
const
|
|
4893
|
-
|
|
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);
|
|
4894
4956
|
return {
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4957
|
+
artifactPath: archivePath,
|
|
4958
|
+
byteSize,
|
|
4959
|
+
sha256
|
|
4898
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
|
+
})));
|
|
4899
4993
|
};
|
|
4900
|
-
const
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
const
|
|
4907
|
-
const
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
const
|
|
4915
|
-
const
|
|
4916
|
-
|
|
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);
|
|
5077
|
+
return {
|
|
5078
|
+
artifactPath,
|
|
5079
|
+
byteSize,
|
|
5080
|
+
sha256
|
|
5081
|
+
};
|
|
5082
|
+
});
|
|
5083
|
+
const runIosBuild = (input) => input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
5084
|
+
|
|
5085
|
+
//#endregion
|
|
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
|
+
};
|
|
5126
|
+
/**
|
|
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.
|
|
5129
|
+
*/
|
|
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
|
|
5139
|
+
});
|
|
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.` });
|
|
5148
|
+
return {
|
|
5149
|
+
id: completed.id,
|
|
5150
|
+
status: "uploaded"
|
|
5151
|
+
};
|
|
5152
|
+
});
|
|
5153
|
+
|
|
5154
|
+
//#endregion
|
|
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);
|
|
5161
|
+
});
|
|
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;
|
|
5166
|
+
});
|
|
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);
|
|
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
|
+
});
|
|
5227
|
+
|
|
5228
|
+
//#endregion
|
|
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;
|
|
5239
|
+
};
|
|
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 }
|
|
5299
|
+
};
|
|
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 }
|
|
5317
|
+
};
|
|
5318
|
+
};
|
|
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
|
+
};
|
|
5344
|
+
};
|
|
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
|
+
}
|
|
4917
5358
|
return {
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
uuid,
|
|
4921
|
-
expirationDate,
|
|
4922
|
-
profileContent,
|
|
4923
|
-
profileType
|
|
5359
|
+
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
5360
|
+
build: profiles
|
|
4924
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 };
|
|
4925
5368
|
};
|
|
4926
|
-
const
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
const
|
|
4931
|
-
|
|
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;
|
|
4932
5380
|
return {
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
name
|
|
5381
|
+
...base,
|
|
5382
|
+
...overlay
|
|
4936
5383
|
};
|
|
4937
5384
|
};
|
|
4938
|
-
const
|
|
4939
|
-
if (!
|
|
4940
|
-
|
|
5385
|
+
const mergeAndroid = (base, overlay) => {
|
|
5386
|
+
if (!base) return overlay;
|
|
5387
|
+
if (!overlay) return base;
|
|
5388
|
+
return {
|
|
5389
|
+
...base,
|
|
5390
|
+
...overlay
|
|
5391
|
+
};
|
|
4941
5392
|
};
|
|
4942
|
-
const
|
|
4943
|
-
if (!
|
|
4944
|
-
|
|
5393
|
+
const mergeEnv = (base, overlay) => {
|
|
5394
|
+
if (!base) return overlay;
|
|
5395
|
+
if (!overlay) return base;
|
|
5396
|
+
return {
|
|
5397
|
+
...base,
|
|
5398
|
+
...overlay
|
|
5399
|
+
};
|
|
4945
5400
|
};
|
|
4946
|
-
const
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
const
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
const
|
|
4956
|
-
return
|
|
4957
|
-
}
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
}
|
|
4968
|
-
} })
|
|
4969
|
-
}), toAscCertificate);
|
|
4970
|
-
if (resource === null) return yield* Effect.fail(malformed("certificate"));
|
|
4971
|
-
return resource;
|
|
4972
|
-
}));
|
|
4973
|
-
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4974
|
-
yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
4975
|
-
}));
|
|
4976
|
-
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4977
|
-
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
4978
|
-
}));
|
|
4979
|
-
const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4980
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/bundleIds", {
|
|
4981
|
-
method: "POST",
|
|
4982
|
-
body: JSON.stringify({ data: {
|
|
4983
|
-
type: "bundleIds",
|
|
4984
|
-
attributes: {
|
|
4985
|
-
identifier: params.identifier,
|
|
4986
|
-
name: params.name,
|
|
4987
|
-
platform: "IOS"
|
|
4988
|
-
}
|
|
4989
|
-
} })
|
|
4990
|
-
}), toAscBundleId);
|
|
4991
|
-
if (resource === null) return yield* Effect.fail(malformed("bundleId"));
|
|
4992
|
-
return resource;
|
|
4993
|
-
}));
|
|
4994
|
-
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4995
|
-
return extractList(yield* fetchRaw(jwt, "/v1/devices?limit=200"), toAscDevice);
|
|
4996
|
-
}));
|
|
4997
|
-
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
4998
|
-
const relationships = {
|
|
4999
|
-
bundleId: { data: {
|
|
5000
|
-
type: "bundleIds",
|
|
5001
|
-
id: params.bundleIdAscId
|
|
5002
|
-
} },
|
|
5003
|
-
certificates: { data: params.certificateAscIds.map((id) => ({
|
|
5004
|
-
type: "certificates",
|
|
5005
|
-
id
|
|
5006
|
-
})) },
|
|
5007
|
-
...params.deviceAscIds.length > 0 ? { devices: { data: params.deviceAscIds.map((id) => ({
|
|
5008
|
-
type: "devices",
|
|
5009
|
-
id
|
|
5010
|
-
})) } } : {}
|
|
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;
|
|
5411
|
+
return {
|
|
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 }
|
|
5011
5422
|
};
|
|
5012
|
-
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/profiles", {
|
|
5013
|
-
method: "POST",
|
|
5014
|
-
body: JSON.stringify({ data: {
|
|
5015
|
-
type: "profiles",
|
|
5016
|
-
attributes: {
|
|
5017
|
-
name: params.profileName,
|
|
5018
|
-
profileType: params.profileType
|
|
5019
|
-
},
|
|
5020
|
-
relationships
|
|
5021
|
-
} })
|
|
5022
|
-
}), toAscProfile);
|
|
5023
|
-
if (resource === null) return yield* Effect.fail(malformed("profile"));
|
|
5024
|
-
return resource;
|
|
5025
|
-
}));
|
|
5026
|
-
const isCertificateLimitError = (error) => {
|
|
5027
|
-
if (error._tag !== "AscApiError") return false;
|
|
5028
|
-
return /already have a current.*certificate|pending certificate request/iu.test(error.message);
|
|
5029
5423
|
};
|
|
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;
|
|
5440
|
+
});
|
|
5441
|
+
const stripExtends = (profile) => {
|
|
5442
|
+
if (profile.extends === void 0) return profile;
|
|
5443
|
+
const { extends: _omit, ...rest } = profile;
|
|
5444
|
+
return rest;
|
|
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
|
+
});
|
|
5030
5451
|
|
|
5031
5452
|
//#endregion
|
|
5032
|
-
//#region src/lib/
|
|
5033
|
-
|
|
5034
|
-
const
|
|
5035
|
-
const
|
|
5036
|
-
|
|
5037
|
-
|
|
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";
|
|
5038
5461
|
};
|
|
5039
|
-
const
|
|
5040
|
-
|
|
5041
|
-
if (
|
|
5042
|
-
|
|
5043
|
-
|
|
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";
|
|
5044
5467
|
};
|
|
5045
|
-
const
|
|
5046
|
-
|
|
5047
|
-
if (
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
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
|
+
};
|
|
5051
5506
|
};
|
|
5052
|
-
const
|
|
5053
|
-
|
|
5054
|
-
|
|
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
|
+
};
|
|
5055
5522
|
};
|
|
5056
|
-
const
|
|
5057
|
-
const
|
|
5058
|
-
const
|
|
5059
|
-
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);
|
|
5060
5526
|
return {
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
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 }
|
|
5068
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)
|
|
5069
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
|
|
5070
5551
|
/**
|
|
5071
|
-
*
|
|
5072
|
-
*
|
|
5073
|
-
*
|
|
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).
|
|
5074
5555
|
*/
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
const password = generatePassword();
|
|
5094
|
-
const p12Asn1 = forge.pkcs12.toPkcs12Asn1(params.privateKey, [cert], password, {
|
|
5095
|
-
friendlyName: "key",
|
|
5096
|
-
algorithm: "3des"
|
|
5097
|
-
});
|
|
5098
|
-
return {
|
|
5099
|
-
cert,
|
|
5100
|
-
p12Base64: forge.util.encode64(forge.asn1.toDer(p12Asn1).getBytes()),
|
|
5101
|
-
password
|
|
5102
|
-
};
|
|
5103
|
-
},
|
|
5104
|
-
catch: (error) => new CertParseError({ message: `Failed to assemble .p12: ${error instanceof Error ? error.message : String(error)}` })
|
|
5105
|
-
});
|
|
5106
|
-
const metadata = yield* extractCertMetadata(result.cert);
|
|
5107
|
-
return {
|
|
5108
|
-
p12Base64: result.p12Base64,
|
|
5109
|
-
password: result.password,
|
|
5110
|
-
metadata
|
|
5111
|
-
};
|
|
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(", ")}`);
|
|
5112
5574
|
});
|
|
5113
5575
|
|
|
5114
5576
|
//#endregion
|
|
5115
|
-
//#region src/lib/
|
|
5116
|
-
const
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
});
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
csr.setSubject([{
|
|
5129
|
-
name: "commonName",
|
|
5130
|
-
shortName: "CN",
|
|
5131
|
-
value: "PEM"
|
|
5132
|
-
}]);
|
|
5133
|
-
csr.sign(keyPair.privateKey, forge.md.sha1.create());
|
|
5134
|
-
return {
|
|
5135
|
-
csrPem: forge.pki.certificationRequestToPem(csr),
|
|
5136
|
-
privateKeyPem: forge.pki.privateKeyToPem(keyPair.privateKey),
|
|
5137
|
-
privateKey: keyPair.privateKey
|
|
5138
|
-
};
|
|
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)}` })));
|
|
5139
5590
|
};
|
|
5140
5591
|
|
|
5141
5592
|
//#endregion
|
|
5142
|
-
//#region src/lib/
|
|
5143
|
-
const
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
if (cause._tag === "AppleAuthError") return "Apple JWT signing failed";
|
|
5158
|
-
return "Network error talking to Apple";
|
|
5159
|
-
};
|
|
5160
|
-
const wrapAscError = (step) => (cause) => {
|
|
5161
|
-
if (cause._tag === "AscApiError" && isCertificateLimitError(cause)) return new CertificateLimitError({ message: cause.message });
|
|
5162
|
-
return new GenerateFailedError({
|
|
5163
|
-
step,
|
|
5164
|
-
message: messageForAscCause(cause)
|
|
5165
|
-
});
|
|
5166
|
-
};
|
|
5167
|
-
const generateAndUploadKeystore = (api, input) => Effect.scoped(Effect.gen(function* () {
|
|
5168
|
-
const fs = yield* FileSystem.FileSystem;
|
|
5169
|
-
const tempDir = yield* acquireBuildTempDir;
|
|
5170
|
-
const keystorePath = path.join(tempDir, "release.keystore");
|
|
5171
|
-
yield* generateAndroidKeystore({
|
|
5172
|
-
outputPath: keystorePath,
|
|
5173
|
-
keyAlias: input.keyAlias,
|
|
5174
|
-
storePassword: input.storePassword,
|
|
5175
|
-
keyPassword: input.keyPassword,
|
|
5176
|
-
commonName: input.commonName,
|
|
5177
|
-
organization: input.organization,
|
|
5178
|
-
...input.validityDays === void 0 ? {} : { validityDays: input.validityDays }
|
|
5179
|
-
});
|
|
5180
|
-
const bytes = yield* fs.readFile(keystorePath);
|
|
5181
|
-
const created = yield* api.androidUploadKeystores.upload({ payload: {
|
|
5182
|
-
keystoreBase64: toBase64(bytes),
|
|
5183
|
-
keyAlias: input.keyAlias,
|
|
5184
|
-
keystorePassword: input.storePassword,
|
|
5185
|
-
keyPassword: input.keyPassword
|
|
5186
|
-
} });
|
|
5187
|
-
return {
|
|
5188
|
-
id: created.id,
|
|
5189
|
-
keyAlias: created.keyAlias
|
|
5190
|
-
};
|
|
5191
|
-
}));
|
|
5192
|
-
const fetchAscCredentials = (api, ascApiKeyId) => api.ascApiKeys.getCredentials({ path: { id: ascApiKeyId } });
|
|
5193
|
-
const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(function* () {
|
|
5194
|
-
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
5195
|
-
const ascCreds = {
|
|
5196
|
-
keyId: creds.keyId,
|
|
5197
|
-
issuerId: creds.issuerId,
|
|
5198
|
-
p8Pem: creds.p8Pem
|
|
5199
|
-
};
|
|
5200
|
-
const csrResult = yield* Effect.tryPromise({
|
|
5201
|
-
try: generateCertificateSigningRequest,
|
|
5202
|
-
catch: (cause) => new GenerateFailedError({
|
|
5203
|
-
step: "csr",
|
|
5204
|
-
message: `CSR generation failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
5205
|
-
})
|
|
5206
|
-
});
|
|
5207
|
-
const certificateType = input.certificateType ?? "IOS_DISTRIBUTION";
|
|
5208
|
-
const apple = yield* createCertificate(ascCreds, {
|
|
5209
|
-
csrPem: csrResult.csrPem,
|
|
5210
|
-
certificateType
|
|
5211
|
-
}).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
|
|
5212
|
-
if (apple.certificateContent === null) return yield* Effect.fail(new GenerateFailedError({
|
|
5213
|
-
step: "apple-create-certificate",
|
|
5214
|
-
message: "Apple response missing certificateContent"
|
|
5215
|
-
}));
|
|
5216
|
-
const bundle = yield* buildDistributionCertP12({
|
|
5217
|
-
certificateContentBase64: apple.certificateContent,
|
|
5218
|
-
privateKey: csrResult.privateKey
|
|
5219
|
-
}).pipe(Effect.mapError((cause) => new GenerateFailedError({
|
|
5220
|
-
step: "p12-build",
|
|
5221
|
-
message: cause.message
|
|
5222
|
-
})));
|
|
5223
|
-
const created = yield* api.appleDistributionCertificates.upload({ payload: {
|
|
5224
|
-
p12Base64: bundle.p12Base64,
|
|
5225
|
-
p12Password: bundle.password,
|
|
5226
|
-
serialNumber: bundle.metadata.serialNumber,
|
|
5227
|
-
appleTeamIdentifier: bundle.metadata.appleTeamId,
|
|
5228
|
-
...bundle.metadata.appleTeamName === null ? {} : { appleTeamName: bundle.metadata.appleTeamName },
|
|
5229
|
-
...bundle.metadata.developerIdIdentifier === null ? {} : { developerIdIdentifier: bundle.metadata.developerIdIdentifier },
|
|
5230
|
-
validFrom: bundle.metadata.validFrom,
|
|
5231
|
-
validUntil: bundle.metadata.validUntil
|
|
5232
|
-
} });
|
|
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" });
|
|
5233
5608
|
return {
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
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
|
|
5239
5613
|
};
|
|
5240
5614
|
});
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
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)));
|
|
5248
5643
|
});
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
yield* api.appleDistributionCertificates.delete({ path: { id: input.distributionCertificateId } });
|
|
5272
|
-
deletedLocally = true;
|
|
5273
|
-
}
|
|
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;
|
|
5274
5666
|
return {
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
deletedLocally
|
|
5667
|
+
...applicationId === void 0 ? {} : { applicationId },
|
|
5668
|
+
...versionCode === void 0 ? {} : { versionCode },
|
|
5669
|
+
...versionName === void 0 ? {} : { versionName }
|
|
5279
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
|
+
})));
|
|
5280
5705
|
});
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
}
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
}).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.` });
|
|
5304
5728
|
});
|
|
5305
|
-
|
|
5306
|
-
|
|
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"];
|
|
5307
5744
|
return {
|
|
5308
|
-
|
|
5309
|
-
|
|
5745
|
+
hash,
|
|
5746
|
+
sources: Array.isArray(sourcesRaw) ? sourcesRaw : []
|
|
5310
5747
|
};
|
|
5311
5748
|
});
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
};
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
}
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
|
|
5327
|
-
if (useDevices && deviceAscIds.length === 0) return yield* Effect.fail(new GenerateFailedError({
|
|
5328
|
-
step: "collect-devices",
|
|
5329
|
-
message: "No registered devices to attach to the provisioning profile"
|
|
5330
|
-
}));
|
|
5331
|
-
const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
|
|
5332
|
-
profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
|
|
5333
|
-
profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
|
|
5334
|
-
bundleIdAscId,
|
|
5335
|
-
certificateAscIds: [certAscId],
|
|
5336
|
-
deviceAscIds
|
|
5337
|
-
}).pipe(Effect.mapError(wrapAscError("apple-create-profile")))).profileContent);
|
|
5338
|
-
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceAscIds) : void 0;
|
|
5339
|
-
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
5340
|
-
profileBase64: toBase64(profileBytes),
|
|
5341
|
-
appleDistributionCertificateId: input.distributionCertificateId,
|
|
5342
|
-
isManaged: true,
|
|
5343
|
-
...rosterHash === void 0 ? {} : { deviceRosterHash: rosterHash }
|
|
5344
|
-
} });
|
|
5345
|
-
return {
|
|
5346
|
-
id: created.id,
|
|
5347
|
-
bundleIdentifier: created.bundleIdentifier,
|
|
5348
|
-
distributionType: created.distributionType,
|
|
5349
|
-
profileName: created.profileName,
|
|
5350
|
-
validUntil: created.validUntil,
|
|
5351
|
-
developerPortalIdentifier: created.developerPortalIdentifier
|
|
5352
|
-
};
|
|
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".` });
|
|
5353
5763
|
});
|
|
5354
5764
|
|
|
5355
5765
|
//#endregion
|