@better-update/cli 0.15.2 → 0.15.3
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 +1718 -1705
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -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.15.
|
|
31
|
+
var version = "0.15.3";
|
|
32
32
|
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/lib/interactive-mode.ts
|
|
@@ -4497,1846 +4497,1858 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
|
4497
4497
|
});
|
|
4498
4498
|
|
|
4499
4499
|
//#endregion
|
|
4500
|
-
//#region src/lib/
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4500
|
+
//#region src/lib/credentials-generator-apple-id.ts
|
|
4501
|
+
const DISTRIBUTION_TO_PROFILE_TYPE = {
|
|
4502
|
+
APP_STORE: AppleUtils.ProfileType.IOS_APP_STORE,
|
|
4503
|
+
AD_HOC: AppleUtils.ProfileType.IOS_APP_ADHOC,
|
|
4504
|
+
DEVELOPMENT: AppleUtils.ProfileType.IOS_APP_DEVELOPMENT,
|
|
4505
|
+
ENTERPRISE: AppleUtils.ProfileType.IOS_APP_INHOUSE
|
|
4506
|
+
};
|
|
4507
|
+
const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
|
|
4508
|
+
APP_STORE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
|
|
4509
|
+
AD_HOC: AppleUtils.CertificateType.IOS_DISTRIBUTION,
|
|
4510
|
+
ENTERPRISE: AppleUtils.CertificateType.IOS_DISTRIBUTION,
|
|
4511
|
+
DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
|
|
4512
|
+
};
|
|
4513
|
+
var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
|
|
4514
|
+
const CERT_LIMIT_PATTERN = /already have a current.*certificate|pending certificate request/iu;
|
|
4515
|
+
const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
4516
|
+
const wrap = (step, run) => Effect.tryPromise({
|
|
4517
|
+
try: run,
|
|
4518
|
+
catch: (cause) => new AppleIdGenerateFailedError({
|
|
4519
|
+
step,
|
|
4520
|
+
message: messageOf(cause)
|
|
4521
|
+
})
|
|
4506
4522
|
});
|
|
4507
|
-
const
|
|
4508
|
-
try:
|
|
4509
|
-
catch: (cause) =>
|
|
4523
|
+
const wrapCertificateCreate = (run) => Effect.tryPromise({
|
|
4524
|
+
try: run,
|
|
4525
|
+
catch: (cause) => {
|
|
4526
|
+
const message = messageOf(cause);
|
|
4527
|
+
if (CERT_LIMIT_PATTERN.test(message)) return new CertificateLimitError({ message });
|
|
4528
|
+
return new AppleIdGenerateFailedError({
|
|
4529
|
+
step: "apple-create-certificate",
|
|
4530
|
+
message
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4510
4533
|
});
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
*
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
}
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4534
|
+
const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effect.gen(function* () {
|
|
4535
|
+
const ctx = input.context;
|
|
4536
|
+
const certificateType = input.certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
|
|
4537
|
+
const result = yield* wrapCertificateCreate(async () => AppleUtils.createCertificateAndP12Async(ctx, { certificateType }));
|
|
4538
|
+
const metadata = yield* extractMetadataFromP12({
|
|
4539
|
+
p12Base64: result.certificateP12,
|
|
4540
|
+
password: result.password
|
|
4541
|
+
}).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
|
|
4542
|
+
step: "parse-p12",
|
|
4543
|
+
message: cause.message
|
|
4544
|
+
})));
|
|
4545
|
+
const created = yield* api.appleDistributionCertificates.upload({ payload: {
|
|
4546
|
+
p12Base64: result.certificateP12,
|
|
4547
|
+
p12Password: result.password,
|
|
4548
|
+
serialNumber: metadata.serialNumber,
|
|
4549
|
+
appleTeamIdentifier: metadata.appleTeamId,
|
|
4550
|
+
...metadata.appleTeamName === null ? {} : { appleTeamName: metadata.appleTeamName },
|
|
4551
|
+
...metadata.developerIdIdentifier === null ? {} : { developerIdIdentifier: metadata.developerIdIdentifier },
|
|
4552
|
+
validFrom: metadata.validFrom,
|
|
4553
|
+
validUntil: metadata.validUntil
|
|
4554
|
+
} });
|
|
4555
|
+
return {
|
|
4556
|
+
id: created.id,
|
|
4557
|
+
serialNumber: metadata.serialNumber,
|
|
4558
|
+
appleTeamId: created.appleTeamId,
|
|
4559
|
+
appleTeamIdentifier: metadata.appleTeamId,
|
|
4560
|
+
developerPortalIdentifier: result.certificate.id
|
|
4561
|
+
};
|
|
4562
|
+
});
|
|
4563
|
+
const listDistributionCertsViaAppleId = (ctx, certificateType = "IOS_DISTRIBUTION") => Effect.gen(function* () {
|
|
4564
|
+
const filter = certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
|
|
4565
|
+
return (yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType: filter } } }))).map((entry) => ({
|
|
4566
|
+
developerPortalIdentifier: entry.id,
|
|
4567
|
+
serialNumber: entry.attributes.serialNumber,
|
|
4568
|
+
displayName: entry.attributes.displayName,
|
|
4569
|
+
expirationDate: entry.attributes.expirationDate
|
|
4570
|
+
}));
|
|
4571
|
+
});
|
|
4572
|
+
const revokeDistributionCertViaAppleId = (ctx, developerPortalIdentifier) => wrap("apple-revoke-certificate", async () => AppleUtils.Certificate.deleteAsync(ctx, { id: developerPortalIdentifier }));
|
|
4573
|
+
const findOrCreateBundleId = (ctx, bundleIdentifier) => Effect.gen(function* () {
|
|
4574
|
+
const existing = yield* wrap("apple-find-bundle-id", async () => AppleUtils.BundleId.findAsync(ctx, { identifier: bundleIdentifier }));
|
|
4575
|
+
if (existing !== null) return existing.id;
|
|
4576
|
+
return (yield* wrap("apple-create-bundle-id", async () => AppleUtils.BundleId.createAsync(ctx, {
|
|
4577
|
+
identifier: bundleIdentifier,
|
|
4578
|
+
name: bundleIdentifier,
|
|
4579
|
+
platform: AppleUtils.BundleIdPlatform.IOS
|
|
4580
|
+
}))).id;
|
|
4581
|
+
});
|
|
4582
|
+
const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(function* () {
|
|
4583
|
+
const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
|
|
4584
|
+
const upper = serialNumber.toUpperCase();
|
|
4585
|
+
const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
|
|
4586
|
+
if (match === void 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
|
|
4587
|
+
step: "match-apple-certificate",
|
|
4588
|
+
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
4589
|
+
}));
|
|
4590
|
+
return match.id;
|
|
4591
|
+
});
|
|
4592
|
+
const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
|
|
4593
|
+
const devices = yield* wrap("apple-list-devices", async () => AppleUtils.Device.getAllIOSProfileDevicesAsync(ctx));
|
|
4594
|
+
if (deviceIds === void 0) return devices.map((device) => device.id);
|
|
4595
|
+
const allowed = new Set(deviceIds);
|
|
4596
|
+
return devices.filter((device) => allowed.has(device.id)).map((device) => device.id);
|
|
4597
|
+
});
|
|
4598
|
+
const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.gen(function* () {
|
|
4599
|
+
const ctx = input.context;
|
|
4600
|
+
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 AppleIdGenerateFailedError({
|
|
4601
|
+
step: "load-distribution-certificate",
|
|
4602
|
+
message: `Distribution certificate ${input.distributionCertificateId} not found`
|
|
4603
|
+
})) : Effect.succeed(match)));
|
|
4604
|
+
const certificateType = DISTRIBUTION_TO_CERTIFICATE_TYPE[input.distributionType];
|
|
4605
|
+
const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
|
|
4606
|
+
const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
|
|
4607
|
+
const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
|
|
4608
|
+
if (useDevices && deviceIds.length === 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
|
|
4609
|
+
step: "collect-devices",
|
|
4610
|
+
message: "No registered devices to attach to the provisioning profile"
|
|
4611
|
+
}));
|
|
4612
|
+
const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
|
|
4613
|
+
const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
|
|
4614
|
+
bundleId: bundleIdAscId,
|
|
4615
|
+
certificates: [certAscId],
|
|
4616
|
+
devices: deviceIds,
|
|
4617
|
+
name: profileName,
|
|
4618
|
+
profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
|
|
4619
|
+
}))).attributes;
|
|
4620
|
+
if (profileContent === null) return yield* Effect.fail(new AppleIdGenerateFailedError({
|
|
4621
|
+
step: "extract-profile-content",
|
|
4622
|
+
message: "Apple returned a profile with no content (likely expired/invalid)"
|
|
4623
|
+
}));
|
|
4624
|
+
const profileBytes = fromBase64(profileContent);
|
|
4625
|
+
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
|
|
4626
|
+
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
4627
|
+
profileBase64: toBase64(profileBytes),
|
|
4628
|
+
appleDistributionCertificateId: input.distributionCertificateId,
|
|
4629
|
+
isManaged: true,
|
|
4630
|
+
...rosterHash === void 0 ? {} : { deviceRosterHash: rosterHash }
|
|
4631
|
+
} });
|
|
4632
|
+
return {
|
|
4633
|
+
id: created.id,
|
|
4634
|
+
bundleIdentifier: created.bundleIdentifier,
|
|
4635
|
+
distributionType: created.distributionType,
|
|
4636
|
+
profileName: created.profileName,
|
|
4637
|
+
validUntil: created.validUntil,
|
|
4638
|
+
developerPortalIdentifier: created.developerPortalIdentifier
|
|
4639
|
+
};
|
|
4550
4640
|
});
|
|
4551
4641
|
|
|
4552
4642
|
//#endregion
|
|
4553
|
-
//#region src/lib/ios-
|
|
4554
|
-
const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
4555
|
-
const boolTag = (value) => value ? "<true/>" : "<false/>";
|
|
4643
|
+
//#region src/lib/ios-bundle-config-upsert.ts
|
|
4556
4644
|
/**
|
|
4557
|
-
*
|
|
4558
|
-
*
|
|
4559
|
-
*
|
|
4560
|
-
*
|
|
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`
|
|
4645
|
+
* Idempotent bind for an iOS bundle configuration. When the row already exists
|
|
4646
|
+
* (e.g. orphaned after a cert was deleted, since the FK is `ON DELETE SET NULL`),
|
|
4647
|
+
* rebind cert + profile in place instead of failing on the unique constraint.
|
|
4648
|
+
* Mirrors EAS's setup behavior where the setup step is rerunnable.
|
|
4564
4649
|
*/
|
|
4565
|
-
const
|
|
4566
|
-
const
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
"
|
|
4580
|
-
"
|
|
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");
|
|
4587
|
-
};
|
|
4588
|
-
|
|
4589
|
-
//#endregion
|
|
4590
|
-
//#region src/lib/ios-keychain.ts
|
|
4591
|
-
const runOrFail = (cmd, step) => Command.string(cmd).pipe(Effect.mapError((cause) => new KeychainError({ message: `keychain ${step} failed: ${String(cause)}` })));
|
|
4592
|
-
const listCurrentKeychains = Effect.gen(function* () {
|
|
4593
|
-
return (yield* runOrFail(Command.make("security", "list-keychains", "-d", "user"), "list-keychains")).split("\n").map((line) => line.trim().replace(/^"/u, "").replace(/"$/u, "")).filter((line) => line.length > 0);
|
|
4594
|
-
});
|
|
4595
|
-
const parseSigningIdentity = (output) => {
|
|
4596
|
-
const lines = output.split("\n");
|
|
4597
|
-
for (const line of lines) {
|
|
4598
|
-
const match = /"([^"]+)"/u.exec(line);
|
|
4599
|
-
if (match?.[1]) return match[1];
|
|
4650
|
+
const upsertIosBundleConfiguration = (api, input) => Effect.gen(function* () {
|
|
4651
|
+
const existing = (yield* api.iosBundleConfigurations.list({ path: { projectId: input.projectId } })).items.find((item) => item.bundleIdentifier === input.bundleIdentifier && item.distributionType === input.distributionType);
|
|
4652
|
+
if (existing === void 0) {
|
|
4653
|
+
yield* api.iosBundleConfigurations.create({
|
|
4654
|
+
path: { projectId: input.projectId },
|
|
4655
|
+
payload: {
|
|
4656
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
4657
|
+
distributionType: input.distributionType,
|
|
4658
|
+
appleTeamId: input.appleTeamId,
|
|
4659
|
+
appleDistributionCertificateId: input.appleDistributionCertificateId,
|
|
4660
|
+
appleProvisioningProfileId: input.appleProvisioningProfileId,
|
|
4661
|
+
...input.ascApiKeyId === void 0 ? {} : { ascApiKeyId: input.ascApiKeyId }
|
|
4662
|
+
}
|
|
4663
|
+
});
|
|
4664
|
+
yield* Console.log("iOS bundle configuration saved.");
|
|
4665
|
+
return { action: "created" };
|
|
4600
4666
|
}
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
* user search list, and tear it all down on scope close. The keychain name is
|
|
4605
|
-
* namespaced as `better-update-<uuid>` and lives in `$tempDir`, so cleanup is
|
|
4606
|
-
* guaranteed under all termination paths.
|
|
4607
|
-
*/
|
|
4608
|
-
const acquireKeychain = ({ tempDir, p12Path, p12Password }) => {
|
|
4609
|
-
const keychainName = `better-update-${randomUUID()}.keychain-db`;
|
|
4610
|
-
const keychainPath = path.join(tempDir, keychainName);
|
|
4611
|
-
const keychainPassword = randomBytes(32).toString("hex");
|
|
4612
|
-
return Effect.acquireRelease(Effect.gen(function* () {
|
|
4613
|
-
const priorKeychains = yield* listCurrentKeychains;
|
|
4614
|
-
yield* runOrFail(Command.make("security", "create-keychain", "-p", keychainPassword, keychainPath), "create-keychain");
|
|
4615
|
-
yield* runOrFail(Command.make("security", "unlock-keychain", "-p", keychainPassword, keychainPath), "unlock-keychain");
|
|
4616
|
-
yield* runOrFail(Command.make("security", "set-keychain-settings", "-t", "3600", "-l", keychainPath), "set-keychain-settings");
|
|
4617
|
-
yield* runOrFail(Command.make("security", "import", p12Path, "-k", keychainPath, "-P", p12Password, "-T", "/usr/bin/codesign"), "import");
|
|
4618
|
-
yield* runOrFail(Command.make("security", "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", keychainPassword, keychainPath), "set-key-partition-list");
|
|
4619
|
-
yield* runOrFail(Command.make("security", "list-keychains", "-d", "user", "-s", keychainPath, ...priorKeychains), "list-keychains -s (add)");
|
|
4620
|
-
const signingIdentity = parseSigningIdentity(yield* runOrFail(Command.make("security", "find-identity", "-v", "-p", "codesigning", keychainPath), "find-identity"));
|
|
4621
|
-
if (!signingIdentity) return yield* new KeychainError({ message: "No code signing identity found after importing .p12 into ephemeral keychain." });
|
|
4622
|
-
return {
|
|
4623
|
-
handle: {
|
|
4624
|
-
keychainName,
|
|
4625
|
-
keychainPath,
|
|
4626
|
-
signingIdentity
|
|
4627
|
-
},
|
|
4628
|
-
priorKeychains
|
|
4629
|
-
};
|
|
4630
|
-
}), ({ priorKeychains }) => Effect.gen(function* () {
|
|
4631
|
-
yield* Command.string(Command.make("security", "list-keychains", "-d", "user", "-s", ...priorKeychains)).pipe(Effect.catchAll(() => Effect.void));
|
|
4632
|
-
yield* Command.string(Command.make("security", "delete-keychain", keychainPath)).pipe(Effect.catchAll(() => Effect.void));
|
|
4633
|
-
})).pipe(Effect.map(({ handle }) => handle));
|
|
4634
|
-
};
|
|
4635
|
-
|
|
4636
|
-
//#endregion
|
|
4637
|
-
//#region src/lib/plist.ts
|
|
4638
|
-
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
4639
|
-
/**
|
|
4640
|
-
* Parse an XML plist string into a typed object.
|
|
4641
|
-
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
4642
|
-
*/
|
|
4643
|
-
const parsePlistXml = (xml) => plist.parse(xml);
|
|
4644
|
-
/**
|
|
4645
|
-
* Parse a binary plist buffer into a typed object.
|
|
4646
|
-
* Uses bplist-parser for Apple's binary plist format.
|
|
4647
|
-
*/
|
|
4648
|
-
const parsePlistBinary = (buffer) => {
|
|
4649
|
-
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
4650
|
-
return result;
|
|
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"));
|
|
4657
|
-
|
|
4658
|
-
//#endregion
|
|
4659
|
-
//#region src/lib/ios-provisioning.ts
|
|
4660
|
-
const getString = (obj, key) => {
|
|
4661
|
-
const value = obj[key];
|
|
4662
|
-
return typeof value === "string" ? value : void 0;
|
|
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* () {
|
|
4674
|
-
const parsed = yield* Effect.try({
|
|
4675
|
-
try: () => parsePlistXml(plistXml),
|
|
4676
|
-
catch: (error) => new ProvisioningError({ message: `Failed to parse provisioning profile plist: ${error instanceof Error ? error.message : String(error)}` })
|
|
4667
|
+
if (existing.appleTeamId !== input.appleTeamId) return yield* new MissingCredentialsError({
|
|
4668
|
+
message: `Bundle "${input.bundleIdentifier}" (${input.distributionType}) is already bound to a different Apple team than the new credentials.`,
|
|
4669
|
+
hint: "Delete the existing bundle configuration via the dashboard before retrying with a different team."
|
|
4677
4670
|
});
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4671
|
+
yield* api.iosBundleConfigurations.update({
|
|
4672
|
+
path: { id: existing.id },
|
|
4673
|
+
payload: {
|
|
4674
|
+
appleDistributionCertificateId: input.appleDistributionCertificateId,
|
|
4675
|
+
appleProvisioningProfileId: input.appleProvisioningProfileId,
|
|
4676
|
+
...input.ascApiKeyId === void 0 ? {} : { ascApiKeyId: input.ascApiKeyId }
|
|
4677
|
+
}
|
|
4678
|
+
});
|
|
4679
|
+
yield* Console.log("iOS bundle configuration rebound.");
|
|
4682
4680
|
return {
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
teamId
|
|
4681
|
+
action: "updated",
|
|
4682
|
+
id: existing.id
|
|
4686
4683
|
};
|
|
4687
4684
|
});
|
|
4688
|
-
const userProvisioningProfilesDir = () => path.join(os.homedir(), "Library", "MobileDevice", "Provisioning Profiles");
|
|
4689
|
-
/**
|
|
4690
|
-
* Scoped installation of a provisioning profile: parses its metadata via
|
|
4691
|
-
* `security cms -D -i`, copies it into `~/Library/MobileDevice/Provisioning Profiles`
|
|
4692
|
-
* under `<uuid>.mobileprovision`, and removes the copy on scope close — but
|
|
4693
|
-
* only if we installed it. If the target file already existed when we arrived
|
|
4694
|
-
* (e.g., Xcode had it), we leave both the file and the contents untouched.
|
|
4695
|
-
*/
|
|
4696
|
-
const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Effect.gen(function* () {
|
|
4697
|
-
const fs = yield* FileSystem.FileSystem;
|
|
4698
|
-
const info = yield* extractProvisioningInfo(yield* Command.string(Command.make("security", "cms", "-D", "-i", profilePath)).pipe(Effect.mapError((cause) => new ProvisioningError({ message: `security cms -D failed for ${profilePath}: ${String(cause)}` }))));
|
|
4699
|
-
const targetDir = userProvisioningProfilesDir();
|
|
4700
|
-
const installedPath = path.join(targetDir, `${info.uuid}.mobileprovision`);
|
|
4701
|
-
yield* fs.makeDirectory(targetDir, { recursive: true }).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to create provisioning profiles dir: ${String(cause)}` })));
|
|
4702
|
-
if (yield* fs.exists(installedPath).pipe(Effect.orElseSucceed(() => false))) return {
|
|
4703
|
-
...info,
|
|
4704
|
-
installedPath,
|
|
4705
|
-
ownsInstallation: false
|
|
4706
|
-
};
|
|
4707
|
-
yield* fs.copyFile(profilePath, installedPath).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to copy provisioning profile into ${installedPath}: ${String(cause)}` })));
|
|
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
|
-
})));
|
|
4722
4685
|
|
|
4723
4686
|
//#endregion
|
|
4724
|
-
//#region src/
|
|
4725
|
-
const
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
}
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
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
|
-
};
|
|
4687
|
+
//#region src/application/credentials-interactive-apple-id.ts
|
|
4688
|
+
const chooseIosSetupPath = (api) => Effect.gen(function* () {
|
|
4689
|
+
if (!(yield* api.ascApiKeys.list()).items.some((key) => key.appleTeamId !== null)) return "apple-id";
|
|
4690
|
+
return yield* promptSelect("How would you like to provide your iOS credentials?", [{
|
|
4691
|
+
value: "apple-id",
|
|
4692
|
+
label: "Login with Apple ID (recommended for interactive use)"
|
|
4693
|
+
}, {
|
|
4694
|
+
value: "asc-key",
|
|
4695
|
+
label: "Use an App Store Connect API key"
|
|
4696
|
+
}]);
|
|
4740
4697
|
});
|
|
4741
|
-
const
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
const
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4698
|
+
const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
|
|
4699
|
+
yield* Console.log("");
|
|
4700
|
+
yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
|
|
4701
|
+
const certs = yield* listDistributionCertsViaAppleId(ctx, "IOS_DISTRIBUTION");
|
|
4702
|
+
if (certs.length === 0) return yield* new AppleIdGenerateFailedError({
|
|
4703
|
+
step: "limit-recover",
|
|
4704
|
+
message: "Apple says the certificate limit is hit but no existing certificates were returned."
|
|
4705
|
+
});
|
|
4706
|
+
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
4707
|
+
value: entry.developerPortalIdentifier,
|
|
4708
|
+
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
4709
|
+
})), { required: true });
|
|
4710
|
+
yield* Effect.forEach(toRevoke, (id) => revokeDistributionCertViaAppleId(ctx, id), { concurrency: "inherit" });
|
|
4711
|
+
yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
|
|
4712
|
+
});
|
|
4713
|
+
const generateDistributionCertViaAppleIdInteractive = (api, ctx) => Effect.gen(function* () {
|
|
4714
|
+
yield* Console.log("Generating distribution certificate via Apple ID...");
|
|
4715
|
+
const generate = generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
|
|
4716
|
+
return yield* generate.pipe(Effect.catchTag("CertificateLimitError", () => interactiveAppleIdCertLimitRecover(ctx).pipe(Effect.flatMap(() => generate))));
|
|
4717
|
+
});
|
|
4718
|
+
const GENERATE_NEW = "__generate__";
|
|
4719
|
+
const chooseDistributionCertViaAppleId = (api, ctx, appleTeamIdentifier) => Effect.gen(function* () {
|
|
4720
|
+
const [teams, all] = yield* Effect.all([api.appleTeams.list(), api.appleDistributionCertificates.list()], { concurrency: 2 });
|
|
4721
|
+
const team = teams.items.find((entry) => entry.appleTeamId === appleTeamIdentifier);
|
|
4722
|
+
const items = team === void 0 ? [] : all.items.filter((cert) => cert.appleTeamId === team.id);
|
|
4723
|
+
if (items.length === 0) {
|
|
4724
|
+
const created = yield* generateDistributionCertViaAppleIdInteractive(api, ctx);
|
|
4725
|
+
return {
|
|
4726
|
+
id: created.id,
|
|
4727
|
+
appleTeamId: created.appleTeamId
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
|
|
4731
|
+
value: GENERATE_NEW,
|
|
4732
|
+
label: "Generate a new distribution certificate"
|
|
4733
|
+
}, ...items.map((cert) => ({
|
|
4734
|
+
value: cert.id,
|
|
4735
|
+
label: `${cert.serialNumber.slice(0, 12)}… (team ${appleTeamIdentifier})`
|
|
4736
|
+
}))]);
|
|
4737
|
+
if (choice === GENERATE_NEW) {
|
|
4738
|
+
const created = yield* generateDistributionCertViaAppleIdInteractive(api, ctx);
|
|
4739
|
+
return {
|
|
4740
|
+
id: created.id,
|
|
4741
|
+
appleTeamId: created.appleTeamId
|
|
4742
|
+
};
|
|
4756
4743
|
}
|
|
4744
|
+
const cert = items.find((entry) => entry.id === choice);
|
|
4745
|
+
if (cert === void 0) return yield* new AppleIdGenerateFailedError({
|
|
4746
|
+
step: "pick-certificate",
|
|
4747
|
+
message: `Selected certificate ${choice} not found after listing`
|
|
4748
|
+
});
|
|
4757
4749
|
return {
|
|
4758
|
-
|
|
4759
|
-
|
|
4750
|
+
id: cert.id,
|
|
4751
|
+
appleTeamId: cert.appleTeamId
|
|
4760
4752
|
};
|
|
4761
4753
|
});
|
|
4762
|
-
const
|
|
4763
|
-
const
|
|
4764
|
-
const
|
|
4765
|
-
const
|
|
4766
|
-
|
|
4767
|
-
|
|
4754
|
+
const setupIosViaAppleId = (api, input) => Effect.gen(function* () {
|
|
4755
|
+
const auth = yield* AppleAuth;
|
|
4756
|
+
const session = yield* auth.ensureLoggedIn();
|
|
4757
|
+
const ctx = auth.buildRequestContext(session);
|
|
4758
|
+
yield* Console.log(`Logged in as ${session.username}. Team: ${session.teamName ?? session.teamId} (${session.teamId}).`);
|
|
4759
|
+
const cert = yield* chooseDistributionCertViaAppleId(api, ctx, session.teamId);
|
|
4760
|
+
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
4761
|
+
yield* Console.log("Generating provisioning profile via Apple ID...");
|
|
4762
|
+
const profile = yield* generateAndUploadProvisioningProfileViaAppleId(api, {
|
|
4763
|
+
context: ctx,
|
|
4764
|
+
distributionCertificateId: cert.id,
|
|
4765
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
4766
|
+
distributionType
|
|
4767
|
+
});
|
|
4768
|
+
yield* upsertIosBundleConfiguration(api, {
|
|
4769
|
+
projectId: input.projectId,
|
|
4770
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
4771
|
+
distributionType,
|
|
4772
|
+
appleTeamId: cert.appleTeamId,
|
|
4773
|
+
appleDistributionCertificateId: cert.id,
|
|
4774
|
+
appleProvisioningProfileId: profile.id
|
|
4775
|
+
});
|
|
4768
4776
|
});
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
* provisioning profile
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
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
|
-
});
|
|
4802
|
-
|
|
4803
|
-
//#endregion
|
|
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;
|
|
4777
|
+
const regenerateProvisioningProfileViaAppleId = (api, input) => Effect.gen(function* () {
|
|
4778
|
+
const auth = yield* AppleAuth;
|
|
4779
|
+
const session = yield* auth.ensureLoggedIn();
|
|
4780
|
+
yield* Console.log("Regenerating provisioning profile via Apple ID...");
|
|
4781
|
+
const created = yield* generateAndUploadProvisioningProfileViaAppleId(api, {
|
|
4782
|
+
context: auth.buildRequestContext(session),
|
|
4783
|
+
distributionCertificateId: input.distributionCertificateId,
|
|
4784
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
4785
|
+
distributionType: input.distributionType
|
|
4838
4786
|
});
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
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;
|
|
4787
|
+
yield* api.iosBundleConfigurations.update({
|
|
4788
|
+
path: { id: input.bundleConfigurationId },
|
|
4789
|
+
payload: { appleProvisioningProfileId: created.id }
|
|
4790
|
+
});
|
|
4791
|
+
return created;
|
|
4883
4792
|
});
|
|
4884
4793
|
|
|
4885
4794
|
//#endregion
|
|
4886
|
-
//#region src/
|
|
4887
|
-
|
|
4888
|
-
*
|
|
4889
|
-
*
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
const formatter = ExpoRunFormatter.create(projectRoot);
|
|
4894
|
-
return {
|
|
4895
|
-
pipe: (line) => formatter.pipe(line),
|
|
4896
|
-
getBuildSummary: () => formatter.getBuildSummary()
|
|
4897
|
-
};
|
|
4898
|
-
};
|
|
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?`
|
|
4795
|
+
//#region src/application/credentials-interactive-ios-asc.ts
|
|
4796
|
+
const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* () {
|
|
4797
|
+
yield* Console.log("");
|
|
4798
|
+
yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
|
|
4799
|
+
const certs = yield* listAppleCertificates(api, {
|
|
4800
|
+
ascApiKeyId,
|
|
4801
|
+
certificateType: "IOS_DISTRIBUTION"
|
|
4908
4802
|
});
|
|
4909
|
-
return
|
|
4803
|
+
if (certs.length === 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
4804
|
+
message: "Apple says the certificate limit is hit but no existing certificates were returned.",
|
|
4805
|
+
hint: "Try again later or check the Apple Developer portal."
|
|
4806
|
+
}));
|
|
4807
|
+
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
4808
|
+
value: entry.id,
|
|
4809
|
+
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
4810
|
+
})), { required: true });
|
|
4811
|
+
yield* Effect.forEach(toRevoke, (id) => revokeAppleCertificate(api, {
|
|
4812
|
+
ascApiKeyId,
|
|
4813
|
+
developerPortalIdentifier: id
|
|
4814
|
+
}), { concurrency: "inherit" });
|
|
4815
|
+
yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
|
|
4910
4816
|
});
|
|
4911
|
-
const
|
|
4912
|
-
yield*
|
|
4913
|
-
|
|
4817
|
+
const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
|
|
4818
|
+
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null);
|
|
4819
|
+
if (teamAscKeys.length === 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
4820
|
+
message: "No ASC API key linked to an Apple team in this organization.",
|
|
4821
|
+
hint: "Upload an ASC API key with a team assignment via the dashboard, then retry."
|
|
4822
|
+
}));
|
|
4823
|
+
const ascKeyId = yield* promptSelect("Select an ASC API key to issue the certificate against", teamAscKeys.map((key) => ({
|
|
4824
|
+
value: key.id,
|
|
4825
|
+
label: `${key.name} (${key.keyId})`
|
|
4826
|
+
})));
|
|
4827
|
+
yield* Console.log("Generating CSR and requesting certificate from Apple...");
|
|
4828
|
+
const generate = generateAndUploadDistributionCertificate(api, { ascApiKeyId: ascKeyId });
|
|
4829
|
+
return yield* generate.pipe(Effect.catchTag("CertificateLimitError", () => interactiveCertLimitRecover(api, ascKeyId).pipe(Effect.flatMap(() => generate))));
|
|
4914
4830
|
});
|
|
4915
|
-
const
|
|
4916
|
-
const
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
}
|
|
4831
|
+
const chooseIosCertificateId = (api) => Effect.gen(function* () {
|
|
4832
|
+
const certs = yield* api.appleDistributionCertificates.list();
|
|
4833
|
+
if (certs.items.length === 0) {
|
|
4834
|
+
yield* Console.log("No distribution certificate found in this organization.");
|
|
4835
|
+
if ((yield* promptSelect("How would you like to proceed?", [{
|
|
4836
|
+
value: "generate",
|
|
4837
|
+
label: "Generate a new distribution certificate"
|
|
4838
|
+
}, {
|
|
4839
|
+
value: "abort",
|
|
4840
|
+
label: "Abort — I'll upload one manually"
|
|
4841
|
+
}])) === "abort") return yield* Effect.fail(new MissingCredentialsError({
|
|
4842
|
+
message: "Build aborted — no distribution certificate available.",
|
|
4843
|
+
hint: "Run `better-update credentials generate distribution-certificate --asc-key-id <id>` or upload via the dashboard."
|
|
4844
|
+
}));
|
|
4845
|
+
return (yield* generateDistributionCertInteractive(api)).id;
|
|
4931
4846
|
}
|
|
4932
|
-
|
|
4847
|
+
const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
|
|
4848
|
+
value: "__generate__",
|
|
4849
|
+
label: "Generate a new distribution certificate"
|
|
4850
|
+
}, ...certs.items.map((cert) => ({
|
|
4851
|
+
value: cert.id,
|
|
4852
|
+
label: `${cert.serialNumber.slice(0, 12)}… (team ${cert.appleTeamId})`
|
|
4853
|
+
}))]);
|
|
4854
|
+
if (choice === "__generate__") return (yield* generateDistributionCertInteractive(api)).id;
|
|
4855
|
+
return choice;
|
|
4933
4856
|
});
|
|
4934
|
-
const
|
|
4935
|
-
const
|
|
4936
|
-
const
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
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);
|
|
4857
|
+
const pickIosCertificate = (api) => Effect.gen(function* () {
|
|
4858
|
+
const chosenId = yield* chooseIosCertificateId(api);
|
|
4859
|
+
const cert = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === chosenId);
|
|
4860
|
+
if (cert === void 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
4861
|
+
message: "Selected certificate not found after generation.",
|
|
4862
|
+
hint: "Retry."
|
|
4863
|
+
}));
|
|
4956
4864
|
return {
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
sha256
|
|
4865
|
+
certId: chosenId,
|
|
4866
|
+
cert
|
|
4960
4867
|
};
|
|
4961
4868
|
});
|
|
4962
|
-
const
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4869
|
+
const pickIosAscKey = (api, appleTeamId) => Effect.gen(function* () {
|
|
4870
|
+
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null && key.appleTeamId === appleTeamId);
|
|
4871
|
+
if (teamAscKeys.length === 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
4872
|
+
message: `No ASC API key linked to Apple team ${appleTeamId}.`,
|
|
4873
|
+
hint: "Upload an ASC API key for that team via the dashboard, then retry."
|
|
4874
|
+
}));
|
|
4875
|
+
return yield* promptSelect("Select an ASC API key", teamAscKeys.map((key) => ({
|
|
4876
|
+
value: key.id,
|
|
4877
|
+
label: `${key.name} (${key.keyId})`
|
|
4878
|
+
})));
|
|
4971
4879
|
});
|
|
4972
|
-
const
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
hint
|
|
4981
|
-
});
|
|
4982
|
-
}
|
|
4983
|
-
return yield* Effect.forEach(signedTargets, (target) => installProfileForTarget(target, profileByBundle));
|
|
4880
|
+
const generateProvisioningProfileForBundle = (api, input, ctx) => Effect.gen(function* () {
|
|
4881
|
+
yield* Console.log("Generating provisioning profile via App Store Connect API...");
|
|
4882
|
+
return (yield* generateAndUploadProvisioningProfile(api, {
|
|
4883
|
+
ascApiKeyId: ctx.ascKeyId,
|
|
4884
|
+
distributionCertificateId: ctx.certId,
|
|
4885
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
4886
|
+
distributionType: ctx.distributionType
|
|
4887
|
+
})).id;
|
|
4984
4888
|
});
|
|
4985
|
-
const
|
|
4986
|
-
const
|
|
4987
|
-
if (
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
profile,
|
|
4991
|
-
|
|
4889
|
+
const resolveIosProfileId = (api, input, ctx) => Effect.gen(function* () {
|
|
4890
|
+
const matching = (yield* api.appleProvisioningProfiles.list({ urlParams: {} })).items.filter((profile) => profile.bundleIdentifier === input.bundleIdentifier && profile.distributionType === ctx.distributionType && profile.appleTeamId === ctx.cert.appleTeamId);
|
|
4891
|
+
if (matching.length === 0) return yield* generateProvisioningProfileForBundle(api, input, ctx);
|
|
4892
|
+
if (!(yield* promptConfirm(`Reuse an existing ${input.distribution} profile for ${input.bundleIdentifier}?`, { initialValue: true }))) return yield* generateProvisioningProfileForBundle(api, input, ctx);
|
|
4893
|
+
return yield* promptSelect("Select a provisioning profile", matching.map((profile) => ({
|
|
4894
|
+
value: profile.id,
|
|
4895
|
+
label: profile.profileName ?? profile.developerPortalIdentifier ?? profile.id
|
|
4992
4896
|
})));
|
|
4993
|
-
};
|
|
4994
|
-
const
|
|
4995
|
-
const
|
|
4996
|
-
const
|
|
4997
|
-
const
|
|
4998
|
-
const
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
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.`
|
|
4897
|
+
});
|
|
4898
|
+
const setupIosViaAscKey = (api, input) => Effect.gen(function* () {
|
|
4899
|
+
const { certId, cert } = yield* pickIosCertificate(api);
|
|
4900
|
+
const ascKeyId = yield* pickIosAscKey(api, cert.appleTeamId);
|
|
4901
|
+
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
4902
|
+
const profileId = yield* resolveIosProfileId(api, input, {
|
|
4903
|
+
certId,
|
|
4904
|
+
cert,
|
|
4905
|
+
ascKeyId,
|
|
4906
|
+
distributionType
|
|
5054
4907
|
});
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
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
|
-
}))
|
|
4908
|
+
yield* upsertIosBundleConfiguration(api, {
|
|
4909
|
+
projectId: input.projectId,
|
|
4910
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
4911
|
+
distributionType,
|
|
4912
|
+
appleTeamId: cert.appleTeamId,
|
|
4913
|
+
appleDistributionCertificateId: certId,
|
|
4914
|
+
appleProvisioningProfileId: profileId,
|
|
4915
|
+
ascApiKeyId: ascKeyId
|
|
5074
4916
|
});
|
|
5075
|
-
const artifactPath = yield* findIosArtifact({ exportPath });
|
|
5076
|
-
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
5077
|
-
return {
|
|
5078
|
-
artifactPath,
|
|
5079
|
-
byteSize,
|
|
5080
|
-
sha256
|
|
5081
|
-
};
|
|
5082
4917
|
});
|
|
5083
|
-
const runIosBuild = (input) => input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
5084
4918
|
|
|
5085
4919
|
//#endregion
|
|
5086
|
-
//#region src/
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
4920
|
+
//#region src/application/credentials-interactive.ts
|
|
4921
|
+
const hasTag = (cause) => typeof cause === "object" && cause !== null && "_tag" in cause;
|
|
4922
|
+
const isMissingResolveError = (cause) => hasTag(cause) && (cause._tag === "NotFound" || cause._tag === "BadRequest");
|
|
4923
|
+
const generateKeystoreInteractive = (api) => Effect.gen(function* () {
|
|
4924
|
+
const alias = yield* promptText("Key alias", { placeholder: "upload-key" });
|
|
4925
|
+
const storePassword = yield* promptPassword("Keystore password");
|
|
4926
|
+
const keyPassword = yield* promptPassword("Key password");
|
|
4927
|
+
const commonName = yield* promptText("Common name (CN)", { placeholder: "Your App" });
|
|
4928
|
+
const organization = yield* promptText("Organization (O)", { placeholder: "Your Company" });
|
|
4929
|
+
yield* Console.log("Generating keystore with keytool...");
|
|
4930
|
+
return (yield* generateAndUploadKeystore(api, {
|
|
4931
|
+
keyAlias: alias,
|
|
4932
|
+
storePassword,
|
|
4933
|
+
keyPassword,
|
|
4934
|
+
commonName,
|
|
4935
|
+
organization
|
|
4936
|
+
})).id;
|
|
5099
4937
|
});
|
|
5100
|
-
const
|
|
5101
|
-
const
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
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
|
-
};
|
|
4938
|
+
const pickExistingKeystore = (api) => Effect.gen(function* () {
|
|
4939
|
+
const keystores = yield* api.androidUploadKeystores.list();
|
|
4940
|
+
if (keystores.items.length === 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
4941
|
+
message: "No existing keystores in this organization.",
|
|
4942
|
+
hint: "Re-run and choose 'Generate new keystore'."
|
|
4943
|
+
}));
|
|
4944
|
+
return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
|
|
4945
|
+
value: item.id,
|
|
4946
|
+
label: item.keyAlias
|
|
4947
|
+
})));
|
|
5152
4948
|
});
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
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
|
-
};
|
|
4949
|
+
const resolveAndroidAppId = (api, input) => Effect.gen(function* () {
|
|
4950
|
+
const existing = (yield* api.androidApplicationIdentifiers.list({ path: { projectId: input.projectId } })).items.find((item) => item.packageName === input.applicationIdentifier);
|
|
4951
|
+
if (existing !== void 0) return existing.id;
|
|
4952
|
+
return (yield* api.androidApplicationIdentifiers.create({
|
|
4953
|
+
path: { projectId: input.projectId },
|
|
4954
|
+
payload: { packageName: input.applicationIdentifier }
|
|
4955
|
+
})).id;
|
|
5182
4956
|
});
|
|
5183
|
-
const
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
4957
|
+
const resolveAndroidKeystoreId = (api, choice) => choice === "generate" ? generateKeystoreInteractive(api) : pickExistingKeystore(api);
|
|
4958
|
+
const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
|
|
4959
|
+
yield* Console.log("");
|
|
4960
|
+
yield* Console.log(`No Android build credentials configured for ${input.applicationIdentifier}.`);
|
|
4961
|
+
const appId = yield* resolveAndroidAppId(api, input);
|
|
4962
|
+
const choice = yield* promptSelect("How would you like to provide a keystore?", [
|
|
4963
|
+
{
|
|
4964
|
+
value: "generate",
|
|
4965
|
+
label: "Generate new keystore"
|
|
4966
|
+
},
|
|
4967
|
+
{
|
|
4968
|
+
value: "existing",
|
|
4969
|
+
label: "Pick an existing keystore"
|
|
4970
|
+
},
|
|
4971
|
+
{
|
|
4972
|
+
value: "abort",
|
|
4973
|
+
label: "Abort — I'll configure it in the dashboard"
|
|
4974
|
+
}
|
|
4975
|
+
]);
|
|
4976
|
+
if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
|
|
4977
|
+
message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
|
|
4978
|
+
hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
|
|
4979
|
+
}));
|
|
4980
|
+
const keystoreId = yield* resolveAndroidKeystoreId(api, choice);
|
|
4981
|
+
yield* api.androidBuildCredentials.create({
|
|
4982
|
+
path: { applicationIdentifierId: appId },
|
|
4983
|
+
payload: {
|
|
4984
|
+
name: "Default",
|
|
4985
|
+
isDefault: true,
|
|
4986
|
+
androidUploadKeystoreId: keystoreId
|
|
4987
|
+
}
|
|
4988
|
+
});
|
|
4989
|
+
yield* Console.log("Android build credentials configured.");
|
|
5189
4990
|
});
|
|
5190
|
-
const
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
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;
|
|
4991
|
+
const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.resolve({
|
|
4992
|
+
path: { projectId: input.projectId },
|
|
4993
|
+
payload: {
|
|
4994
|
+
platform: "android",
|
|
4995
|
+
applicationIdentifier: input.applicationIdentifier
|
|
5223
4996
|
}
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
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)}` })
|
|
4997
|
+
}).pipe(Effect.asVoid);
|
|
4998
|
+
const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
4999
|
+
const mode = yield* InteractiveMode;
|
|
5000
|
+
if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
|
|
5001
|
+
message: `No Android build credentials for ${input.applicationIdentifier}.`,
|
|
5002
|
+
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
5349
5003
|
}));
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
return {
|
|
5359
|
-
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
5360
|
-
build: profiles
|
|
5361
|
-
};
|
|
5362
|
-
});
|
|
5363
|
-
const parseCli = (raw) => {
|
|
5364
|
-
const record = asRecord(raw);
|
|
5365
|
-
if (!record) return {};
|
|
5366
|
-
const version = asStringValue(record["version"]);
|
|
5367
|
-
return version === void 0 ? {} : { version };
|
|
5368
|
-
};
|
|
5369
|
-
const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
5370
|
-
return (yield* Path.Path).join(projectRoot, "eas.json");
|
|
5371
|
-
});
|
|
5372
|
-
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
5373
|
-
const fs = yield* FileSystem.FileSystem;
|
|
5374
|
-
const filePath = yield* easJsonPath(projectRoot);
|
|
5375
|
-
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.catchAll((cause) => Effect.fail(new BuildProfileError({ message: cause._tag === "SystemError" && cause.reason === "NotFound" ? `No eas.json found at ${filePath}. Create one with a "build" section.` : `Failed to read eas.json: ${cause.message}` })))));
|
|
5004
|
+
yield* setupAndroidInteractive(api, input);
|
|
5005
|
+
return yield* ensureAndroidCredentialsAvailable(api, input);
|
|
5006
|
+
})));
|
|
5007
|
+
const setupIosInteractive = (api, input) => Effect.gen(function* () {
|
|
5008
|
+
yield* Console.log("");
|
|
5009
|
+
yield* Console.log(`No iOS bundle configuration for ${input.bundleIdentifier} (${input.distribution}).`);
|
|
5010
|
+
if ((yield* chooseIosSetupPath(api)) === "apple-id") return yield* setupIosViaAppleId(api, input);
|
|
5011
|
+
return yield* setupIosViaAscKey(api, input);
|
|
5376
5012
|
});
|
|
5377
|
-
const
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
};
|
|
5384
|
-
};
|
|
5385
|
-
const mergeAndroid = (base, overlay) => {
|
|
5386
|
-
if (!base) return overlay;
|
|
5387
|
-
if (!overlay) return base;
|
|
5388
|
-
return {
|
|
5389
|
-
...base,
|
|
5390
|
-
...overlay
|
|
5391
|
-
};
|
|
5392
|
-
};
|
|
5393
|
-
const mergeEnv = (base, overlay) => {
|
|
5394
|
-
if (!base) return overlay;
|
|
5395
|
-
if (!overlay) return base;
|
|
5396
|
-
return {
|
|
5397
|
-
...base,
|
|
5398
|
-
...overlay
|
|
5399
|
-
};
|
|
5400
|
-
};
|
|
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 }
|
|
5422
|
-
};
|
|
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}.` });
|
|
5013
|
+
const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve({
|
|
5014
|
+
path: { projectId: input.projectId },
|
|
5015
|
+
payload: {
|
|
5016
|
+
platform: "ios",
|
|
5017
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
5018
|
+
distributionType: IOS_DISTRIBUTION_TO_TYPE[input.distribution]
|
|
5438
5019
|
}
|
|
5439
|
-
return chain;
|
|
5440
5020
|
});
|
|
5441
|
-
const
|
|
5442
|
-
|
|
5443
|
-
const {
|
|
5444
|
-
return
|
|
5445
|
-
}
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5021
|
+
const findBoundIosConfig = (api, input) => Effect.gen(function* () {
|
|
5022
|
+
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
5023
|
+
const match = (yield* api.iosBundleConfigurations.list({ path: { projectId: input.projectId } })).items.find((config) => config.bundleIdentifier === input.bundleIdentifier && config.distributionType === distributionType);
|
|
5024
|
+
if (match === void 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
5025
|
+
message: `iOS bundle configuration vanished while regenerating stale profile for ${input.bundleIdentifier}`,
|
|
5026
|
+
hint: "Retry; the configuration must exist before regeneration"
|
|
5027
|
+
}));
|
|
5028
|
+
return match;
|
|
5029
|
+
});
|
|
5030
|
+
const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
5031
|
+
const config = yield* findBoundIosConfig(api, input);
|
|
5032
|
+
if (config.appleDistributionCertificateId === null) return yield* new MissingCredentialsError({
|
|
5033
|
+
message: "Profile cannot be regenerated: bundle configuration is missing the distribution certificate",
|
|
5034
|
+
hint: "Re-bind credentials via `better-update credentials generate` or the dashboard"
|
|
5035
|
+
});
|
|
5036
|
+
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
5037
|
+
if (config.ascApiKeyId === null) return yield* regenerateProvisioningProfileViaAppleId(api, {
|
|
5038
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
5039
|
+
distributionCertificateId: config.appleDistributionCertificateId,
|
|
5040
|
+
distributionType,
|
|
5041
|
+
bundleConfigurationId: config.id
|
|
5042
|
+
});
|
|
5043
|
+
yield* Console.log("Regenerating provisioning profile via App Store Connect API...");
|
|
5044
|
+
const created = yield* generateAndUploadProvisioningProfile(api, {
|
|
5045
|
+
ascApiKeyId: config.ascApiKeyId,
|
|
5046
|
+
distributionCertificateId: config.appleDistributionCertificateId,
|
|
5047
|
+
bundleIdentifier: input.bundleIdentifier,
|
|
5048
|
+
distributionType
|
|
5049
|
+
});
|
|
5050
|
+
yield* api.iosBundleConfigurations.update({
|
|
5051
|
+
path: { id: config.id },
|
|
5052
|
+
payload: { appleProvisioningProfileId: created.id }
|
|
5053
|
+
});
|
|
5054
|
+
return created;
|
|
5450
5055
|
});
|
|
5056
|
+
const ensureIosCredentials = (api, input, options) => resolveIosBuildCredentials(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
5057
|
+
const mode = yield* InteractiveMode;
|
|
5058
|
+
if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
|
|
5059
|
+
message: `No iOS build credentials for ${input.bundleIdentifier} (${input.distribution}).`,
|
|
5060
|
+
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
5061
|
+
}));
|
|
5062
|
+
yield* setupIosInteractive(api, input);
|
|
5063
|
+
return yield* resolveIosBuildCredentials(api, input);
|
|
5064
|
+
})), Effect.flatMap((resolved) => Effect.gen(function* () {
|
|
5065
|
+
if (resolved.platform !== "ios" || !resolved.profileStale) return;
|
|
5066
|
+
const mode = yield* InteractiveMode;
|
|
5067
|
+
if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
|
|
5068
|
+
message: `Stale provisioning profile for ${input.bundleIdentifier}; cannot regenerate without an interactive session.`,
|
|
5069
|
+
hint: options.freezeCredentials ? "Run a build without --freeze-credentials once to refresh the profile, or run `better-update credentials regenerate-profile`." : "Run `better-update credentials regenerate-profile --bundle <id> --distribution <type>` from an interactive terminal."
|
|
5070
|
+
}));
|
|
5071
|
+
yield* Console.log(`Stale provisioning profile for ${input.bundleIdentifier} (device roster changed). Regenerating...`);
|
|
5072
|
+
yield* regenerateProvisioningProfile(api, input);
|
|
5073
|
+
})));
|
|
5451
5074
|
|
|
5452
5075
|
//#endregion
|
|
5453
|
-
//#region src/lib/
|
|
5454
|
-
const
|
|
5455
|
-
const
|
|
5456
|
-
const
|
|
5457
|
-
if (
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
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
|
-
};
|
|
5506
|
-
};
|
|
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
|
-
};
|
|
5522
|
-
};
|
|
5523
|
-
const fromEasProfile = (eas, profileName) => {
|
|
5524
|
-
const ios = toIosProfile(eas);
|
|
5525
|
-
const android = toAndroidProfile(eas);
|
|
5526
|
-
return {
|
|
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 }
|
|
5534
|
-
};
|
|
5076
|
+
//#region src/lib/ios-codesign-pbxproj.ts
|
|
5077
|
+
const loadXcodeModule$1 = () => __require("xcode");
|
|
5078
|
+
const findXcodeProjectDir$1 = (iosDir) => Effect.gen(function* () {
|
|
5079
|
+
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"));
|
|
5080
|
+
if (!projectDir) return yield* new XcodeProjectError({ message: `No .xcodeproj directory found under ${iosDir}.` });
|
|
5081
|
+
return path.join(iosDir, projectDir);
|
|
5082
|
+
});
|
|
5083
|
+
const parseProject$1 = (pbxprojPath) => Effect.try({
|
|
5084
|
+
try: () => loadXcodeModule$1().project(pbxprojPath).parseSync(),
|
|
5085
|
+
catch: (cause) => new XcodeProjectError({ message: `Failed to parse ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
5086
|
+
});
|
|
5087
|
+
/**
|
|
5088
|
+
* Always wrap a value in double quotes for safe pbxproj serialization. The
|
|
5089
|
+
* `xcode` writer emits values verbatim (e.g. `KEY = %s;`), so any string with
|
|
5090
|
+
* spaces, brackets or non-identifier characters needs explicit quoting.
|
|
5091
|
+
*/
|
|
5092
|
+
const quote = (value) => `"${value.replaceAll("\"", String.raw`\"`)}"`;
|
|
5093
|
+
const SDK_CONDITIONAL_IDENTITY_KEYS = ["\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"", "CODE_SIGN_IDENTITY[sdk=iphoneos*]"];
|
|
5094
|
+
const mutateConfig = (project, configUuid, settings) => {
|
|
5095
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
5096
|
+
if (!cfg || typeof cfg === "string") return false;
|
|
5097
|
+
cfg.buildSettings["CODE_SIGN_STYLE"] = "Manual";
|
|
5098
|
+
cfg.buildSettings["DEVELOPMENT_TEAM"] = quote(settings.teamId);
|
|
5099
|
+
cfg.buildSettings["CODE_SIGN_IDENTITY"] = quote(settings.signingIdentity);
|
|
5100
|
+
cfg.buildSettings["PROVISIONING_PROFILE_SPECIFIER"] = quote(settings.profileSpecifier);
|
|
5101
|
+
delete cfg.buildSettings["PROVISIONING_PROFILE"];
|
|
5102
|
+
for (const key of SDK_CONDITIONAL_IDENTITY_KEYS) delete cfg.buildSettings[key];
|
|
5103
|
+
return true;
|
|
5535
5104
|
};
|
|
5536
|
-
|
|
5537
|
-
|
|
5105
|
+
/**
|
|
5106
|
+
* Write `CODE_SIGN_STYLE=Manual`, `DEVELOPMENT_TEAM`, `CODE_SIGN_IDENTITY`, and
|
|
5107
|
+
* `PROVISIONING_PROFILE_SPECIFIER` into the specified XCBuildConfiguration
|
|
5108
|
+
* entries of the project under `iosDir`, then serialize back to disk.
|
|
5109
|
+
*
|
|
5110
|
+
* Only mutates the main app project — `Pods.xcodeproj` is left untouched. The
|
|
5111
|
+
* caller is responsible for ensuring each entry's `buildConfigurationUuids`
|
|
5112
|
+
* only includes configurations that belong to a signed target (see
|
|
5113
|
+
* `discoverSignedTargets`).
|
|
5114
|
+
*/
|
|
5115
|
+
const applyTargetSigning = (options) => Effect.gen(function* () {
|
|
5116
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5117
|
+
const projectDir = yield* findXcodeProjectDir$1(options.iosDir);
|
|
5118
|
+
const pbxprojPath = path.join(projectDir, "project.pbxproj");
|
|
5119
|
+
const project = yield* parseProject$1(pbxprojPath);
|
|
5120
|
+
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}.` });
|
|
5121
|
+
const serialized = yield* Effect.try({
|
|
5122
|
+
try: () => project.writeSync(),
|
|
5123
|
+
catch: (cause) => new XcodeProjectError({ message: `Failed to serialize ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
5124
|
+
});
|
|
5125
|
+
yield* fs.writeFileString(pbxprojPath, serialized).pipe(Effect.mapError((cause) => new XcodeProjectError({ message: `Failed to write ${pbxprojPath}: ${String(cause)}` })));
|
|
5538
5126
|
});
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5127
|
+
|
|
5128
|
+
//#endregion
|
|
5129
|
+
//#region src/lib/ios-export-options.ts
|
|
5130
|
+
const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
5131
|
+
const boolTag = (value) => value ? "<true/>" : "<false/>";
|
|
5132
|
+
/**
|
|
5133
|
+
* Render an Xcode `ExportOptions.plist` for `xcodebuild -exportArchive`.
|
|
5134
|
+
*
|
|
5135
|
+
* - `signingStyle` is always `manual` (ephemeral keychain + downloaded profile)
|
|
5136
|
+
* - `uploadSymbols` is emitted only for `app-store` exports
|
|
5137
|
+
* - `provisioningProfiles` dict maps each bundleId → profile name (one entry
|
|
5138
|
+
* per signed target: main app + any extensions like notification service)
|
|
5139
|
+
* - `compileBitcode` defaults to `false`
|
|
5140
|
+
*/
|
|
5141
|
+
const renderExportOptionsPlist = ({ method, teamId, provisioningProfiles, compileBitcode = false }) => {
|
|
5142
|
+
const lines = [
|
|
5143
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
5144
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
5145
|
+
"<plist version=\"1.0\">",
|
|
5146
|
+
"<dict>",
|
|
5147
|
+
" <key>method</key>",
|
|
5148
|
+
`\t<string>${escapeXml(method)}</string>`,
|
|
5149
|
+
" <key>teamID</key>",
|
|
5150
|
+
`\t<string>${escapeXml(teamId)}</string>`,
|
|
5151
|
+
" <key>signingStyle</key>",
|
|
5152
|
+
" <string>manual</string>",
|
|
5153
|
+
" <key>compileBitcode</key>",
|
|
5154
|
+
`\t${boolTag(compileBitcode)}`,
|
|
5155
|
+
" <key>provisioningProfiles</key>",
|
|
5156
|
+
" <dict>"
|
|
5157
|
+
];
|
|
5158
|
+
for (const { bundleId, profileName } of provisioningProfiles) lines.push(`\t\t<key>${escapeXml(bundleId)}</key>`, `\t\t<string>${escapeXml(profileName)}</string>`);
|
|
5159
|
+
lines.push(" </dict>");
|
|
5160
|
+
if (method === "app-store") lines.push(" <key>uploadSymbols</key>", " <true/>");
|
|
5161
|
+
lines.push("</dict>", "</plist>", "");
|
|
5162
|
+
return lines.join("\n");
|
|
5163
|
+
};
|
|
5164
|
+
|
|
5165
|
+
//#endregion
|
|
5166
|
+
//#region src/lib/ios-keychain.ts
|
|
5167
|
+
const runOrFail = (cmd, step) => Command.string(cmd).pipe(Effect.mapError((cause) => new KeychainError({ message: `keychain ${step} failed: ${String(cause)}` })));
|
|
5168
|
+
const listCurrentKeychains = Effect.gen(function* () {
|
|
5169
|
+
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);
|
|
5542
5170
|
});
|
|
5543
|
-
const
|
|
5544
|
-
|
|
5545
|
-
const
|
|
5546
|
-
|
|
5171
|
+
const parseSigningIdentity = (output) => {
|
|
5172
|
+
const lines = output.split("\n");
|
|
5173
|
+
for (const line of lines) {
|
|
5174
|
+
const match = /"([^"]+)"/u.exec(line);
|
|
5175
|
+
if (match?.[1]) return match[1];
|
|
5176
|
+
}
|
|
5177
|
+
};
|
|
5178
|
+
/**
|
|
5179
|
+
* Acquire an ephemeral macOS keychain, import a `.p12` into it, add it to the
|
|
5180
|
+
* user search list, and tear it all down on scope close. The keychain name is
|
|
5181
|
+
* namespaced as `better-update-<uuid>` and lives in `$tempDir`, so cleanup is
|
|
5182
|
+
* guaranteed under all termination paths.
|
|
5183
|
+
*/
|
|
5184
|
+
const acquireKeychain = ({ tempDir, p12Path, p12Password }) => {
|
|
5185
|
+
const keychainName = `better-update-${randomUUID()}.keychain-db`;
|
|
5186
|
+
const keychainPath = path.join(tempDir, keychainName);
|
|
5187
|
+
const keychainPassword = randomBytes(32).toString("hex");
|
|
5188
|
+
return Effect.acquireRelease(Effect.gen(function* () {
|
|
5189
|
+
const priorKeychains = yield* listCurrentKeychains;
|
|
5190
|
+
yield* runOrFail(Command.make("security", "create-keychain", "-p", keychainPassword, keychainPath), "create-keychain");
|
|
5191
|
+
yield* runOrFail(Command.make("security", "unlock-keychain", "-p", keychainPassword, keychainPath), "unlock-keychain");
|
|
5192
|
+
yield* runOrFail(Command.make("security", "set-keychain-settings", "-t", "3600", "-l", keychainPath), "set-keychain-settings");
|
|
5193
|
+
yield* runOrFail(Command.make("security", "import", p12Path, "-k", keychainPath, "-P", p12Password, "-T", "/usr/bin/codesign"), "import");
|
|
5194
|
+
yield* runOrFail(Command.make("security", "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", keychainPassword, keychainPath), "set-key-partition-list");
|
|
5195
|
+
yield* runOrFail(Command.make("security", "list-keychains", "-d", "user", "-s", keychainPath, ...priorKeychains), "list-keychains -s (add)");
|
|
5196
|
+
const signingIdentity = parseSigningIdentity(yield* runOrFail(Command.make("security", "find-identity", "-v", "-p", "codesigning", keychainPath), "find-identity"));
|
|
5197
|
+
if (!signingIdentity) return yield* new KeychainError({ message: "No code signing identity found after importing .p12 into ephemeral keychain." });
|
|
5198
|
+
return {
|
|
5199
|
+
handle: {
|
|
5200
|
+
keychainName,
|
|
5201
|
+
keychainPath,
|
|
5202
|
+
signingIdentity
|
|
5203
|
+
},
|
|
5204
|
+
priorKeychains
|
|
5205
|
+
};
|
|
5206
|
+
}), ({ priorKeychains }) => Effect.gen(function* () {
|
|
5207
|
+
yield* Command.string(Command.make("security", "list-keychains", "-d", "user", "-s", ...priorKeychains)).pipe(Effect.catchAll(() => Effect.void));
|
|
5208
|
+
yield* Command.string(Command.make("security", "delete-keychain", keychainPath)).pipe(Effect.catchAll(() => Effect.void));
|
|
5209
|
+
})).pipe(Effect.map(({ handle }) => handle));
|
|
5547
5210
|
};
|
|
5548
5211
|
|
|
5549
5212
|
//#endregion
|
|
5550
|
-
//#region src/lib/
|
|
5213
|
+
//#region src/lib/plist.ts
|
|
5214
|
+
const plist = typeof plistMod.parse === "function" ? plistMod : plistMod.default;
|
|
5551
5215
|
/**
|
|
5552
|
-
*
|
|
5553
|
-
*
|
|
5554
|
-
* pod install rebuild — leave to the user).
|
|
5216
|
+
* Parse an XML plist string into a typed object.
|
|
5217
|
+
* Throws on malformed XML — callers should wrap in Effect.try.
|
|
5555
5218
|
*/
|
|
5556
|
-
const
|
|
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(", ")}`);
|
|
5574
|
-
});
|
|
5575
|
-
|
|
5576
|
-
//#endregion
|
|
5577
|
-
//#region src/lib/env-exporter.ts
|
|
5578
|
-
const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
|
|
5219
|
+
const parsePlistXml = (xml) => plist.parse(xml);
|
|
5579
5220
|
/**
|
|
5580
|
-
*
|
|
5581
|
-
*
|
|
5221
|
+
* Parse a binary plist buffer into a typed object.
|
|
5222
|
+
* Uses bplist-parser for Apple's binary plist format.
|
|
5582
5223
|
*/
|
|
5583
|
-
const
|
|
5584
|
-
const
|
|
5585
|
-
|
|
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)}` })));
|
|
5224
|
+
const parsePlistBinary = (buffer) => {
|
|
5225
|
+
const [result] = __require("bplist-parser").parseBuffer(buffer);
|
|
5226
|
+
return result;
|
|
5590
5227
|
};
|
|
5228
|
+
const BPLIST_MAGIC = Buffer.from("bplist00");
|
|
5229
|
+
/**
|
|
5230
|
+
* Auto-detect plist format (binary vs XML) and parse accordingly.
|
|
5231
|
+
*/
|
|
5232
|
+
const parsePlist = (data) => data.subarray(0, 8).equals(BPLIST_MAGIC) ? parsePlistBinary(data) : parsePlistXml(data.toString("utf8"));
|
|
5591
5233
|
|
|
5592
5234
|
//#endregion
|
|
5593
|
-
//#region src/lib/
|
|
5594
|
-
const
|
|
5235
|
+
//#region src/lib/ios-provisioning.ts
|
|
5236
|
+
const getString = (obj, key) => {
|
|
5237
|
+
const value = obj[key];
|
|
5238
|
+
return typeof value === "string" ? value : void 0;
|
|
5239
|
+
};
|
|
5240
|
+
const getFirstArrayString = (obj, key) => {
|
|
5241
|
+
const value = obj[key];
|
|
5242
|
+
if (Array.isArray(value) && typeof value[0] === "string") return value[0];
|
|
5243
|
+
};
|
|
5595
5244
|
/**
|
|
5596
|
-
*
|
|
5597
|
-
*
|
|
5598
|
-
*
|
|
5599
|
-
* not a requirement.
|
|
5245
|
+
* Extract `UUID`, `Name`, and the first `TeamIdentifier` from the XML plist
|
|
5246
|
+
* output of `security cms -D -i <path>`. Returns `ProvisioningError` when any
|
|
5247
|
+
* of the three fields are missing.
|
|
5600
5248
|
*/
|
|
5601
|
-
const
|
|
5602
|
-
const
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5249
|
+
const extractProvisioningInfo = (plistXml) => Effect.gen(function* () {
|
|
5250
|
+
const parsed = yield* Effect.try({
|
|
5251
|
+
try: () => parsePlistXml(plistXml),
|
|
5252
|
+
catch: (error) => new ProvisioningError({ message: `Failed to parse provisioning profile plist: ${error instanceof Error ? error.message : String(error)}` })
|
|
5253
|
+
});
|
|
5254
|
+
const uuid = getString(parsed, "UUID");
|
|
5255
|
+
const name = getString(parsed, "Name");
|
|
5256
|
+
const teamId = getFirstArrayString(parsed, "TeamIdentifier");
|
|
5257
|
+
if (!uuid || !name || !teamId) return yield* new ProvisioningError({ message: `Failed to parse provisioning profile: missing ${uuid ? "" : "UUID "}${name ? "" : "Name "}${teamId ? "" : "TeamIdentifier "}`.trim() });
|
|
5608
5258
|
return {
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
dirty: status.trim().length > 0
|
|
5259
|
+
uuid,
|
|
5260
|
+
name,
|
|
5261
|
+
teamId
|
|
5613
5262
|
};
|
|
5614
5263
|
});
|
|
5615
|
-
|
|
5616
|
-
//#endregion
|
|
5617
|
-
//#region src/lib/gradle-config.ts
|
|
5264
|
+
const userProvisioningProfilesDir = () => path.join(os.homedir(), "Library", "MobileDevice", "Provisioning Profiles");
|
|
5618
5265
|
/**
|
|
5619
|
-
*
|
|
5620
|
-
*
|
|
5621
|
-
*
|
|
5622
|
-
*
|
|
5623
|
-
*
|
|
5624
|
-
*
|
|
5625
|
-
* Informational only — never blocks the build.
|
|
5266
|
+
* Scoped installation of a provisioning profile: parses its metadata via
|
|
5267
|
+
* `security cms -D -i`, copies it into `~/Library/MobileDevice/Provisioning Profiles`
|
|
5268
|
+
* under `<uuid>.mobileprovision`, and removes the copy on scope close — but
|
|
5269
|
+
* only if we installed it. If the target file already existed when we arrived
|
|
5270
|
+
* (e.g., Xcode had it), we leave both the file and the contents untouched.
|
|
5626
5271
|
*/
|
|
5627
|
-
const
|
|
5272
|
+
const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Effect.gen(function* () {
|
|
5628
5273
|
const fs = yield* FileSystem.FileSystem;
|
|
5629
|
-
const
|
|
5630
|
-
const
|
|
5631
|
-
const
|
|
5632
|
-
|
|
5633
|
-
if (
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
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)));
|
|
5643
|
-
});
|
|
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
|
+
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)}` }))));
|
|
5275
|
+
const targetDir = userProvisioningProfilesDir();
|
|
5276
|
+
const installedPath = path.join(targetDir, `${info.uuid}.mobileprovision`);
|
|
5277
|
+
yield* fs.makeDirectory(targetDir, { recursive: true }).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to create provisioning profiles dir: ${String(cause)}` })));
|
|
5278
|
+
if (yield* fs.exists(installedPath).pipe(Effect.orElseSucceed(() => false))) return {
|
|
5279
|
+
...info,
|
|
5280
|
+
installedPath,
|
|
5281
|
+
ownsInstallation: false
|
|
5282
|
+
};
|
|
5283
|
+
yield* fs.copyFile(profilePath, installedPath).pipe(Effect.catchAll((cause) => new ProvisioningError({ message: `Failed to copy provisioning profile into ${installedPath}: ${String(cause)}` })));
|
|
5666
5284
|
return {
|
|
5667
|
-
...
|
|
5668
|
-
|
|
5669
|
-
|
|
5285
|
+
...info,
|
|
5286
|
+
installedPath,
|
|
5287
|
+
ownsInstallation: true
|
|
5670
5288
|
};
|
|
5671
|
-
}
|
|
5672
|
-
|
|
5289
|
+
}), (acquired) => Effect.gen(function* () {
|
|
5290
|
+
if (!acquired.ownsInstallation) return;
|
|
5291
|
+
yield* (yield* FileSystem.FileSystem).remove(acquired.installedPath).pipe(Effect.catchAll(() => Effect.void));
|
|
5292
|
+
})).pipe(Effect.map(({ uuid, name, teamId, installedPath }) => ({
|
|
5293
|
+
uuid,
|
|
5294
|
+
name,
|
|
5295
|
+
teamId,
|
|
5296
|
+
installedPath
|
|
5297
|
+
})));
|
|
5673
5298
|
|
|
5674
5299
|
//#endregion
|
|
5675
|
-
//#region src/lib/
|
|
5676
|
-
const
|
|
5677
|
-
const
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
return
|
|
5684
|
-
|
|
5300
|
+
//#region src/lib/post-build-validation.ts
|
|
5301
|
+
const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Effect.gen(function* () {
|
|
5302
|
+
const bundleId = yield* readBundleId(bundleDir).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
5303
|
+
if (!bundleId) return {
|
|
5304
|
+
bundleId: void 0,
|
|
5305
|
+
warnings: [`Missing CFBundleIdentifier in Info.plist at ${bundleDir}`]
|
|
5306
|
+
};
|
|
5307
|
+
const expected = expectedByBundleId.get(bundleId);
|
|
5308
|
+
if (!expected) return {
|
|
5309
|
+
bundleId,
|
|
5310
|
+
warnings: [`Unexpected signed bundle "${bundleId}" found in archive at ${bundleDir}`]
|
|
5311
|
+
};
|
|
5312
|
+
return {
|
|
5313
|
+
bundleId,
|
|
5314
|
+
warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.catchAll(() => Effect.succeed([])))
|
|
5315
|
+
};
|
|
5316
|
+
});
|
|
5317
|
+
const validateIosBuild = (params) => Effect.gen(function* () {
|
|
5318
|
+
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
5319
|
+
if (!appDir) return {
|
|
5320
|
+
passed: false,
|
|
5321
|
+
warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
|
|
5322
|
+
};
|
|
5323
|
+
const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.catchAll(() => Effect.succeed([appDir])));
|
|
5324
|
+
const expectedByBundleId = new Map(params.expectedTargets.map((target) => [target.bundleId, target]));
|
|
5325
|
+
const perBundle = yield* Effect.forEach(bundleDirs, (bundleDir) => validateOneBundle(bundleDir, expectedByBundleId, params.expectedTeamId));
|
|
5326
|
+
const warnings = perBundle.flatMap((entry) => [...entry.warnings]);
|
|
5327
|
+
const validatedBundleIds = new Set(perBundle.map((entry) => entry.bundleId).filter((id) => id !== void 0));
|
|
5328
|
+
for (const expected of params.expectedTargets) if (!validatedBundleIds.has(expected.bundleId)) warnings.push(`Expected signed target "${expected.bundleId}" was not found in the archive.`);
|
|
5329
|
+
if (warnings.length > 0) {
|
|
5330
|
+
yield* Console.warn("Post-build validation warnings:");
|
|
5331
|
+
for (const warning of warnings) yield* Console.warn(` - ${warning}`);
|
|
5332
|
+
}
|
|
5333
|
+
return {
|
|
5334
|
+
passed: warnings.length === 0,
|
|
5335
|
+
warnings
|
|
5336
|
+
};
|
|
5337
|
+
});
|
|
5338
|
+
const findAppDirectory$1 = (archivePath) => Effect.gen(function* () {
|
|
5339
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5340
|
+
const productsDir = path.join(archivePath, "Products", "Applications");
|
|
5341
|
+
const appEntry = (yield* fs.readDirectory(productsDir)).find((entry) => entry.endsWith(".app"));
|
|
5342
|
+
if (!appEntry) return yield* Effect.fail("No .app found");
|
|
5343
|
+
return path.join(productsDir, appEntry);
|
|
5344
|
+
});
|
|
5685
5345
|
/**
|
|
5686
|
-
*
|
|
5687
|
-
*
|
|
5688
|
-
*
|
|
5689
|
-
* disallowed.
|
|
5346
|
+
* Return the main `.app` plus every `.appex` extension under `<app>/PlugIns/`.
|
|
5347
|
+
* Each returned path is a signed bundle that should carry its own embedded
|
|
5348
|
+
* provisioning profile + Info.plist with CFBundleIdentifier.
|
|
5690
5349
|
*/
|
|
5691
|
-
const
|
|
5692
|
-
|
|
5693
|
-
const
|
|
5694
|
-
if (
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5350
|
+
const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
|
|
5351
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5352
|
+
const plugInsDir = path.join(appDir, "PlugIns");
|
|
5353
|
+
if (!(yield* fs.exists(plugInsDir).pipe(Effect.catchAll(() => Effect.succeed(false))))) return [appDir];
|
|
5354
|
+
return [appDir, ...(yield* fs.readDirectory(plugInsDir)).filter((entry) => entry.endsWith(".appex")).map((entry) => path.join(plugInsDir, entry))];
|
|
5355
|
+
});
|
|
5356
|
+
const readBundleId = (bundleDir) => Effect.gen(function* () {
|
|
5357
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5358
|
+
const plistPath = path.join(bundleDir, "Info.plist");
|
|
5359
|
+
const data = yield* fs.readFile(plistPath);
|
|
5360
|
+
const bundleId = parsePlist(Buffer.from(data))["CFBundleIdentifier"];
|
|
5361
|
+
return typeof bundleId === "string" ? bundleId : void 0;
|
|
5362
|
+
});
|
|
5363
|
+
const validateEmbeddedProfile = (bundleDir, expectedUuid, expectedTeamId, bundleId) => Effect.gen(function* () {
|
|
5364
|
+
const warnings = [];
|
|
5365
|
+
const profilePath = path.join(bundleDir, "embedded.mobileprovision");
|
|
5366
|
+
const parsed = parsePlistXml(yield* Command.string(Command.make("security", "cms", "-D", "-i", profilePath)));
|
|
5367
|
+
const actualUuid = parsed["UUID"];
|
|
5368
|
+
if (typeof actualUuid === "string" && actualUuid !== expectedUuid) warnings.push(`[${bundleId}] Profile UUID mismatch: expected "${expectedUuid}", got "${actualUuid}"`);
|
|
5369
|
+
const teamIdentifiers = parsed["TeamIdentifier"];
|
|
5370
|
+
if (Array.isArray(teamIdentifiers)) {
|
|
5371
|
+
const [actualTeamId] = teamIdentifiers;
|
|
5372
|
+
if (typeof actualTeamId === "string" && actualTeamId !== expectedTeamId) warnings.push(`[${bundleId}] Team ID mismatch: expected "${expectedTeamId}", got "${actualTeamId}"`);
|
|
5699
5373
|
}
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
label: entry
|
|
5704
|
-
})));
|
|
5374
|
+
const expirationDate = parsed["ExpirationDate"];
|
|
5375
|
+
if (expirationDate instanceof Date && expirationDate.getTime() < Date.now()) warnings.push(`[${bundleId}] Embedded provisioning profile expired on ${expirationDate.toISOString()}`);
|
|
5376
|
+
return warnings;
|
|
5705
5377
|
});
|
|
5706
5378
|
|
|
5707
5379
|
//#endregion
|
|
5708
|
-
//#region src/lib/
|
|
5709
|
-
|
|
5710
|
-
const
|
|
5380
|
+
//#region src/lib/xcode-targets.ts
|
|
5381
|
+
/** Product types whose targets require code-signing with a provisioning profile. */
|
|
5382
|
+
const SIGNED_PRODUCT_TYPES = new Set([
|
|
5383
|
+
"com.apple.product-type.application",
|
|
5384
|
+
"com.apple.product-type.app-extension",
|
|
5385
|
+
"com.apple.product-type.messages-extension",
|
|
5386
|
+
"com.apple.product-type.tv-app-extension",
|
|
5387
|
+
"com.apple.product-type.watchapp2",
|
|
5388
|
+
"com.apple.product-type.watchkit2-extension"
|
|
5389
|
+
]);
|
|
5390
|
+
const loadXcodeModule = () => __require("xcode");
|
|
5711
5391
|
/**
|
|
5712
|
-
*
|
|
5713
|
-
*
|
|
5714
|
-
*
|
|
5392
|
+
* Strip surrounding quotes from a pbxproj string value. `xcode` returns values
|
|
5393
|
+
* verbatim from the project file, so identifiers like productType are usually
|
|
5394
|
+
* wrapped in double quotes (e.g. `"com.apple.product-type.application"`).
|
|
5715
5395
|
*/
|
|
5716
|
-
const
|
|
5717
|
-
|
|
5718
|
-
const
|
|
5719
|
-
if (
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5396
|
+
const unquote$1 = (value) => value.length >= 2 && value.startsWith("\"") && value.endsWith("\"") ? value.slice(1, -1) : value;
|
|
5397
|
+
const findXcodeProjectDir = (iosDir) => Effect.gen(function* () {
|
|
5398
|
+
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"));
|
|
5399
|
+
if (!projectDir) return yield* new XcodeProjectError({ message: `No .xcodeproj directory found under ${iosDir}. Did "expo prebuild" run?` });
|
|
5400
|
+
return path.join(iosDir, projectDir);
|
|
5401
|
+
});
|
|
5402
|
+
const parseProject = (pbxprojPath) => Effect.try({
|
|
5403
|
+
try: () => loadXcodeModule().project(pbxprojPath).parseSync(),
|
|
5404
|
+
catch: (cause) => new XcodeProjectError({ message: `Failed to parse ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
5405
|
+
});
|
|
5406
|
+
const collectConfigUuidsForTarget = (project, target, configurationName) => {
|
|
5407
|
+
const configList = project.pbxXCConfigurationList()[target.buildConfigurationList];
|
|
5408
|
+
if (!configList || typeof configList === "string") return [];
|
|
5409
|
+
const buildConfigSection = project.pbxXCBuildConfigurationSection();
|
|
5410
|
+
return configList.buildConfigurations.map((entry) => entry.value).filter((uuid) => {
|
|
5411
|
+
const cfg = buildConfigSection[uuid];
|
|
5412
|
+
if (!cfg || typeof cfg === "string") return false;
|
|
5413
|
+
return unquote$1(cfg.name) === configurationName;
|
|
5414
|
+
});
|
|
5415
|
+
};
|
|
5416
|
+
const extractBundleIdForConfig = (project, configUuid) => {
|
|
5417
|
+
const cfg = project.pbxXCBuildConfigurationSection()[configUuid];
|
|
5418
|
+
if (!cfg || typeof cfg === "string") return;
|
|
5419
|
+
const raw = cfg.buildSettings["PRODUCT_BUNDLE_IDENTIFIER"];
|
|
5420
|
+
if (typeof raw !== "string") return;
|
|
5421
|
+
return unquote$1(raw);
|
|
5422
|
+
};
|
|
5423
|
+
const collectSignedTargets = (project, pbxprojPath, configurationName) => Effect.gen(function* () {
|
|
5424
|
+
const results = [];
|
|
5425
|
+
const nativeTargets = project.pbxNativeTargetSection();
|
|
5426
|
+
for (const [uuid, entry] of Object.entries(nativeTargets)) {
|
|
5427
|
+
if (uuid.endsWith("_comment") || typeof entry === "string") continue;
|
|
5428
|
+
const productType = unquote$1(entry.productType);
|
|
5429
|
+
if (!SIGNED_PRODUCT_TYPES.has(productType)) continue;
|
|
5430
|
+
const configUuids = collectConfigUuidsForTarget(project, entry, configurationName);
|
|
5431
|
+
const [firstConfigUuid] = configUuids;
|
|
5432
|
+
if (!firstConfigUuid) return yield* new XcodeProjectError({ message: `Target "${unquote$1(entry.name)}" has no "${configurationName}" build configuration in ${pbxprojPath}.` });
|
|
5433
|
+
const bundleId = extractBundleIdForConfig(project, firstConfigUuid);
|
|
5434
|
+
if (!bundleId) return yield* new XcodeProjectError({ message: `Target "${unquote$1(entry.name)}" is missing PRODUCT_BUNDLE_IDENTIFIER in the "${configurationName}" configuration.` });
|
|
5435
|
+
results.push({
|
|
5436
|
+
targetName: unquote$1(entry.name),
|
|
5437
|
+
bundleId,
|
|
5438
|
+
productType,
|
|
5439
|
+
buildConfigurationUuids: configUuids
|
|
5440
|
+
});
|
|
5726
5441
|
}
|
|
5727
|
-
|
|
5442
|
+
return results;
|
|
5443
|
+
});
|
|
5444
|
+
/**
|
|
5445
|
+
* Enumerate code-signed native targets (main app + extensions) declared in the
|
|
5446
|
+
* single `.xcodeproj` under `iosDir`, restricted to a given build configuration
|
|
5447
|
+
* (e.g. "Release"). Pod targets and other library product types are excluded.
|
|
5448
|
+
*
|
|
5449
|
+
* The returned `buildConfigurationUuids` list is the set of XCBuildConfiguration
|
|
5450
|
+
* UUIDs that belong to this target *and* match `configurationName` — the
|
|
5451
|
+
* per-target signing mutator writes settings into exactly those configurations.
|
|
5452
|
+
*/
|
|
5453
|
+
const discoverSignedTargets = (options) => Effect.gen(function* () {
|
|
5454
|
+
const projectDir = yield* findXcodeProjectDir(options.iosDir);
|
|
5455
|
+
const pbxprojPath = path.join(projectDir, "project.pbxproj");
|
|
5456
|
+
const results = yield* collectSignedTargets(yield* parseProject(pbxprojPath), pbxprojPath, options.configurationName);
|
|
5457
|
+
if (results.length === 0) return yield* new XcodeProjectError({ message: `No signed native targets found in ${pbxprojPath} for configuration "${options.configurationName}".` });
|
|
5458
|
+
return results;
|
|
5728
5459
|
});
|
|
5729
5460
|
|
|
5730
5461
|
//#endregion
|
|
5731
|
-
//#region src/lib/
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
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"];
|
|
5462
|
+
//#region src/lib/xcpretty-formatter.ts
|
|
5463
|
+
/**
|
|
5464
|
+
* Create a stateful xcodebuild output formatter backed by `@expo/xcpretty`.
|
|
5465
|
+
* Each `pipe(line)` call may return zero or more formatted lines — zero means
|
|
5466
|
+
* the line was suppressed (e.g., intermediate compiler invocations).
|
|
5467
|
+
*/
|
|
5468
|
+
const createXcodebuildFormatter = (projectRoot) => {
|
|
5469
|
+
const formatter = ExpoRunFormatter.create(projectRoot);
|
|
5744
5470
|
return {
|
|
5745
|
-
|
|
5746
|
-
|
|
5471
|
+
pipe: (line) => formatter.pipe(line),
|
|
5472
|
+
getBuildSummary: () => formatter.getBuildSummary()
|
|
5747
5473
|
};
|
|
5748
|
-
}
|
|
5474
|
+
};
|
|
5749
5475
|
|
|
5750
5476
|
//#endregion
|
|
5751
|
-
//#region src/
|
|
5752
|
-
const
|
|
5753
|
-
|
|
5754
|
-
if (
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5477
|
+
//#region src/commands/build/ios.ts
|
|
5478
|
+
const findXcworkspace = (iosDir) => Effect.gen(function* () {
|
|
5479
|
+
const workspace = (yield* (yield* FileSystem.FileSystem).readDirectory(iosDir)).find((entry) => entry.endsWith(".xcworkspace"));
|
|
5480
|
+
if (!workspace) return yield* new BuildFailedError({
|
|
5481
|
+
step: "detect xcworkspace",
|
|
5482
|
+
exitCode: 1,
|
|
5483
|
+
message: `No .xcworkspace found under ${iosDir}. Did "pod install" run?`
|
|
5484
|
+
});
|
|
5485
|
+
return workspace;
|
|
5486
|
+
});
|
|
5487
|
+
const prebuildAndPods = (params) => Effect.gen(function* () {
|
|
5488
|
+
yield* runStep(Command.make("bunx", "expo", "prebuild", "--platform", "ios", "--clean").pipe(Command.workingDirectory(params.projectRoot), Command.env(params.commandEnv)), "expo prebuild ios");
|
|
5489
|
+
yield* runStep(Command.make("pod", "install").pipe(Command.workingDirectory(params.iosDir), Command.env(params.commandEnv)), "pod install");
|
|
5490
|
+
});
|
|
5491
|
+
const findAppDirectory = (root) => Effect.gen(function* () {
|
|
5492
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5493
|
+
const stack = [root];
|
|
5494
|
+
let depth = 0;
|
|
5495
|
+
while (stack.length > 0 && depth < 6) {
|
|
5496
|
+
const layer = stack.splice(0);
|
|
5497
|
+
depth += 1;
|
|
5498
|
+
for (const dir of layer) {
|
|
5499
|
+
const entries = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
|
|
5500
|
+
for (const entry of entries) {
|
|
5501
|
+
const full = path.join(dir, entry);
|
|
5502
|
+
if (entry.endsWith(".app")) return full;
|
|
5503
|
+
const stat = yield* fs.stat(full).pipe(Effect.option);
|
|
5504
|
+
if (stat._tag === "Some" && stat.value.type === "Directory") stack.push(full);
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5759
5507
|
}
|
|
5760
|
-
|
|
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".` });
|
|
5508
|
+
return yield* new ArtifactNotFoundError({ message: `No .app bundle found under "${root}".` });
|
|
5763
5509
|
});
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
const
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
const
|
|
5782
|
-
const
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5510
|
+
const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
5511
|
+
const { projectRoot, iosProfile, envVars, tempDir } = input;
|
|
5512
|
+
const runtime = yield* CliRuntime;
|
|
5513
|
+
const iosDir = path.join(projectRoot, "ios");
|
|
5514
|
+
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
5515
|
+
yield* prebuildAndPods({
|
|
5516
|
+
projectRoot,
|
|
5517
|
+
iosDir,
|
|
5518
|
+
commandEnv
|
|
5519
|
+
});
|
|
5520
|
+
const workspaceFilename = yield* findXcworkspace(iosDir);
|
|
5521
|
+
const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
|
|
5522
|
+
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
5523
|
+
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
5524
|
+
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));
|
|
5525
|
+
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
5526
|
+
yield* formatter ? runStepFormatted(buildCmd, "xcodebuild build (simulator)", formatter) : runStep(buildCmd, "xcodebuild build (simulator)");
|
|
5527
|
+
const appDir = yield* findAppDirectory(path.join(derivedDataPath, "Build", "Products", `${configuration}-iphonesimulator`));
|
|
5528
|
+
const archiveName = `${path.basename(appDir, ".app")}-simulator.tar.gz`;
|
|
5529
|
+
const archivePath = path.join(tempDir, archiveName);
|
|
5530
|
+
yield* runStep(Command.make("tar", "-czf", archivePath, "-C", path.dirname(appDir), path.basename(appDir)).pipe(Command.env(commandEnv)), "tar simulator .app");
|
|
5531
|
+
const { sha256, byteSize } = yield* sha256File(archivePath);
|
|
5532
|
+
return {
|
|
5533
|
+
artifactPath: archivePath,
|
|
5534
|
+
byteSize,
|
|
5535
|
+
sha256
|
|
5536
|
+
};
|
|
5788
5537
|
});
|
|
5789
|
-
const
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5538
|
+
const ensurePerTargetCredentials = (params) => Effect.forEach(params.signedTargets, (target) => ensureIosCredentials(params.api, {
|
|
5539
|
+
projectId: params.projectId,
|
|
5540
|
+
bundleIdentifier: target.bundleId,
|
|
5541
|
+
distribution: params.distribution
|
|
5542
|
+
}, { freezeCredentials: params.freezeCredentials }), { concurrency: 1 });
|
|
5543
|
+
const fetchAllCredentials = (params) => params.input.credentialsSource === "local" ? loadLocalIosCredentials({
|
|
5544
|
+
projectRoot: params.input.projectRoot,
|
|
5545
|
+
mainBundleIdentifier: params.mainBundleIdentifier
|
|
5546
|
+
}) : downloadIosCredentials(params.api, {
|
|
5547
|
+
projectId: params.input.projectId,
|
|
5548
|
+
mainBundleIdentifier: params.mainBundleIdentifier,
|
|
5549
|
+
bundleIdentifiers: params.allBundleIdentifiers,
|
|
5550
|
+
distribution: params.input.iosProfile.distribution,
|
|
5551
|
+
tempDir: params.input.tempDir
|
|
5552
|
+
});
|
|
5553
|
+
const installPerTarget = (signedTargets, credentials, credentialsSource) => Effect.gen(function* () {
|
|
5554
|
+
const profileByBundle = new Map(credentials.profiles.map((profile) => [profile.bundleIdentifier, profile]));
|
|
5555
|
+
const missing = signedTargets.filter((target) => !profileByBundle.has(target.bundleId));
|
|
5556
|
+
if (missing.length > 0) {
|
|
5557
|
+
const list = missing.map((target) => `"${target.targetName}" (${target.bundleId})`).join(", ");
|
|
5558
|
+
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.";
|
|
5559
|
+
return yield* new MissingCredentialsError({
|
|
5560
|
+
message: `Missing provisioning profile for signed target(s): ${list}.`,
|
|
5561
|
+
hint
|
|
5797
5562
|
});
|
|
5798
5563
|
}
|
|
5564
|
+
return yield* Effect.forEach(signedTargets, (target) => installProfileForTarget(target, profileByBundle));
|
|
5799
5565
|
});
|
|
5800
|
-
const
|
|
5801
|
-
const
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
}).pipe(Effect.mapError((cause) => new AppleIdGenerateFailedError({
|
|
5808
|
-
step: "parse-p12",
|
|
5809
|
-
message: cause.message
|
|
5566
|
+
const installProfileForTarget = (target, profileByBundle) => {
|
|
5567
|
+
const profile = profileByBundle.get(target.bundleId);
|
|
5568
|
+
if (!profile) return Effect.fail(new ProvisioningError({ message: `Internal: no profile for ${target.bundleId} after pre-check.` }));
|
|
5569
|
+
return installProvisioningProfile({ profilePath: profile.profilePath }).pipe(Effect.map((installed) => ({
|
|
5570
|
+
target,
|
|
5571
|
+
profile,
|
|
5572
|
+
installed
|
|
5810
5573
|
})));
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5574
|
+
};
|
|
5575
|
+
const pickMainTarget = (signedTargets) => signedTargets.find((target) => target.productType === "com.apple.product-type.application") ?? signedTargets[0];
|
|
5576
|
+
const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
5577
|
+
const { api, tempDir, projectRoot, iosProfile, envVars } = input;
|
|
5578
|
+
const runtime = yield* CliRuntime;
|
|
5579
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5580
|
+
const iosDir = path.join(projectRoot, "ios");
|
|
5581
|
+
const { distribution } = iosProfile;
|
|
5582
|
+
const commandEnv = yield* runtime.commandEnvironment(envVars);
|
|
5583
|
+
yield* prebuildAndPods({
|
|
5584
|
+
projectRoot,
|
|
5585
|
+
iosDir,
|
|
5586
|
+
commandEnv
|
|
5587
|
+
});
|
|
5588
|
+
const workspaceFilename = yield* findXcworkspace(iosDir);
|
|
5589
|
+
const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
|
|
5590
|
+
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
5591
|
+
const signedTargets = yield* discoverSignedTargets({
|
|
5592
|
+
iosDir,
|
|
5593
|
+
configurationName: configuration
|
|
5594
|
+
});
|
|
5595
|
+
const mainTarget = pickMainTarget(signedTargets);
|
|
5596
|
+
if (!mainTarget) return yield* new BuildFailedError({
|
|
5597
|
+
step: "discover signed targets",
|
|
5598
|
+
exitCode: 1,
|
|
5599
|
+
message: `No signed iOS targets found in the Xcode project for configuration "${configuration}".`
|
|
5600
|
+
});
|
|
5601
|
+
if (input.credentialsSource === "remote") yield* ensurePerTargetCredentials({
|
|
5602
|
+
api,
|
|
5603
|
+
projectId: input.projectId,
|
|
5604
|
+
distribution: iosProfile.distribution,
|
|
5605
|
+
signedTargets,
|
|
5606
|
+
freezeCredentials: input.freezeCredentials ?? false
|
|
5607
|
+
});
|
|
5608
|
+
const credentials = yield* fetchAllCredentials({
|
|
5609
|
+
api,
|
|
5610
|
+
input,
|
|
5611
|
+
mainBundleIdentifier: mainTarget.bundleId,
|
|
5612
|
+
allBundleIdentifiers: signedTargets.map((target) => target.bundleId)
|
|
5613
|
+
});
|
|
5614
|
+
const keychain = yield* acquireKeychain({
|
|
5615
|
+
tempDir,
|
|
5616
|
+
p12Path: credentials.p12Path,
|
|
5617
|
+
p12Password: credentials.p12Password
|
|
5618
|
+
});
|
|
5619
|
+
const installedTargets = yield* installPerTarget(signedTargets, credentials, input.credentialsSource);
|
|
5620
|
+
yield* applyTargetSigning({
|
|
5621
|
+
iosDir,
|
|
5622
|
+
entries: installedTargets.map(({ target, installed }) => ({
|
|
5623
|
+
targetName: target.targetName,
|
|
5624
|
+
buildConfigurationUuids: target.buildConfigurationUuids,
|
|
5625
|
+
settings: {
|
|
5626
|
+
teamId: installed.teamId,
|
|
5627
|
+
signingIdentity: keychain.signingIdentity,
|
|
5628
|
+
profileSpecifier: installed.name
|
|
5629
|
+
}
|
|
5630
|
+
}))
|
|
5631
|
+
});
|
|
5632
|
+
const archivePath = path.join(tempDir, "build.xcarchive");
|
|
5633
|
+
const archiveCmd = Command.make("xcodebuild", "-workspace", workspaceFilename, "-scheme", scheme, "-configuration", configuration, "-archivePath", archivePath, "-allowProvisioningUpdates", "archive").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
5634
|
+
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
5635
|
+
yield* formatter ? runStepFormatted(archiveCmd, "xcodebuild archive", formatter) : runStep(archiveCmd, "xcodebuild archive");
|
|
5636
|
+
const exportOptionsPath = path.join(tempDir, "ExportOptions.plist");
|
|
5637
|
+
const mainInstall = installedTargets.find((entry) => entry.target.targetName === mainTarget.targetName);
|
|
5638
|
+
if (!mainInstall) return yield* new BuildFailedError({
|
|
5639
|
+
step: "resolve main target signing",
|
|
5640
|
+
exitCode: 1,
|
|
5641
|
+
message: `Internal: main target "${mainTarget.targetName}" was not in the installed list.`
|
|
5642
|
+
});
|
|
5643
|
+
const { teamId } = mainInstall.installed;
|
|
5644
|
+
yield* fs.writeFileString(exportOptionsPath, renderExportOptionsPlist({
|
|
5645
|
+
method: distribution,
|
|
5646
|
+
teamId,
|
|
5647
|
+
provisioningProfiles: installedTargets.map(({ target, installed }) => ({
|
|
5648
|
+
bundleId: target.bundleId,
|
|
5649
|
+
profileName: installed.name
|
|
5650
|
+
}))
|
|
5651
|
+
}));
|
|
5652
|
+
const exportPath = path.join(tempDir, "export");
|
|
5653
|
+
const exportCmd = Command.make("xcodebuild", "-exportArchive", "-archivePath", archivePath, "-exportPath", exportPath, "-exportOptionsPlist", exportOptionsPath, "-allowProvisioningUpdates").pipe(Command.workingDirectory(iosDir), Command.env(commandEnv));
|
|
5654
|
+
yield* formatter ? runStepFormatted(exportCmd, "xcodebuild exportArchive", formatter) : runStep(exportCmd, "xcodebuild exportArchive");
|
|
5655
|
+
yield* validateIosBuild({
|
|
5656
|
+
archivePath,
|
|
5657
|
+
expectedTeamId: teamId,
|
|
5658
|
+
expectedTargets: installedTargets.map(({ target, installed }) => ({
|
|
5659
|
+
bundleId: target.bundleId,
|
|
5660
|
+
profileUuid: installed.uuid
|
|
5661
|
+
}))
|
|
5662
|
+
});
|
|
5663
|
+
const artifactPath = yield* findIosArtifact({ exportPath });
|
|
5664
|
+
const { sha256, byteSize } = yield* sha256File(artifactPath);
|
|
5821
5665
|
return {
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
appleTeamIdentifier: metadata.appleTeamId,
|
|
5826
|
-
developerPortalIdentifier: result.certificate.id
|
|
5666
|
+
artifactPath,
|
|
5667
|
+
byteSize,
|
|
5668
|
+
sha256
|
|
5827
5669
|
};
|
|
5828
5670
|
});
|
|
5829
|
-
const
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
platform: AppleUtils.BundleIdPlatform.IOS
|
|
5846
|
-
}))).id;
|
|
5847
|
-
});
|
|
5848
|
-
const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(function* () {
|
|
5849
|
-
const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
|
|
5850
|
-
const upper = serialNumber.toUpperCase();
|
|
5851
|
-
const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
|
|
5852
|
-
if (match === void 0) return yield* Effect.fail(new AppleIdGenerateFailedError({
|
|
5853
|
-
step: "match-apple-certificate",
|
|
5854
|
-
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
5855
|
-
}));
|
|
5856
|
-
return match.id;
|
|
5857
|
-
});
|
|
5858
|
-
const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
|
|
5859
|
-
const devices = yield* wrap("apple-list-devices", async () => AppleUtils.Device.getAllIOSProfileDevicesAsync(ctx));
|
|
5860
|
-
if (deviceIds === void 0) return devices.map((device) => device.id);
|
|
5861
|
-
const allowed = new Set(deviceIds);
|
|
5862
|
-
return devices.filter((device) => allowed.has(device.id)).map((device) => device.id);
|
|
5671
|
+
const runIosBuild = (input) => input.iosProfile.simulator === true ? runIosSimulatorBuild(input) : runIosDeviceBuild(input);
|
|
5672
|
+
|
|
5673
|
+
//#endregion
|
|
5674
|
+
//#region src/commands/build/reserve-and-upload.ts
|
|
5675
|
+
const buildReserveCommon = (input) => ({
|
|
5676
|
+
projectId: input.projectId,
|
|
5677
|
+
profile: input.profileName,
|
|
5678
|
+
runtimeVersion: input.runtimeVersion,
|
|
5679
|
+
bundleId: input.bundleId,
|
|
5680
|
+
sha256: input.sha256,
|
|
5681
|
+
byteSize: input.byteSize,
|
|
5682
|
+
...input.appVersion === void 0 ? {} : { appVersion: input.appVersion },
|
|
5683
|
+
...input.buildNumber === void 0 ? {} : { buildNumber: input.buildNumber },
|
|
5684
|
+
...input.gitContext.ref === void 0 ? {} : { gitRef: input.gitContext.ref },
|
|
5685
|
+
...input.gitContext.commit === void 0 ? {} : { gitCommit: input.gitContext.commit },
|
|
5686
|
+
...input.message === void 0 ? {} : { message: input.message }
|
|
5863
5687
|
});
|
|
5864
|
-
const
|
|
5865
|
-
const
|
|
5866
|
-
const
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
})
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
message: "Apple returned a profile with no content (likely expired/invalid)"
|
|
5889
|
-
}));
|
|
5890
|
-
const profileBytes = fromBase64(profileContent);
|
|
5891
|
-
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
|
|
5892
|
-
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
5893
|
-
profileBase64: toBase64(profileBytes),
|
|
5894
|
-
appleDistributionCertificateId: input.distributionCertificateId,
|
|
5895
|
-
isManaged: true,
|
|
5896
|
-
...rosterHash === void 0 ? {} : { deviceRosterHash: rosterHash }
|
|
5688
|
+
const callReserve = (api, input) => {
|
|
5689
|
+
const common = buildReserveCommon(input);
|
|
5690
|
+
const { target } = input;
|
|
5691
|
+
if (target.platform === "ios") return target.distribution === "simulator" ? api.builds.reserve({ payload: {
|
|
5692
|
+
...common,
|
|
5693
|
+
platform: "ios",
|
|
5694
|
+
distribution: "simulator",
|
|
5695
|
+
artifactFormat: "tar.gz"
|
|
5696
|
+
} }) : api.builds.reserve({ payload: {
|
|
5697
|
+
...common,
|
|
5698
|
+
platform: "ios",
|
|
5699
|
+
distribution: target.distribution,
|
|
5700
|
+
artifactFormat: "ipa"
|
|
5701
|
+
} });
|
|
5702
|
+
return target.distribution === "play-store" ? api.builds.reserve({ payload: {
|
|
5703
|
+
...common,
|
|
5704
|
+
platform: "android",
|
|
5705
|
+
distribution: "play-store",
|
|
5706
|
+
artifactFormat: "aab"
|
|
5707
|
+
} }) : api.builds.reserve({ payload: {
|
|
5708
|
+
...common,
|
|
5709
|
+
platform: "android",
|
|
5710
|
+
distribution: "direct",
|
|
5711
|
+
artifactFormat: "apk"
|
|
5897
5712
|
} });
|
|
5713
|
+
};
|
|
5714
|
+
/**
|
|
5715
|
+
* Reserve a build record on the server, upload the artifact to the returned
|
|
5716
|
+
* presigned URL, and finalize the build with its sha256 + byteSize.
|
|
5717
|
+
*/
|
|
5718
|
+
const reserveAndUpload = (api, input) => Effect.gen(function* () {
|
|
5719
|
+
const presignedUploadClient = yield* PresignedUploadClient;
|
|
5720
|
+
const reserveResult = yield* callReserve(api, input).pipe(Effect.mapError((cause) => new ReserveError({ message: `Failed to reserve build: ${formatCause(cause)}` })));
|
|
5721
|
+
yield* presignedUploadClient.putToPresignedUrl({
|
|
5722
|
+
url: reserveResult.uploadUrl,
|
|
5723
|
+
filePath: input.artifactPath,
|
|
5724
|
+
byteSize: input.byteSize,
|
|
5725
|
+
expiresAt: reserveResult.uploadExpiresAt,
|
|
5726
|
+
headers: reserveResult.uploadHeaders
|
|
5727
|
+
});
|
|
5728
|
+
const completed = yield* api.builds.complete({
|
|
5729
|
+
path: { id: reserveResult.id },
|
|
5730
|
+
payload: {
|
|
5731
|
+
sha256: input.sha256,
|
|
5732
|
+
byteSize: input.byteSize
|
|
5733
|
+
}
|
|
5734
|
+
}).pipe(Effect.mapError((cause) => new CompleteError({ message: `Failed to complete build ${reserveResult.id}: ${formatCause(cause)}` })));
|
|
5735
|
+
if (!completed.artifact) return yield* new CompleteError({ message: `Build ${completed.id} completed but server returned no artifact record.` });
|
|
5898
5736
|
return {
|
|
5899
|
-
id:
|
|
5900
|
-
|
|
5901
|
-
distributionType: created.distributionType,
|
|
5902
|
-
profileName: created.profileName,
|
|
5903
|
-
validUntil: created.validUntil,
|
|
5904
|
-
developerPortalIdentifier: created.developerPortalIdentifier
|
|
5737
|
+
id: completed.id,
|
|
5738
|
+
status: "uploaded"
|
|
5905
5739
|
};
|
|
5906
5740
|
});
|
|
5907
5741
|
|
|
5908
5742
|
//#endregion
|
|
5909
|
-
//#region src/lib/
|
|
5743
|
+
//#region src/lib/auto-increment.ts
|
|
5744
|
+
const bumpBuildNumber = (current) => Effect.gen(function* () {
|
|
5745
|
+
const raw = current ?? "0";
|
|
5746
|
+
const parsed = Number.parseInt(raw, 10);
|
|
5747
|
+
if (Number.isNaN(parsed)) return yield* new BuildProfileError({ message: `Cannot autoIncrement ios.buildNumber: current value "${raw}" is not a base-10 integer.` });
|
|
5748
|
+
return String(parsed + 1);
|
|
5749
|
+
});
|
|
5750
|
+
const bumpVersionCode = (current) => Effect.gen(function* () {
|
|
5751
|
+
const value = current ?? 0;
|
|
5752
|
+
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.` });
|
|
5753
|
+
return value + 1;
|
|
5754
|
+
});
|
|
5755
|
+
const SEMVER_PATCH = /^(\d+)\.(\d+)\.(\d+)(.*)$/u;
|
|
5756
|
+
const bumpVersion = (current) => Effect.gen(function* () {
|
|
5757
|
+
if (current === void 0) return yield* new BuildProfileError({ message: "Cannot autoIncrement version: no `version` field set in Expo config." });
|
|
5758
|
+
const match = SEMVER_PATCH.exec(current);
|
|
5759
|
+
if (!match) return yield* new BuildProfileError({ message: `Cannot autoIncrement version: "${current}" is not a semver string like "1.2.3".` });
|
|
5760
|
+
const [, major, minor, patch, suffix] = match;
|
|
5761
|
+
const nextPatch = Number.parseInt(patch ?? "0", 10) + 1;
|
|
5762
|
+
return `${major ?? "0"}.${minor ?? "0"}.${String(nextPatch)}${suffix ?? ""}`;
|
|
5763
|
+
});
|
|
5764
|
+
const computeIosBumps = (config, mode) => Effect.gen(function* () {
|
|
5765
|
+
if (mode === "buildNumber") return { nextBuildNumber: yield* bumpBuildNumber(config.ios?.buildNumber) };
|
|
5766
|
+
return {
|
|
5767
|
+
nextVersion: yield* bumpVersion(config.version),
|
|
5768
|
+
nextBuildNumber: yield* bumpBuildNumber(config.ios?.buildNumber)
|
|
5769
|
+
};
|
|
5770
|
+
});
|
|
5771
|
+
const computeAndroidBumps = (config, mode) => Effect.gen(function* () {
|
|
5772
|
+
if (mode === "versionCode") return { nextVersionCode: yield* bumpVersionCode(config.android?.versionCode) };
|
|
5773
|
+
return {
|
|
5774
|
+
nextVersion: yield* bumpVersion(config.version),
|
|
5775
|
+
nextVersionCode: yield* bumpVersionCode(config.android?.versionCode)
|
|
5776
|
+
};
|
|
5777
|
+
});
|
|
5778
|
+
const buildPatch = (platform, bumps) => {
|
|
5779
|
+
const patch = {};
|
|
5780
|
+
if (bumps.nextVersion !== void 0) patch["version"] = bumps.nextVersion;
|
|
5781
|
+
if (platform === "ios" && bumps.nextBuildNumber !== void 0) patch["ios"] = { buildNumber: bumps.nextBuildNumber };
|
|
5782
|
+
if (platform === "android" && bumps.nextVersionCode !== void 0) patch["android"] = { versionCode: bumps.nextVersionCode };
|
|
5783
|
+
return patch;
|
|
5784
|
+
};
|
|
5785
|
+
const describeBumps = (platform, bumps) => {
|
|
5786
|
+
const parts = [];
|
|
5787
|
+
if (bumps.nextVersion !== void 0) parts.push(`version=${bumps.nextVersion}`);
|
|
5788
|
+
if (platform === "ios" && bumps.nextBuildNumber !== void 0) parts.push(`ios.buildNumber=${bumps.nextBuildNumber}`);
|
|
5789
|
+
if (platform === "android" && bumps.nextVersionCode !== void 0) parts.push(`android.versionCode=${String(bumps.nextVersionCode)}`);
|
|
5790
|
+
return parts.join(", ");
|
|
5791
|
+
};
|
|
5792
|
+
const computeBumps = (input) => {
|
|
5793
|
+
if (input.platform === "ios") return input.iosMode === void 0 ? Effect.succeed({}) : computeIosBumps(input.config, input.iosMode);
|
|
5794
|
+
return input.androidMode === void 0 ? Effect.succeed({}) : computeAndroidBumps(input.config, input.androidMode);
|
|
5795
|
+
};
|
|
5796
|
+
const hasAnyBump = (bumps) => bumps.nextVersion !== void 0 || bumps.nextBuildNumber !== void 0 || bumps.nextVersionCode !== void 0;
|
|
5910
5797
|
/**
|
|
5911
|
-
*
|
|
5912
|
-
*
|
|
5913
|
-
*
|
|
5914
|
-
*
|
|
5798
|
+
* Bump `version` / `ios.buildNumber` / `android.versionCode` per the resolved
|
|
5799
|
+
* autoIncrement mode, persist via `@expo/config.modifyConfigAsync`, and log a
|
|
5800
|
+
* Human-readable summary. No-op when the mode is undefined. Returns the new
|
|
5801
|
+
* Bumped values so callers can refresh their in-memory ExpoConfig.
|
|
5915
5802
|
*/
|
|
5916
|
-
const
|
|
5917
|
-
const
|
|
5918
|
-
if (
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5803
|
+
const applyAutoIncrement = (input) => Effect.gen(function* () {
|
|
5804
|
+
const bumps = yield* computeBumps(input);
|
|
5805
|
+
if (!hasAnyBump(bumps)) return bumps;
|
|
5806
|
+
const patch = buildPatch(input.platform, bumps);
|
|
5807
|
+
const result = yield* writeExpoConfigPatch(input.projectRoot, patch).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to persist autoIncrement: ${cause.message}` })));
|
|
5808
|
+
if (result.type === "warn" && result.configPath === null) {
|
|
5809
|
+
yield* Console.log(`autoIncrement: dynamic Expo config detected, cannot write back. Update manually: ${describeBumps(input.platform, bumps)}`);
|
|
5810
|
+
return bumps;
|
|
5811
|
+
}
|
|
5812
|
+
yield* Console.log(`autoIncrement: bumped ${describeBumps(input.platform, bumps)}`);
|
|
5813
|
+
return bumps;
|
|
5814
|
+
});
|
|
5815
|
+
|
|
5816
|
+
//#endregion
|
|
5817
|
+
//#region src/lib/eas-config.ts
|
|
5818
|
+
const MAX_EXTENDS_DEPTH = 10;
|
|
5819
|
+
const asStringValue = (value) => typeof value === "string" ? value : void 0;
|
|
5820
|
+
const asBooleanValue = (value) => typeof value === "boolean" ? value : void 0;
|
|
5821
|
+
const asEnv = (value) => {
|
|
5822
|
+
const record = asRecord(value);
|
|
5823
|
+
if (!record) return;
|
|
5824
|
+
const env = {};
|
|
5825
|
+
for (const [key, raw] of Object.entries(record)) if (typeof raw === "string") env[key] = raw;
|
|
5826
|
+
return Object.keys(env).length === 0 ? void 0 : env;
|
|
5827
|
+
};
|
|
5828
|
+
const asIosDistribution = (raw) => {
|
|
5829
|
+
const value = asStringValue(raw);
|
|
5830
|
+
if (value === "app-store" || value === "ad-hoc" || value === "development" || value === "enterprise") return value;
|
|
5831
|
+
};
|
|
5832
|
+
const asEnterpriseProvisioning = (raw) => {
|
|
5833
|
+
const value = asStringValue(raw);
|
|
5834
|
+
return value === "adhoc" || value === "universal" ? value : void 0;
|
|
5835
|
+
};
|
|
5836
|
+
const asAndroidBuildType = (raw) => {
|
|
5837
|
+
const value = asStringValue(raw);
|
|
5838
|
+
return value === "debug" || value === "release" ? value : void 0;
|
|
5839
|
+
};
|
|
5840
|
+
const asAndroidFormat = (raw) => {
|
|
5841
|
+
const value = asStringValue(raw);
|
|
5842
|
+
return value === "apk" || value === "aab" ? value : void 0;
|
|
5843
|
+
};
|
|
5844
|
+
const asAndroidDistribution = (raw) => {
|
|
5845
|
+
const value = asStringValue(raw);
|
|
5846
|
+
return value === "play-store" || value === "direct" ? value : void 0;
|
|
5847
|
+
};
|
|
5848
|
+
const asIosAutoIncrement = (raw) => {
|
|
5849
|
+
if (typeof raw === "boolean") return raw;
|
|
5850
|
+
const value = asStringValue(raw);
|
|
5851
|
+
return value === "buildNumber" || value === "version" ? value : void 0;
|
|
5852
|
+
};
|
|
5853
|
+
const asAndroidAutoIncrement = (raw) => {
|
|
5854
|
+
if (typeof raw === "boolean") return raw;
|
|
5855
|
+
const value = asStringValue(raw);
|
|
5856
|
+
return value === "versionCode" || value === "version" ? value : void 0;
|
|
5857
|
+
};
|
|
5858
|
+
const asAutoIncrement = (raw) => {
|
|
5859
|
+
if (typeof raw === "boolean") return raw;
|
|
5860
|
+
const value = asStringValue(raw);
|
|
5861
|
+
return value === "buildNumber" || value === "versionCode" || value === "version" ? value : void 0;
|
|
5862
|
+
};
|
|
5863
|
+
const asEasDistribution = (raw) => {
|
|
5864
|
+
const value = asStringValue(raw);
|
|
5865
|
+
return value === "internal" || value === "store" ? value : void 0;
|
|
5866
|
+
};
|
|
5867
|
+
const asCredentialsSource = (raw) => {
|
|
5868
|
+
const value = asStringValue(raw);
|
|
5869
|
+
return value === "remote" || value === "local" ? value : void 0;
|
|
5870
|
+
};
|
|
5871
|
+
const parseIosProfile = (raw) => {
|
|
5872
|
+
const record = asRecord(raw);
|
|
5873
|
+
if (!record) return;
|
|
5874
|
+
const distribution = asIosDistribution(record["distribution"]);
|
|
5875
|
+
const buildConfiguration = asStringValue(record["buildConfiguration"]);
|
|
5876
|
+
const scheme = asStringValue(record["scheme"]);
|
|
5877
|
+
const simulator = asBooleanValue(record["simulator"]);
|
|
5878
|
+
const enterpriseProvisioning = asEnterpriseProvisioning(record["enterpriseProvisioning"]);
|
|
5879
|
+
const autoIncrement = asIosAutoIncrement(record["autoIncrement"]);
|
|
5880
|
+
return {
|
|
5881
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5882
|
+
...buildConfiguration === void 0 ? {} : { buildConfiguration },
|
|
5883
|
+
...scheme === void 0 ? {} : { scheme },
|
|
5884
|
+
...simulator === void 0 ? {} : { simulator },
|
|
5885
|
+
...enterpriseProvisioning === void 0 ? {} : { enterpriseProvisioning },
|
|
5886
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
5887
|
+
};
|
|
5888
|
+
};
|
|
5889
|
+
const parseAndroidProfile = (raw) => {
|
|
5890
|
+
const record = asRecord(raw);
|
|
5891
|
+
if (!record) return;
|
|
5892
|
+
const buildType = asAndroidBuildType(record["buildType"]);
|
|
5893
|
+
const flavor = asStringValue(record["flavor"]);
|
|
5894
|
+
const gradleCommand = asStringValue(record["gradleCommand"]);
|
|
5895
|
+
const format = asAndroidFormat(record["format"]);
|
|
5896
|
+
const distribution = asAndroidDistribution(record["distribution"]);
|
|
5897
|
+
const autoIncrement = asAndroidAutoIncrement(record["autoIncrement"]);
|
|
5898
|
+
return {
|
|
5899
|
+
...buildType === void 0 ? {} : { buildType },
|
|
5900
|
+
...flavor === void 0 ? {} : { flavor },
|
|
5901
|
+
...gradleCommand === void 0 ? {} : { gradleCommand },
|
|
5902
|
+
...format === void 0 ? {} : { format },
|
|
5903
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5904
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
5905
|
+
};
|
|
5906
|
+
};
|
|
5907
|
+
const parseBuildProfile = (raw) => {
|
|
5908
|
+
const record = asRecord(raw);
|
|
5909
|
+
if (!record) return;
|
|
5910
|
+
const extendsName = asStringValue(record["extends"]);
|
|
5911
|
+
const developmentClient = asBooleanValue(record["developmentClient"]);
|
|
5912
|
+
const distribution = asEasDistribution(record["distribution"]);
|
|
5913
|
+
const channel = asStringValue(record["channel"]);
|
|
5914
|
+
const environment = asStringValue(record["environment"]);
|
|
5915
|
+
const env = asEnv(record["env"]);
|
|
5916
|
+
const ios = parseIosProfile(record["ios"]);
|
|
5917
|
+
const android = parseAndroidProfile(record["android"]);
|
|
5918
|
+
const credentialsSource = asCredentialsSource(record["credentialsSource"]);
|
|
5919
|
+
const autoIncrement = asAutoIncrement(record["autoIncrement"]);
|
|
5920
|
+
return {
|
|
5921
|
+
...extendsName === void 0 ? {} : { extends: extendsName },
|
|
5922
|
+
...developmentClient === void 0 ? {} : { developmentClient },
|
|
5923
|
+
...distribution === void 0 ? {} : { distribution },
|
|
5924
|
+
...channel === void 0 ? {} : { channel },
|
|
5925
|
+
...environment === void 0 ? {} : { environment },
|
|
5926
|
+
...env === void 0 ? {} : { env },
|
|
5927
|
+
...ios === void 0 ? {} : { ios },
|
|
5928
|
+
...android === void 0 ? {} : { android },
|
|
5929
|
+
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
5930
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
5931
|
+
};
|
|
5932
|
+
};
|
|
5933
|
+
const parseEasConfig = (text) => Effect.gen(function* () {
|
|
5934
|
+
const root = asRecord(yield* Effect.try({
|
|
5935
|
+
try: () => JSON.parse(text),
|
|
5936
|
+
catch: (cause) => new BuildProfileError({ message: `eas.json is not valid JSON: ${formatCause(cause)}` })
|
|
5937
|
+
}));
|
|
5938
|
+
if (!root) return yield* new BuildProfileError({ message: "eas.json must be a JSON object at the top level." });
|
|
5939
|
+
const buildRecord = asRecord(root["build"]);
|
|
5940
|
+
if (!buildRecord) return asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {};
|
|
5941
|
+
const profiles = {};
|
|
5942
|
+
for (const [name, value] of Object.entries(buildRecord)) {
|
|
5943
|
+
const profile = parseBuildProfile(value);
|
|
5944
|
+
if (profile) profiles[name] = profile;
|
|
5932
5945
|
}
|
|
5933
|
-
if (existing.appleTeamId !== input.appleTeamId) return yield* new MissingCredentialsError({
|
|
5934
|
-
message: `Bundle "${input.bundleIdentifier}" (${input.distributionType}) is already bound to a different Apple team than the new credentials.`,
|
|
5935
|
-
hint: "Delete the existing bundle configuration via the dashboard before retrying with a different team."
|
|
5936
|
-
});
|
|
5937
|
-
yield* api.iosBundleConfigurations.update({
|
|
5938
|
-
path: { id: existing.id },
|
|
5939
|
-
payload: {
|
|
5940
|
-
appleDistributionCertificateId: input.appleDistributionCertificateId,
|
|
5941
|
-
appleProvisioningProfileId: input.appleProvisioningProfileId,
|
|
5942
|
-
...input.ascApiKeyId === void 0 ? {} : { ascApiKeyId: input.ascApiKeyId }
|
|
5943
|
-
}
|
|
5944
|
-
});
|
|
5945
|
-
yield* Console.log("iOS bundle configuration rebound.");
|
|
5946
5946
|
return {
|
|
5947
|
-
|
|
5948
|
-
|
|
5947
|
+
...asRecord(root["cli"]) ? { cli: parseCli(root["cli"]) } : {},
|
|
5948
|
+
build: profiles
|
|
5949
5949
|
};
|
|
5950
5950
|
});
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
const
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
}, {
|
|
5960
|
-
value: "asc-key",
|
|
5961
|
-
label: "Use an App Store Connect API key"
|
|
5962
|
-
}]);
|
|
5963
|
-
});
|
|
5964
|
-
const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
|
|
5965
|
-
yield* Console.log("");
|
|
5966
|
-
yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
|
|
5967
|
-
const certs = yield* listDistributionCertsViaAppleId(ctx, "IOS_DISTRIBUTION");
|
|
5968
|
-
if (certs.length === 0) return yield* new AppleIdGenerateFailedError({
|
|
5969
|
-
step: "limit-recover",
|
|
5970
|
-
message: "Apple says the certificate limit is hit but no existing certificates were returned."
|
|
5971
|
-
});
|
|
5972
|
-
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
5973
|
-
value: entry.developerPortalIdentifier,
|
|
5974
|
-
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
5975
|
-
})), { required: true });
|
|
5976
|
-
yield* Effect.forEach(toRevoke, (id) => revokeDistributionCertViaAppleId(ctx, id), { concurrency: "inherit" });
|
|
5977
|
-
yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
|
|
5951
|
+
const parseCli = (raw) => {
|
|
5952
|
+
const record = asRecord(raw);
|
|
5953
|
+
if (!record) return {};
|
|
5954
|
+
const version = asStringValue(record["version"]);
|
|
5955
|
+
return version === void 0 ? {} : { version };
|
|
5956
|
+
};
|
|
5957
|
+
const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
5958
|
+
return (yield* Path.Path).join(projectRoot, "eas.json");
|
|
5978
5959
|
});
|
|
5979
|
-
const
|
|
5980
|
-
yield*
|
|
5981
|
-
const
|
|
5982
|
-
return yield*
|
|
5960
|
+
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
5961
|
+
const fs = yield* FileSystem.FileSystem;
|
|
5962
|
+
const filePath = yield* easJsonPath(projectRoot);
|
|
5963
|
+
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}` })))));
|
|
5983
5964
|
});
|
|
5984
|
-
const
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
const team = teams.items.find((entry) => entry.appleTeamId === appleTeamIdentifier);
|
|
5988
|
-
const items = team === void 0 ? [] : all.items.filter((cert) => cert.appleTeamId === team.id);
|
|
5989
|
-
if (items.length === 0) {
|
|
5990
|
-
const created = yield* generateDistributionCertViaAppleIdInteractive(api, ctx);
|
|
5991
|
-
return {
|
|
5992
|
-
id: created.id,
|
|
5993
|
-
appleTeamId: created.appleTeamId
|
|
5994
|
-
};
|
|
5995
|
-
}
|
|
5996
|
-
const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
|
|
5997
|
-
value: GENERATE_NEW,
|
|
5998
|
-
label: "Generate a new distribution certificate"
|
|
5999
|
-
}, ...items.map((cert) => ({
|
|
6000
|
-
value: cert.id,
|
|
6001
|
-
label: `${cert.serialNumber.slice(0, 12)}… (team ${appleTeamIdentifier})`
|
|
6002
|
-
}))]);
|
|
6003
|
-
if (choice === GENERATE_NEW) {
|
|
6004
|
-
const created = yield* generateDistributionCertViaAppleIdInteractive(api, ctx);
|
|
6005
|
-
return {
|
|
6006
|
-
id: created.id,
|
|
6007
|
-
appleTeamId: created.appleTeamId
|
|
6008
|
-
};
|
|
6009
|
-
}
|
|
6010
|
-
const cert = items.find((entry) => entry.id === choice);
|
|
6011
|
-
if (cert === void 0) return yield* new AppleIdGenerateFailedError({
|
|
6012
|
-
step: "pick-certificate",
|
|
6013
|
-
message: `Selected certificate ${choice} not found after listing`
|
|
6014
|
-
});
|
|
5965
|
+
const mergeIos = (base, overlay) => {
|
|
5966
|
+
if (!base) return overlay;
|
|
5967
|
+
if (!overlay) return base;
|
|
6015
5968
|
return {
|
|
6016
|
-
|
|
6017
|
-
|
|
5969
|
+
...base,
|
|
5970
|
+
...overlay
|
|
6018
5971
|
};
|
|
5972
|
+
};
|
|
5973
|
+
const mergeAndroid = (base, overlay) => {
|
|
5974
|
+
if (!base) return overlay;
|
|
5975
|
+
if (!overlay) return base;
|
|
5976
|
+
return {
|
|
5977
|
+
...base,
|
|
5978
|
+
...overlay
|
|
5979
|
+
};
|
|
5980
|
+
};
|
|
5981
|
+
const mergeEnv = (base, overlay) => {
|
|
5982
|
+
if (!base) return overlay;
|
|
5983
|
+
if (!overlay) return base;
|
|
5984
|
+
return {
|
|
5985
|
+
...base,
|
|
5986
|
+
...overlay
|
|
5987
|
+
};
|
|
5988
|
+
};
|
|
5989
|
+
const mergeProfile = (base, overlay) => {
|
|
5990
|
+
const ios = mergeIos(base.ios, overlay.ios);
|
|
5991
|
+
const android = mergeAndroid(base.android, overlay.android);
|
|
5992
|
+
const env = mergeEnv(base.env, overlay.env);
|
|
5993
|
+
const developmentClient = overlay.developmentClient ?? base.developmentClient;
|
|
5994
|
+
const distribution = overlay.distribution ?? base.distribution;
|
|
5995
|
+
const channel = overlay.channel ?? base.channel;
|
|
5996
|
+
const environment = overlay.environment ?? base.environment;
|
|
5997
|
+
const credentialsSource = overlay.credentialsSource ?? base.credentialsSource;
|
|
5998
|
+
const autoIncrement = overlay.autoIncrement ?? base.autoIncrement;
|
|
5999
|
+
return {
|
|
6000
|
+
...overlay.extends === void 0 ? {} : { extends: overlay.extends },
|
|
6001
|
+
...developmentClient === void 0 ? {} : { developmentClient },
|
|
6002
|
+
...distribution === void 0 ? {} : { distribution },
|
|
6003
|
+
...channel === void 0 ? {} : { channel },
|
|
6004
|
+
...environment === void 0 ? {} : { environment },
|
|
6005
|
+
...env === void 0 ? {} : { env },
|
|
6006
|
+
...ios === void 0 ? {} : { ios },
|
|
6007
|
+
...android === void 0 ? {} : { android },
|
|
6008
|
+
...credentialsSource === void 0 ? {} : { credentialsSource },
|
|
6009
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
6010
|
+
};
|
|
6011
|
+
};
|
|
6012
|
+
const collectExtendsChain = (profiles, profileName) => Effect.gen(function* () {
|
|
6013
|
+
const chain = [];
|
|
6014
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6015
|
+
let current = profileName;
|
|
6016
|
+
let depth = 0;
|
|
6017
|
+
while (current !== void 0) {
|
|
6018
|
+
if (visited.has(current)) return yield* new BuildProfileError({ message: `Cycle detected in eas.json build.${profileName} extends chain at "${current}".` });
|
|
6019
|
+
visited.add(current);
|
|
6020
|
+
const profile = profiles[current];
|
|
6021
|
+
if (!profile) return yield* new BuildProfileError({ message: current === profileName ? `Build profile "${profileName}" not found in eas.json.` : `Build profile "${profileName}" extends missing profile "${current}".` });
|
|
6022
|
+
chain.unshift(profile);
|
|
6023
|
+
current = profile.extends;
|
|
6024
|
+
depth += 1;
|
|
6025
|
+
if (depth > MAX_EXTENDS_DEPTH) return yield* new BuildProfileError({ message: `Too many "extends" levels (max ${String(MAX_EXTENDS_DEPTH)}) in eas.json build.${profileName}.` });
|
|
6026
|
+
}
|
|
6027
|
+
return chain;
|
|
6019
6028
|
});
|
|
6020
|
-
const
|
|
6021
|
-
|
|
6022
|
-
const
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
const
|
|
6027
|
-
yield*
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
return
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
if (
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
ascApiKeyId,
|
|
6079
|
-
developerPortalIdentifier: id
|
|
6080
|
-
}), { concurrency: "inherit" });
|
|
6081
|
-
yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
|
|
6082
|
-
});
|
|
6083
|
-
const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
|
|
6084
|
-
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null);
|
|
6085
|
-
if (teamAscKeys.length === 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
6086
|
-
message: "No ASC API key linked to an Apple team in this organization.",
|
|
6087
|
-
hint: "Upload an ASC API key with a team assignment via the dashboard, then retry."
|
|
6088
|
-
}));
|
|
6089
|
-
const ascKeyId = yield* promptSelect("Select an ASC API key to issue the certificate against", teamAscKeys.map((key) => ({
|
|
6090
|
-
value: key.id,
|
|
6091
|
-
label: `${key.name} (${key.keyId})`
|
|
6092
|
-
})));
|
|
6093
|
-
yield* Console.log("Generating CSR and requesting certificate from Apple...");
|
|
6094
|
-
const generate = generateAndUploadDistributionCertificate(api, { ascApiKeyId: ascKeyId });
|
|
6095
|
-
return yield* generate.pipe(Effect.catchTag("CertificateLimitError", () => interactiveCertLimitRecover(api, ascKeyId).pipe(Effect.flatMap(() => generate))));
|
|
6096
|
-
});
|
|
6097
|
-
const chooseIosCertificateId = (api) => Effect.gen(function* () {
|
|
6098
|
-
const certs = yield* api.appleDistributionCertificates.list();
|
|
6099
|
-
if (certs.items.length === 0) {
|
|
6100
|
-
yield* Console.log("No distribution certificate found in this organization.");
|
|
6101
|
-
if ((yield* promptSelect("How would you like to proceed?", [{
|
|
6102
|
-
value: "generate",
|
|
6103
|
-
label: "Generate a new distribution certificate"
|
|
6104
|
-
}, {
|
|
6105
|
-
value: "abort",
|
|
6106
|
-
label: "Abort — I'll upload one manually"
|
|
6107
|
-
}])) === "abort") return yield* Effect.fail(new MissingCredentialsError({
|
|
6108
|
-
message: "Build aborted — no distribution certificate available.",
|
|
6109
|
-
hint: "Run `better-update credentials generate distribution-certificate --asc-key-id <id>` or upload via the dashboard."
|
|
6110
|
-
}));
|
|
6111
|
-
return (yield* generateDistributionCertInteractive(api)).id;
|
|
6112
|
-
}
|
|
6113
|
-
const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
|
|
6114
|
-
value: "__generate__",
|
|
6115
|
-
label: "Generate a new distribution certificate"
|
|
6116
|
-
}, ...certs.items.map((cert) => ({
|
|
6117
|
-
value: cert.id,
|
|
6118
|
-
label: `${cert.serialNumber.slice(0, 12)}… (team ${cert.appleTeamId})`
|
|
6119
|
-
}))]);
|
|
6120
|
-
if (choice === "__generate__") return (yield* generateDistributionCertInteractive(api)).id;
|
|
6121
|
-
return choice;
|
|
6122
|
-
});
|
|
6123
|
-
const pickIosCertificate = (api) => Effect.gen(function* () {
|
|
6124
|
-
const chosenId = yield* chooseIosCertificateId(api);
|
|
6125
|
-
const cert = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === chosenId);
|
|
6126
|
-
if (cert === void 0) return yield* Effect.fail(new MissingCredentialsError({
|
|
6127
|
-
message: "Selected certificate not found after generation.",
|
|
6128
|
-
hint: "Retry."
|
|
6129
|
-
}));
|
|
6029
|
+
const stripExtends = (profile) => {
|
|
6030
|
+
if (profile.extends === void 0) return profile;
|
|
6031
|
+
const { extends: _omit, ...rest } = profile;
|
|
6032
|
+
return rest;
|
|
6033
|
+
};
|
|
6034
|
+
const resolveEasBuildProfile = (config, profileName) => Effect.gen(function* () {
|
|
6035
|
+
const profiles = config.build;
|
|
6036
|
+
if (!profiles) return yield* new BuildProfileError({ message: "eas.json has no \"build\" section. Add at least one profile." });
|
|
6037
|
+
return stripExtends((yield* collectExtendsChain(profiles, profileName)).reduce((acc, next, index) => index === 0 ? next : mergeProfile(acc, next), {}));
|
|
6038
|
+
});
|
|
6039
|
+
|
|
6040
|
+
//#endregion
|
|
6041
|
+
//#region src/lib/build-profile.ts
|
|
6042
|
+
const asString$1 = (value) => typeof value === "string" ? value : void 0;
|
|
6043
|
+
const deriveIosDistribution = (eas) => {
|
|
6044
|
+
const override = eas.ios?.distribution;
|
|
6045
|
+
if (override) return override;
|
|
6046
|
+
if (eas.developmentClient === true) return "development";
|
|
6047
|
+
if (eas.distribution === "internal") return "ad-hoc";
|
|
6048
|
+
if (eas.distribution === "store") return "app-store";
|
|
6049
|
+
};
|
|
6050
|
+
const deriveAndroidFormat = (eas) => {
|
|
6051
|
+
if (eas.android?.format) return eas.android.format;
|
|
6052
|
+
if (eas.distribution === "store") return "aab";
|
|
6053
|
+
if (eas.distribution === "internal") return "apk";
|
|
6054
|
+
if (eas.developmentClient === true) return "apk";
|
|
6055
|
+
};
|
|
6056
|
+
const deriveAndroidDistribution = (eas, format) => {
|
|
6057
|
+
if (eas.android?.distribution) return eas.android.distribution;
|
|
6058
|
+
if (format === "aab") return "play-store";
|
|
6059
|
+
return "direct";
|
|
6060
|
+
};
|
|
6061
|
+
const hasIosIntent = (eas) => eas.ios !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
|
|
6062
|
+
const hasAndroidIntent = (eas) => eas.android !== void 0 || eas.distribution !== void 0 || eas.developmentClient === true;
|
|
6063
|
+
const resolveIosAutoIncrement = (eas) => {
|
|
6064
|
+
const override = eas.ios?.autoIncrement;
|
|
6065
|
+
if (override === false) return;
|
|
6066
|
+
if (override === true) return "buildNumber";
|
|
6067
|
+
if (override === "buildNumber" || override === "version") return override;
|
|
6068
|
+
const top = eas.autoIncrement;
|
|
6069
|
+
if (top === true || top === "buildNumber") return "buildNumber";
|
|
6070
|
+
if (top === "version") return "version";
|
|
6071
|
+
};
|
|
6072
|
+
const resolveAndroidAutoIncrement = (eas) => {
|
|
6073
|
+
const override = eas.android?.autoIncrement;
|
|
6074
|
+
if (override === false) return;
|
|
6075
|
+
if (override === true) return "versionCode";
|
|
6076
|
+
if (override === "versionCode" || override === "version") return override;
|
|
6077
|
+
const top = eas.autoIncrement;
|
|
6078
|
+
if (top === true || top === "versionCode") return "versionCode";
|
|
6079
|
+
if (top === "version") return "version";
|
|
6080
|
+
};
|
|
6081
|
+
const toIosProfile = (eas) => {
|
|
6082
|
+
if (!hasIosIntent(eas)) return;
|
|
6083
|
+
const distribution = deriveIosDistribution(eas);
|
|
6084
|
+
if (!distribution) return;
|
|
6085
|
+
const ios = eas.ios ?? {};
|
|
6086
|
+
const autoIncrement = resolveIosAutoIncrement(eas);
|
|
6130
6087
|
return {
|
|
6131
|
-
|
|
6132
|
-
|
|
6088
|
+
distribution,
|
|
6089
|
+
...ios.buildConfiguration === void 0 ? {} : { buildConfiguration: ios.buildConfiguration },
|
|
6090
|
+
...ios.scheme === void 0 ? {} : { scheme: ios.scheme },
|
|
6091
|
+
...ios.simulator === void 0 ? {} : { simulator: ios.simulator },
|
|
6092
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
6133
6093
|
};
|
|
6094
|
+
};
|
|
6095
|
+
const toAndroidProfile = (eas) => {
|
|
6096
|
+
if (!hasAndroidIntent(eas)) return;
|
|
6097
|
+
const format = deriveAndroidFormat(eas);
|
|
6098
|
+
if (!format) return;
|
|
6099
|
+
const android = eas.android ?? {};
|
|
6100
|
+
const distribution = deriveAndroidDistribution(eas, format);
|
|
6101
|
+
const autoIncrement = resolveAndroidAutoIncrement(eas);
|
|
6102
|
+
return {
|
|
6103
|
+
format,
|
|
6104
|
+
distribution,
|
|
6105
|
+
...android.buildType === void 0 ? {} : { buildType: android.buildType },
|
|
6106
|
+
...android.flavor === void 0 ? {} : { flavor: android.flavor },
|
|
6107
|
+
...android.gradleCommand === void 0 ? {} : { gradleCommand: android.gradleCommand },
|
|
6108
|
+
...autoIncrement === void 0 ? {} : { autoIncrement }
|
|
6109
|
+
};
|
|
6110
|
+
};
|
|
6111
|
+
const fromEasProfile = (eas, profileName) => {
|
|
6112
|
+
const ios = toIosProfile(eas);
|
|
6113
|
+
const android = toAndroidProfile(eas);
|
|
6114
|
+
return {
|
|
6115
|
+
name: profileName,
|
|
6116
|
+
environment: eas.environment ?? "production",
|
|
6117
|
+
...eas.channel === void 0 ? {} : { channel: eas.channel },
|
|
6118
|
+
...eas.env === void 0 ? {} : { env: eas.env },
|
|
6119
|
+
...ios === void 0 ? {} : { ios },
|
|
6120
|
+
...android === void 0 ? {} : { android },
|
|
6121
|
+
...eas.credentialsSource === void 0 ? {} : { credentialsSource: eas.credentialsSource }
|
|
6122
|
+
};
|
|
6123
|
+
};
|
|
6124
|
+
const readBuildProfile = (projectRoot, profileName) => Effect.gen(function* () {
|
|
6125
|
+
return fromEasProfile(yield* resolveEasBuildProfile(yield* readEasJson(projectRoot), profileName), profileName);
|
|
6134
6126
|
});
|
|
6135
|
-
const
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
message: `No ASC API key linked to Apple team ${appleTeamId}.`,
|
|
6139
|
-
hint: "Upload an ASC API key for that team via the dashboard, then retry."
|
|
6140
|
-
}));
|
|
6141
|
-
return yield* promptSelect("Select an ASC API key", teamAscKeys.map((key) => ({
|
|
6142
|
-
value: key.id,
|
|
6143
|
-
label: `${key.name} (${key.keyId})`
|
|
6144
|
-
})));
|
|
6145
|
-
});
|
|
6146
|
-
const generateProvisioningProfileForBundle = (api, input, ctx) => Effect.gen(function* () {
|
|
6147
|
-
yield* Console.log("Generating provisioning profile via App Store Connect API...");
|
|
6148
|
-
return (yield* generateAndUploadProvisioningProfile(api, {
|
|
6149
|
-
ascApiKeyId: ctx.ascKeyId,
|
|
6150
|
-
distributionCertificateId: ctx.certId,
|
|
6151
|
-
bundleIdentifier: input.bundleIdentifier,
|
|
6152
|
-
distributionType: ctx.distributionType
|
|
6153
|
-
})).id;
|
|
6154
|
-
});
|
|
6155
|
-
const resolveIosProfileId = (api, input, ctx) => Effect.gen(function* () {
|
|
6156
|
-
const matching = (yield* api.appleProvisioningProfiles.list({ urlParams: {} })).items.filter((profile) => profile.bundleIdentifier === input.bundleIdentifier && profile.distributionType === ctx.distributionType && profile.appleTeamId === ctx.cert.appleTeamId);
|
|
6157
|
-
if (matching.length === 0) return yield* generateProvisioningProfileForBundle(api, input, ctx);
|
|
6158
|
-
if (!(yield* promptConfirm(`Reuse an existing ${input.distribution} profile for ${input.bundleIdentifier}?`, { initialValue: true }))) return yield* generateProvisioningProfileForBundle(api, input, ctx);
|
|
6159
|
-
return yield* promptSelect("Select a provisioning profile", matching.map((profile) => ({
|
|
6160
|
-
value: profile.id,
|
|
6161
|
-
label: profile.profileName ?? profile.developerPortalIdentifier ?? profile.id
|
|
6162
|
-
})));
|
|
6163
|
-
});
|
|
6164
|
-
const setupIosViaAscKey = (api, input) => Effect.gen(function* () {
|
|
6165
|
-
const { certId, cert } = yield* pickIosCertificate(api);
|
|
6166
|
-
const ascKeyId = yield* pickIosAscKey(api, cert.appleTeamId);
|
|
6167
|
-
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
6168
|
-
const profileId = yield* resolveIosProfileId(api, input, {
|
|
6169
|
-
certId,
|
|
6170
|
-
cert,
|
|
6171
|
-
ascKeyId,
|
|
6172
|
-
distributionType
|
|
6173
|
-
});
|
|
6174
|
-
yield* upsertIosBundleConfiguration(api, {
|
|
6175
|
-
projectId: input.projectId,
|
|
6176
|
-
bundleIdentifier: input.bundleIdentifier,
|
|
6177
|
-
distributionType,
|
|
6178
|
-
appleTeamId: cert.appleTeamId,
|
|
6179
|
-
appleDistributionCertificateId: certId,
|
|
6180
|
-
appleProvisioningProfileId: profileId,
|
|
6181
|
-
ascApiKeyId: ascKeyId
|
|
6182
|
-
});
|
|
6127
|
+
const readRuntimeVersionMeta = (config) => ({
|
|
6128
|
+
appVersion: config.version,
|
|
6129
|
+
rawRuntimeVersion: readRawRuntimeVersion(config.runtimeVersion)
|
|
6183
6130
|
});
|
|
6131
|
+
const readRawRuntimeVersion = (value) => {
|
|
6132
|
+
if (typeof value === "string") return value;
|
|
6133
|
+
const policy = asString$1(asRecord(value)?.["policy"]);
|
|
6134
|
+
if (policy) return { policy };
|
|
6135
|
+
};
|
|
6184
6136
|
|
|
6185
6137
|
//#endregion
|
|
6186
|
-
//#region src/
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
const
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
})
|
|
6210
|
-
return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
|
|
6211
|
-
value: item.id,
|
|
6212
|
-
label: item.keyAlias
|
|
6213
|
-
})));
|
|
6138
|
+
//#region src/lib/clear-cache.ts
|
|
6139
|
+
/**
|
|
6140
|
+
* Project-scoped build cache directories to remove when --clear-cache is passed.
|
|
6141
|
+
* Intentionally avoids `~/.gradle/caches` (global) and `ios/Pods/` (requires
|
|
6142
|
+
* pod install rebuild — leave to the user).
|
|
6143
|
+
*/
|
|
6144
|
+
const CACHE_DIRS = [
|
|
6145
|
+
"android/.gradle",
|
|
6146
|
+
"android/app/build",
|
|
6147
|
+
"android/build",
|
|
6148
|
+
"ios/build",
|
|
6149
|
+
".expo",
|
|
6150
|
+
"node_modules/.cache"
|
|
6151
|
+
];
|
|
6152
|
+
const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
6153
|
+
const fs = yield* FileSystem.FileSystem;
|
|
6154
|
+
const removed = [];
|
|
6155
|
+
yield* Effect.forEach(CACHE_DIRS, (rel) => Effect.gen(function* () {
|
|
6156
|
+
const target = path.join(projectRoot, rel);
|
|
6157
|
+
if (!(yield* fs.exists(target).pipe(Effect.catchAll(() => Effect.succeed(false))))) return;
|
|
6158
|
+
yield* fs.remove(target, { recursive: true }).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
6159
|
+
removed.push(rel);
|
|
6160
|
+
}), { concurrency: 4 });
|
|
6161
|
+
if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
|
|
6214
6162
|
});
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6163
|
+
|
|
6164
|
+
//#endregion
|
|
6165
|
+
//#region src/lib/env-exporter.ts
|
|
6166
|
+
const coerceEnvironment = (raw) => raw === "development" || raw === "preview" || raw === "production" ? raw : void 0;
|
|
6167
|
+
/**
|
|
6168
|
+
* Pull environment variables for a project + environment and flatten them into
|
|
6169
|
+
* a key/value map. Returns an empty map when the project has no variables.
|
|
6170
|
+
*/
|
|
6171
|
+
const pullEnvVars = (api, { projectId, environment }) => {
|
|
6172
|
+
const validated = coerceEnvironment(environment);
|
|
6173
|
+
if (!validated) return Effect.fail(new EnvExportError({ message: `Invalid environment "${environment}". Must be one of: development, preview, production.` }));
|
|
6174
|
+
return api["env-vars"].export({ urlParams: {
|
|
6175
|
+
projectId,
|
|
6176
|
+
environment: validated
|
|
6177
|
+
} }).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)}` })));
|
|
6178
|
+
};
|
|
6179
|
+
|
|
6180
|
+
//#endregion
|
|
6181
|
+
//#region src/lib/git-context.ts
|
|
6182
|
+
const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd));
|
|
6183
|
+
/**
|
|
6184
|
+
* Best-effort git context extraction. If git is missing, the directory isn't
|
|
6185
|
+
* a repo, or any command fails, we silently return undefined fields so the
|
|
6186
|
+
* build can still proceed. This is intentional — git context is metadata,
|
|
6187
|
+
* not a requirement.
|
|
6188
|
+
*/
|
|
6189
|
+
const readGitContext = (projectRoot) => Effect.gen(function* () {
|
|
6190
|
+
const [commit, ref, commitMessage, status] = yield* Effect.all([
|
|
6191
|
+
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
6192
|
+
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
6193
|
+
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.catchAll(() => Effect.succeed(""))),
|
|
6194
|
+
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.catchAll(() => Effect.succeed("")))
|
|
6195
|
+
], { concurrency: "unbounded" });
|
|
6196
|
+
return {
|
|
6197
|
+
ref: ref.length > 0 ? ref : void 0,
|
|
6198
|
+
commit: commit.length > 0 ? commit : void 0,
|
|
6199
|
+
commitMessage: commitMessage.length > 0 ? commitMessage : void 0,
|
|
6200
|
+
dirty: status.trim().length > 0
|
|
6201
|
+
};
|
|
6222
6202
|
});
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6203
|
+
|
|
6204
|
+
//#endregion
|
|
6205
|
+
//#region src/lib/gradle-config.ts
|
|
6206
|
+
/**
|
|
6207
|
+
* Parse Groovy `build.gradle` to extract key Android config values.
|
|
6208
|
+
* Returns `undefined` if:
|
|
6209
|
+
* - Only `build.gradle.kts` exists (Kotlin DSL not supported by gradle-to-js)
|
|
6210
|
+
* - No build.gradle found at all
|
|
6211
|
+
* - Parse fails
|
|
6212
|
+
*
|
|
6213
|
+
* Informational only — never blocks the build.
|
|
6214
|
+
*/
|
|
6215
|
+
const readGradleConfig = (androidDir) => Effect.gen(function* () {
|
|
6216
|
+
const fs = yield* FileSystem.FileSystem;
|
|
6217
|
+
const gradlePath = path.join(androidDir, "app", "build.gradle");
|
|
6218
|
+
const ktsPath = path.join(androidDir, "app", "build.gradle.kts");
|
|
6219
|
+
const hasGroovy = yield* fs.exists(gradlePath).pipe(Effect.orElseSucceed(() => false));
|
|
6220
|
+
const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
|
|
6221
|
+
if (!hasGroovy && hasKts) return;
|
|
6222
|
+
if (!hasGroovy) return;
|
|
6223
|
+
const content = yield* fs.readFileString(gradlePath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
|
|
6224
|
+
if (!content) return;
|
|
6225
|
+
return yield* Effect.tryPromise({
|
|
6226
|
+
try: async () => {
|
|
6227
|
+
return __require("gradle-to-js").parseText(stripGroovyComments(content));
|
|
6236
6228
|
},
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
label: "Abort — I'll configure it in the dashboard"
|
|
6240
|
-
}
|
|
6241
|
-
]);
|
|
6242
|
-
if (choice === "abort") return yield* Effect.fail(new MissingCredentialsError({
|
|
6243
|
-
message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
|
|
6244
|
-
hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
|
|
6245
|
-
}));
|
|
6246
|
-
const keystoreId = yield* resolveAndroidKeystoreId(api, choice);
|
|
6247
|
-
yield* api.androidBuildCredentials.create({
|
|
6248
|
-
path: { applicationIdentifierId: appId },
|
|
6249
|
-
payload: {
|
|
6250
|
-
name: "Default",
|
|
6251
|
-
isDefault: true,
|
|
6252
|
-
androidUploadKeystoreId: keystoreId
|
|
6253
|
-
}
|
|
6254
|
-
});
|
|
6255
|
-
yield* Console.log("Android build credentials configured.");
|
|
6229
|
+
catch: () => void 0
|
|
6230
|
+
}).pipe(Effect.map(extractGradleConfig), Effect.catchAll(() => Effect.succeed(void 0)));
|
|
6256
6231
|
});
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6232
|
+
/**
|
|
6233
|
+
* Log a warning if Gradle applicationId differs from app.json package name.
|
|
6234
|
+
*/
|
|
6235
|
+
const warnOnGradleMismatch = (gradleConfig, expectedPackage) => {
|
|
6236
|
+
if (!gradleConfig?.applicationId) return Effect.void;
|
|
6237
|
+
if (gradleConfig.applicationId === expectedPackage) return Effect.void;
|
|
6238
|
+
return Console.warn(`Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". The Gradle value will be used in the built APK/AAB.`);
|
|
6239
|
+
};
|
|
6240
|
+
/**
|
|
6241
|
+
* Strip Groovy single-line and block comments.
|
|
6242
|
+
* gradle-to-js chokes on comments — EAS CLI does this same pre-processing.
|
|
6243
|
+
*/
|
|
6244
|
+
const stripGroovyComments = (text) => text.replaceAll(/\/\/.*$/gmu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
|
|
6245
|
+
const parseVersionCode = (raw) => {
|
|
6246
|
+
if (typeof raw === "number") return raw;
|
|
6247
|
+
if (typeof raw === "string") return Number.parseInt(raw, 10) || void 0;
|
|
6248
|
+
};
|
|
6249
|
+
const extractGradleConfig = (parsed) => {
|
|
6250
|
+
const defaultConfig = asRecord(asRecord(parsed["android"])?.["defaultConfig"]);
|
|
6251
|
+
const applicationId = typeof defaultConfig?.["applicationId"] === "string" ? unquote(defaultConfig["applicationId"]) : void 0;
|
|
6252
|
+
const versionCode = parseVersionCode(defaultConfig?.["versionCode"]);
|
|
6253
|
+
const versionName = typeof defaultConfig?.["versionName"] === "string" ? unquote(defaultConfig["versionName"]) : void 0;
|
|
6254
|
+
return {
|
|
6255
|
+
...applicationId === void 0 ? {} : { applicationId },
|
|
6256
|
+
...versionCode === void 0 ? {} : { versionCode },
|
|
6257
|
+
...versionName === void 0 ? {} : { versionName }
|
|
6258
|
+
};
|
|
6259
|
+
};
|
|
6260
|
+
const unquote = (input) => input.startsWith("\"") && input.endsWith("\"") ? input.slice(1, -1) : input;
|
|
6261
|
+
|
|
6262
|
+
//#endregion
|
|
6263
|
+
//#region src/lib/platform-detect.ts
|
|
6264
|
+
const PLATFORMS = ["ios", "android"];
|
|
6265
|
+
const inferPlatforms = (config) => {
|
|
6266
|
+
const fromConfig = config["platforms"];
|
|
6267
|
+
if (Array.isArray(fromConfig)) return fromConfig.filter((entry) => entry === "ios" || entry === "android");
|
|
6268
|
+
const present = [];
|
|
6269
|
+
if (config.ios !== void 0) present.push("ios");
|
|
6270
|
+
if (config.android !== void 0) present.push("android");
|
|
6271
|
+
return present;
|
|
6272
|
+
};
|
|
6273
|
+
/**
|
|
6274
|
+
* Resolve a build platform from an explicit flag, or fall back to the Expo
|
|
6275
|
+
* config (`expo.platforms` or the presence of `ios`/`android` sections). Prompts
|
|
6276
|
+
* when the config declares both platforms; fails when ambiguous and prompts are
|
|
6277
|
+
* disallowed.
|
|
6278
|
+
*/
|
|
6279
|
+
const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
6280
|
+
if (explicit !== void 0) return explicit;
|
|
6281
|
+
const candidates = inferPlatforms(config);
|
|
6282
|
+
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." });
|
|
6283
|
+
if (candidates.length === 1) {
|
|
6284
|
+
const [only] = candidates;
|
|
6285
|
+
if (only === void 0) return yield* new BuildProfileError({ message: "Internal: empty platform candidate list." });
|
|
6286
|
+
return only;
|
|
6262
6287
|
}
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
6269
|
-
}));
|
|
6270
|
-
yield* setupAndroidInteractive(api, input);
|
|
6271
|
-
return yield* ensureAndroidCredentialsAvailable(api, input);
|
|
6272
|
-
})));
|
|
6273
|
-
const setupIosInteractive = (api, input) => Effect.gen(function* () {
|
|
6274
|
-
yield* Console.log("");
|
|
6275
|
-
yield* Console.log(`No iOS bundle configuration for ${input.bundleIdentifier} (${input.distribution}).`);
|
|
6276
|
-
if ((yield* chooseIosSetupPath(api)) === "apple-id") return yield* setupIosViaAppleId(api, input);
|
|
6277
|
-
return yield* setupIosViaAscKey(api, input);
|
|
6288
|
+
if (!(yield* InteractiveMode).allow) return yield* new BuildProfileError({ message: `Multiple platforms detected (${candidates.join(", ")}). Pass --platform explicitly when running non-interactively.` });
|
|
6289
|
+
return yield* promptSelect("Which platform to build?", PLATFORMS.filter((entry) => candidates.includes(entry)).map((entry) => ({
|
|
6290
|
+
value: entry,
|
|
6291
|
+
label: entry
|
|
6292
|
+
})));
|
|
6278
6293
|
});
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6294
|
+
|
|
6295
|
+
//#endregion
|
|
6296
|
+
//#region src/lib/repo-clean.ts
|
|
6297
|
+
const MAX_FILES_SHOWN = 10;
|
|
6298
|
+
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([])));
|
|
6299
|
+
/**
|
|
6300
|
+
* Refuse to proceed when the working tree has uncommitted changes. Skipped when
|
|
6301
|
+
* `allowDirty` is true. In interactive mode, prompts the user to confirm; in
|
|
6302
|
+
* non-interactive mode, fails with `DirtyRepoError`.
|
|
6303
|
+
*/
|
|
6304
|
+
const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(function* () {
|
|
6305
|
+
if (allowDirty) return;
|
|
6306
|
+
const dirty = yield* readPorcelain(projectRoot);
|
|
6307
|
+
if (dirty.length === 0) return;
|
|
6308
|
+
const preview = dirty.slice(0, MAX_FILES_SHOWN).join("\n ");
|
|
6309
|
+
const overflow = dirty.length > MAX_FILES_SHOWN ? `\n ... and ${String(dirty.length - MAX_FILES_SHOWN)} more` : "";
|
|
6310
|
+
yield* Console.error(`Uncommitted changes (${String(dirty.length)} file(s)):\n ${preview}${overflow}`);
|
|
6311
|
+
if (!(yield* InteractiveMode).allow) {
|
|
6312
|
+
yield* new DirtyRepoError({ message: `Refusing to ${label} with a dirty working tree. Commit your changes or pass --allow-dirty.` });
|
|
6313
|
+
return;
|
|
6285
6314
|
}
|
|
6315
|
+
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
6286
6316
|
});
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
}));
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
const config = yield* findBoundIosConfig(api, input);
|
|
6298
|
-
if (config.appleDistributionCertificateId === null) return yield* new MissingCredentialsError({
|
|
6299
|
-
message: "Profile cannot be regenerated: bundle configuration is missing the distribution certificate",
|
|
6300
|
-
hint: "Re-bind credentials via `better-update credentials generate` or the dashboard"
|
|
6301
|
-
});
|
|
6302
|
-
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
6303
|
-
if (config.ascApiKeyId === null) return yield* regenerateProvisioningProfileViaAppleId(api, {
|
|
6304
|
-
bundleIdentifier: input.bundleIdentifier,
|
|
6305
|
-
distributionCertificateId: config.appleDistributionCertificateId,
|
|
6306
|
-
distributionType,
|
|
6307
|
-
bundleConfigurationId: config.id
|
|
6308
|
-
});
|
|
6309
|
-
yield* Console.log("Regenerating provisioning profile via App Store Connect API...");
|
|
6310
|
-
const created = yield* generateAndUploadProvisioningProfile(api, {
|
|
6311
|
-
ascApiKeyId: config.ascApiKeyId,
|
|
6312
|
-
distributionCertificateId: config.appleDistributionCertificateId,
|
|
6313
|
-
bundleIdentifier: input.bundleIdentifier,
|
|
6314
|
-
distributionType
|
|
6315
|
-
});
|
|
6316
|
-
yield* api.iosBundleConfigurations.update({
|
|
6317
|
-
path: { id: config.id },
|
|
6318
|
-
payload: { appleProvisioningProfileId: created.id }
|
|
6317
|
+
|
|
6318
|
+
//#endregion
|
|
6319
|
+
//#region src/lib/fingerprint.ts
|
|
6320
|
+
var FingerprintError = class extends Data.TaggedError("FingerprintError") {};
|
|
6321
|
+
const runFingerprintFull = (projectRoot) => Effect.gen(function* () {
|
|
6322
|
+
const cmd = Command.make("bunx", "@expo/fingerprint", projectRoot).pipe(Command.workingDirectory(projectRoot));
|
|
6323
|
+
const stdout = yield* Command.string(cmd).pipe(Effect.mapError((cause) => new FingerprintError({ message: `Failed to run "@expo/fingerprint": ${cause.message}` })));
|
|
6324
|
+
const parsed = yield* Effect.try({
|
|
6325
|
+
try: () => JSON.parse(stdout),
|
|
6326
|
+
catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
|
|
6319
6327
|
});
|
|
6320
|
-
return
|
|
6328
|
+
if (!isRecord(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
|
|
6329
|
+
const { hash } = parsed;
|
|
6330
|
+
if (typeof hash !== "string" || hash.length === 0) return yield* new FingerprintError({ message: "@expo/fingerprint output did not contain a \"hash\" string field." });
|
|
6331
|
+
const sourcesRaw = parsed["sources"];
|
|
6332
|
+
return {
|
|
6333
|
+
hash,
|
|
6334
|
+
sources: Array.isArray(sourcesRaw) ? sourcesRaw : []
|
|
6335
|
+
};
|
|
6336
|
+
});
|
|
6337
|
+
|
|
6338
|
+
//#endregion
|
|
6339
|
+
//#region src/lib/runtime-version.ts
|
|
6340
|
+
const resolveRuntimeVersion = ({ raw, appVersion, projectRoot }) => Effect.gen(function* () {
|
|
6341
|
+
if (typeof raw === "string") return raw;
|
|
6342
|
+
if (raw === void 0) return yield* new RuntimeVersionError({ message: "No runtimeVersion configured in expo section of app.json." });
|
|
6343
|
+
const { policy } = raw;
|
|
6344
|
+
if (policy === "appVersion") {
|
|
6345
|
+
if (appVersion === void 0) return yield* new RuntimeVersionError({ message: "runtimeVersion policy is \"appVersion\" but expo.version is missing in app.json." });
|
|
6346
|
+
return appVersion;
|
|
6347
|
+
}
|
|
6348
|
+
if (policy === "fingerprint") return yield* runFingerprintFull(projectRoot).pipe(Effect.map((result) => result.hash), Effect.mapError((cause) => new RuntimeVersionError({ message: cause.message })));
|
|
6349
|
+
if (policy === "nativeVersion") return yield* new RuntimeVersionError({ message: "runtimeVersion policy \"nativeVersion\" is not supported. Set a static runtimeVersion string in your Expo config." });
|
|
6350
|
+
return yield* new RuntimeVersionError({ message: `Unsupported runtimeVersion policy "${policy}". Use a static string, "appVersion", or "fingerprint".` });
|
|
6321
6351
|
});
|
|
6322
|
-
const ensureIosCredentials = (api, input, options) => resolveIosBuildCredentials(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
6323
|
-
const mode = yield* InteractiveMode;
|
|
6324
|
-
if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
|
|
6325
|
-
message: `No iOS build credentials for ${input.bundleIdentifier} (${input.distribution}).`,
|
|
6326
|
-
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
6327
|
-
}));
|
|
6328
|
-
yield* setupIosInteractive(api, input);
|
|
6329
|
-
return yield* resolveIosBuildCredentials(api, input);
|
|
6330
|
-
})), Effect.flatMap((resolved) => Effect.gen(function* () {
|
|
6331
|
-
if (resolved.platform !== "ios" || !resolved.profileStale) return;
|
|
6332
|
-
const mode = yield* InteractiveMode;
|
|
6333
|
-
if (options.freezeCredentials || !mode.allow) return yield* Effect.fail(new MissingCredentialsError({
|
|
6334
|
-
message: `Stale provisioning profile for ${input.bundleIdentifier}; cannot regenerate without an interactive session.`,
|
|
6335
|
-
hint: options.freezeCredentials ? "Run a build without --freeze-credentials once to refresh the profile, or run `better-update credentials regenerate-profile`." : "Run `better-update credentials regenerate-profile --bundle <id> --distribution <type>` from an interactive terminal."
|
|
6336
|
-
}));
|
|
6337
|
-
yield* Console.log(`Stale provisioning profile for ${input.bundleIdentifier} (device roster changed). Regenerating...`);
|
|
6338
|
-
yield* regenerateProvisioningProfile(api, input);
|
|
6339
|
-
})));
|
|
6340
6352
|
|
|
6341
6353
|
//#endregion
|
|
6342
6354
|
//#region src/application/build-workflow.ts
|
|
@@ -6363,7 +6375,8 @@ const runIosPlatformBuild = (input) => Effect.gen(function* () {
|
|
|
6363
6375
|
envVars,
|
|
6364
6376
|
projectId,
|
|
6365
6377
|
credentialsSource,
|
|
6366
|
-
rawOutput: options.rawOutput
|
|
6378
|
+
rawOutput: options.rawOutput,
|
|
6379
|
+
freezeCredentials: options.freezeCredentials ?? false
|
|
6367
6380
|
}),
|
|
6368
6381
|
target: isSimulator ? {
|
|
6369
6382
|
platform: "ios",
|