@hot-updater/apple-helper 1.2.0-rc.2 → 1.2.0-rc.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.
Files changed (3) hide show
  1. package/dist/index.cjs +106 -49
  2. package/dist/index.mjs +106 -49
  3. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -65,7 +65,7 @@ const ASC_RELEASE_ASSET = {
65
65
  x64: `asc_${ASC_CLI_VERSION}_macOS_amd64`
66
66
  };
67
67
  const ASC_RELEASE_URL = `https://github.com/rorkai/App-Store-Connect-CLI/releases/download/${ASC_CLI_VERSION}`;
68
- const INSTALL_HINT = `Install asc ${ASC_CLI_VERSION} or later with \`brew install asc\` (https://github.com/rorkai/App-Store-Connect-CLI), then run the command again.`;
68
+ const INSTALL_HINT = `Install asc ${ASC_CLI_VERSION} or later with brew install asc, then run this again.`;
69
69
  /** asc could not be found, downloaded, or verified. */
70
70
  var AscCliUnavailableError = class extends Error {
71
71
  constructor(message, options) {
@@ -97,7 +97,7 @@ const defaultAscCacheDir = () => node_path.default.join(node_os.default.homedir(
97
97
  * one: TestFlight builds need Xcode.
98
98
  */
99
99
  const findAscCli = async ({ cacheDir = defaultAscCacheDir(), fetch = globalThis.fetch, platform = process.platform, arch = process.arch, pathCommand = "asc" } = {}) => {
100
- if (platform !== "darwin") throw new AscCliUnavailableError("Signing in with an Apple Account to issue an App Store Connect API key runs on macOS, where iOS builds run.");
100
+ if (platform !== "darwin") throw new AscCliUnavailableError("Signing in to issue an App Store Connect API key needs macOS.");
101
101
  const minimum = parseVersion(ASC_CLI_VERSION);
102
102
  const onPath = await (0, execa.execa)(pathCommand, ["--version"], {
103
103
  reject: false,
@@ -112,7 +112,7 @@ const findAscCli = async ({ cacheDir = defaultAscCacheDir(), fetch = globalThis.
112
112
  };
113
113
  const asset = ASC_RELEASE_ASSET[arch];
114
114
  const expected = ASC_CLI_SHA256[arch];
115
- if (!asset || !expected) throw new AscCliUnavailableError(`asc publishes no macOS binary for ${arch}. ${INSTALL_HINT}`);
115
+ if (!asset || !expected) throw new AscCliUnavailableError(`asc publishes no macOS binary for ${arch}.\n${INSTALL_HINT}`);
116
116
  const binary = node_path.default.join(cacheDir, "asc", ASC_CLI_VERSION, asset);
117
117
  if (await sha256Of(binary).catch(() => void 0) === expected) return {
118
118
  path: binary,
@@ -126,12 +126,17 @@ const findAscCli = async ({ cacheDir = defaultAscCacheDir(), fetch = globalThis.
126
126
  try {
127
127
  response = await fetch(`${ASC_RELEASE_URL}/${asset}`, { redirect: "follow" });
128
128
  } catch (error) {
129
- throw new AscCliUnavailableError(`Could not download asc ${ASC_CLI_VERSION} from GitHub (${error instanceof Error ? error.message : String(error)}). ${INSTALL_HINT}`, { cause: error });
129
+ throw new AscCliUnavailableError(`Couldn't download asc ${ASC_CLI_VERSION} from GitHub.\n${error instanceof Error ? error.message : String(error)}\n${INSTALL_HINT}`, { cause: error });
130
130
  }
131
- if (!response.ok || !response.body) throw new AscCliUnavailableError(`Could not download asc ${ASC_CLI_VERSION} from GitHub (HTTP ${response.status}). ${INSTALL_HINT}`);
131
+ if (!response.ok || !response.body) throw new AscCliUnavailableError(`Couldn't download asc ${ASC_CLI_VERSION} from GitHub (HTTP ${response.status}).\n${INSTALL_HINT}`);
132
132
  await node_fs_promises.default.writeFile(partial, Buffer.from(await response.arrayBuffer()), { mode: 448 });
133
133
  const actual = await sha256Of(partial);
134
- if (actual !== expected) throw new AscCliUnavailableError(`The downloaded asc ${ASC_CLI_VERSION} does not match its published SHA-256 (expected ${expected}, got ${actual}), so it was deleted and not run. ${INSTALL_HINT}`);
134
+ if (actual !== expected) throw new AscCliUnavailableError([
135
+ `The downloaded asc ${ASC_CLI_VERSION} failed its SHA-256 check; it was deleted.`,
136
+ `Expected ${expected}`,
137
+ `Got ${actual}`,
138
+ INSTALL_HINT
139
+ ].join("\n"));
135
140
  await node_fs_promises.default.chmod(partial, 493);
136
141
  await node_fs_promises.default.rename(partial, binary);
137
142
  } finally {
@@ -234,7 +239,7 @@ const cliPrompts = {
234
239
  validate: (value) => value ? void 0 : "Enter the password."
235
240
  })),
236
241
  twoFactorCode: async (attempt) => answer(await _hot_updater_cli_tools.p.text({
237
- message: attempt === 1 ? "Two-factor code from your trusted device (no device at hand? enter any wrong code once, and Apple texts one to your phone)" : "Verification code Apple sent to your phone",
242
+ message: attempt === 1 ? "Two-factor code (no trusted device? enter a wrong one for an SMS)" : "Verification code Apple sent to your phone",
238
243
  validate: (value) => /^\d{6}$/u.test(value?.trim() ?? "") ? void 0 : "Enter the 6-digit code."
239
244
  }))?.trim()
240
245
  };
@@ -289,9 +294,9 @@ const text = (value) => typeof value === "string" && value.trim() !== "" ? value
289
294
  const lastLine = (stderr) => stderr.split("\n").map((line) => line.trim()).filter(Boolean).at(-1);
290
295
  /** What to do about a key asc could not create, by what App Store Connect said. */
291
296
  const createFailureHint = (stderr) => {
292
- if (/agreement/iu.test(stderr)) return "The Apple Developer Program License Agreement has changed: the Account Holder accepts it at https://developer.apple.com/account, then run this again.";
293
- if (/limit|maximum/iu.test(stderr)) return `The team has as many active API keys as App Store Connect allows: revoke one you no longer use at ${ASC_INTEGRATIONS_URL}, then run this again.`;
294
- if (/account holder|admin|permission|forbidden|not allowed|403/iu.test(stderr)) return "Creating an App Store Connect API key needs the Account Holder or Admin role in the team: sign in with such an Apple Account, or ask one to run this.";
297
+ if (/agreement/iu.test(stderr)) return "The Account Holder must accept the new Apple Developer Program License\nAgreement at https://developer.apple.com/account, then run this again.";
298
+ if (/limit|maximum/iu.test(stderr)) return `The team has the most API keys App Store Connect allows. Revoke one at:\n${ASC_INTEGRATIONS_URL}`;
299
+ if (/account holder|admin|permission|forbidden|not allowed|403/iu.test(stderr)) return "Creating an API key needs the Account Holder or Admin role in the team.";
295
300
  };
296
301
  /** A P-256 private key, as App Store Connect issues one, or a thrown error. */
297
302
  const assertP8 = (p8) => {
@@ -327,7 +332,7 @@ const assertP8 = (p8) => {
327
332
  */
328
333
  const createAscCliKeyIssuer = ({ asc, env = process.env, prompts = cliPrompts } = {}) => async ({ name, role, appleId, teamId, chooseTeam }) => {
329
334
  const cli = asc ?? await findAscCli();
330
- _hot_updater_cli_tools.p.log.info("Hot Updater signs in with asc, the open-source App Store Connect CLI. It keeps only the API key it issues: neither the password nor the session.");
335
+ _hot_updater_cli_tools.p.log.info("Signing in with asc, the App Store Connect CLI. Only the issued key is kept.");
331
336
  const password = await prompts.password(appleId);
332
337
  if (password === void 0) throw new AscCommandError("Signing in was cancelled.", void 0);
333
338
  (0, _hot_updater_cli_tools.registerSecretValue)(password);
@@ -679,6 +684,26 @@ const getDefaultDestination = ({ platform, useGeneric, deviceType }) => {
679
684
  });
680
685
  };
681
686
  //#endregion
687
+ //#region src/utils/formatCommand.ts
688
+ /**
689
+ * A command to print, broken at spaces with ` \` once a line would pass
690
+ * `width`, each continuation indented two spaces, so a terminal never wraps
691
+ * it mid-word and it still pastes as one command.
692
+ */
693
+ const formatCommand = (words, width = 74) => {
694
+ const lines = [];
695
+ let line = "";
696
+ for (const word of words) {
697
+ const next = line ? `${line} ${word}` : word;
698
+ if (line && next.length > width - 2) {
699
+ lines.push(`${line} \\`);
700
+ line = ` ${word}`;
701
+ } else line = next;
702
+ }
703
+ lines.push(line);
704
+ return lines.join("\n");
705
+ };
706
+ //#endregion
682
707
  //#region src/utils/parseXcodeProjectInfo.ts
683
708
  const parseXcodeProjectInfo = async (sourceDir) => {
684
709
  const files = await fs.default.promises.readdir(sourceDir);
@@ -701,8 +726,8 @@ const parseXcodeProjectInfo = async (sourceDir) => {
701
726
  const prettifyXcodebuildError = (error) => {
702
727
  if (error instanceof _hot_updater_cli_tools.BuildFailedError) {
703
728
  const errorLines = error.summary.join("\n");
704
- const message = errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "Not valid provisioning profile is set in the project. Check out the Signing & Capabilities tab in Xcode." : error.message;
705
- return new _hot_updater_cli_tools.BuildFailedError(`Xcodebuild failed: ${message}`, {
729
+ const message = errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "No valid provisioning profile is set in the project.\nCheck the Signing & Capabilities tab in Xcode." : error.message;
730
+ return new _hot_updater_cli_tools.BuildFailedError(message, {
706
731
  logPath: error.logPath,
707
732
  summary: error.summary,
708
733
  cause: error.cause
@@ -965,12 +990,14 @@ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, inst
965
990
  xcconfig: build.xcconfig,
966
991
  xcodeScheme
967
992
  });
968
- _hot_updater_cli_tools.p.log.info(`Xcode Archive Settings:
969
- Project ${xcodeProject.name}
970
- Scheme ${xcodeScheme}
971
- Platform ${platform}
972
- Command xcodebuild ${archiveArgs.join(" ")}
973
- `);
993
+ _hot_updater_cli_tools.p.log.info([
994
+ "Xcode archive",
995
+ `Project ${xcodeProject.name}`,
996
+ `Scheme ${xcodeScheme}`,
997
+ `Platform ${platform}`,
998
+ "Command",
999
+ formatCommand(["xcodebuild", ...archiveArgs])
1000
+ ].join("\n"));
974
1001
  try {
975
1002
  await runXcodebuildWithLogging({
976
1003
  args: archiveArgs,
@@ -1061,14 +1088,16 @@ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeSc
1061
1088
  sourceDir,
1062
1089
  xcodeProject
1063
1090
  });
1064
- _hot_updater_cli_tools.p.log.info(`Xcode Build Settings:
1065
- Project ${xcodeProject.name}
1066
- Scheme ${xcodeScheme}
1067
- Configuration ${configuration}
1068
- Platform ${platform}
1069
- Device Type ${deviceType}
1070
- Command xcodebuild ${buildArgs.join(" ")}
1071
- `);
1091
+ _hot_updater_cli_tools.p.log.info([
1092
+ "Xcode build",
1093
+ `Project ${xcodeProject.name}`,
1094
+ `Scheme ${xcodeScheme}`,
1095
+ `Configuration ${configuration}`,
1096
+ `Platform ${platform}`,
1097
+ `Device type ${deviceType}`,
1098
+ "Command",
1099
+ formatCommand(["xcodebuild", ...buildArgs])
1100
+ ].join("\n"));
1072
1101
  let builtAppPath;
1073
1102
  try {
1074
1103
  await runXcodebuildWithLogging({
@@ -1349,7 +1378,7 @@ var TestFlightBuildExistsError = class extends Error {
1349
1378
  bundleVersion;
1350
1379
  name = "TestFlightBuildExistsError";
1351
1380
  constructor(shortVersion, bundleVersion, options) {
1352
- super(`App Store Connect already has build ${bundleVersion} of version ${shortVersion}. Each build of a version needs a new build number (CFBundleVersion).`, options);
1381
+ super(`App Store Connect already has build ${bundleVersion} of version ${shortVersion}.\nUse a new build number (CFBundleVersion).`, options);
1353
1382
  this.shortVersion = shortVersion;
1354
1383
  this.bundleVersion = bundleVersion;
1355
1384
  }
@@ -1536,15 +1565,15 @@ const codeSigningOff = async (iosScheme) => {
1536
1565
  * it (`assertTestFlightExportOptions`).
1537
1566
  */
1538
1567
  const resolveTestFlightUploadKey = async ({ scheme, iosScheme }) => {
1539
- if (iosScheme.simulator) throw new Error(`TestFlight takes an .ipa, and the scheme ${scheme} builds a simulator .app.`);
1568
+ if (iosScheme.simulator) throw new Error(`The scheme ${scheme} builds a simulator .app, not an .ipa for TestFlight.`);
1540
1569
  const configuration = iosScheme.configuration ?? "Release";
1541
- if (/debug$/i.test(configuration)) throw new Error(`TestFlight takes a release build, and the scheme ${scheme} builds the ${configuration} configuration, which loads its JavaScript from Metro.`);
1570
+ if (/debug$/i.test(configuration)) throw new Error(`The scheme ${scheme} builds ${configuration}; TestFlight takes a release build.`);
1542
1571
  const unsigned = await codeSigningOff(iosScheme);
1543
- if (unsigned) throw new Error(`TestFlight takes a signed .ipa, and the scheme ${scheme} turns code signing off (${unsigned}).`);
1544
- if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa. Set signing.ascApiKey, or an exportOptionsPlist with the method app-store-connect.`);
1572
+ if (unsigned) throw new Error(`The scheme ${scheme} turns code signing off:\n${unsigned}`);
1573
+ if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa.\nSet signing.ascApiKey, or exportOptionsPlist with method app-store-connect.`);
1545
1574
  if (iosScheme.exportOptionsPlist && fs.default.existsSync(schemeFile(iosScheme.exportOptionsPlist))) {
1546
1575
  const method = await readExportMethod(schemeFile(iosScheme.exportOptionsPlist));
1547
- if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`TestFlight takes an .ipa exported for App Store Connect, and the scheme ${scheme}'s exportOptionsPlist exports with the method ${method || "(none)"}. Set its method to app-store-connect.`);
1576
+ if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`The scheme ${scheme}'s exportOptionsPlist uses method ${method || "(none)"}.\nSet its method to app-store-connect for TestFlight.`);
1548
1577
  }
1549
1578
  return {
1550
1579
  kind: "asc-api-key",
@@ -1559,7 +1588,7 @@ const resolveTestFlightUploadKey = async ({ scheme, iosScheme }) => {
1559
1588
  const assertTestFlightExportOptions = async (schemeConfig) => {
1560
1589
  if (!schemeConfig.exportOptionsPlist) return;
1561
1590
  const method = await readExportMethod(schemeConfig.exportOptionsPlist);
1562
- if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`TestFlight takes an .ipa exported for App Store Connect, and the scheme ${schemeConfig.hotUpdaterSchemeName}'s exportOptionsPlist exports with the method ${method || "(none)"}. Set its method to app-store-connect.`);
1591
+ if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`The scheme ${schemeConfig.hotUpdaterSchemeName}'s exportOptionsPlist uses method ${method || "(none)"}.\nSet its method to app-store-connect for TestFlight.`);
1563
1592
  };
1564
1593
  /**
1565
1594
  * Requests to the App Store Connect API with `apiKey`: each carries a token
@@ -1594,8 +1623,8 @@ const createAppStoreConnectRequest = ({ apiKey, privateKey, fetch }) => {
1594
1623
  });
1595
1624
  if (!response.ok) {
1596
1625
  const error = new AppStoreConnectError(action, response.status, await readApiErrors(response), response.headers.get("retry-after"));
1597
- if (response.status === 401) throw new Error("App Store Connect refused the API key: check its key ID and issuer ID, and that the key is not revoked.", { cause: error });
1598
- if (response.status === 403) throw new Error(`${error.message} An API key with the Admin, App Manager, or Developer role uploads builds.`, { cause: error });
1626
+ if (response.status === 401) throw new Error("App Store Connect refused the API key.\nCheck its key ID and issuer ID, and that it isn't revoked.", { cause: error });
1627
+ if (response.status === 403) throw new Error(`${error.message}\nUploads need a key with the Admin, App Manager, or Developer role.`, { cause: error });
1599
1628
  throw error;
1600
1629
  }
1601
1630
  return response.status === 204 ? void 0 : await response.json();
@@ -1608,7 +1637,7 @@ const findApp = async (request, bundleId) => {
1608
1637
  "fields[apps]": "bundleId"
1609
1638
  })}`));
1610
1639
  const app = apps.find((app) => app.attributes?.bundleId === bundleId);
1611
- if (!app) throw new Error(`App Store Connect has no app with the bundle ID ${bundleId} that this API key can see. Create the app in App Store Connect, or use a key of its team.`);
1640
+ if (!app) throw new Error(`App Store Connect has no app ${bundleId} that this API key can see.\nCreate the app in App Store Connect, or use a key of its team.`);
1612
1641
  return {
1613
1642
  app,
1614
1643
  testFlightUrl: `https://appstoreconnect.apple.com/apps/${app.id}/testflight/ios`
@@ -1680,14 +1709,18 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1680
1709
  } }));
1681
1710
  } catch (error) {
1682
1711
  if (error instanceof AppStoreConnectError && error.status === 409 && error.errors.some((apiError) => apiError.code === DUPLICATE_ERROR_CODE)) {
1683
- if (unanswered) throw new Error(`App Store Connect already has build ${info.bundleVersion} of version ${info.shortVersion}, which a request of this upload it did not answer may have reserved. Upload with a new build number (CFBundleVersion).`, { cause: error });
1712
+ if (unanswered) throw new Error([
1713
+ `App Store Connect already has build ${info.bundleVersion} of version ${info.shortVersion}.`,
1714
+ "An unanswered request of this upload may have reserved it.",
1715
+ "Upload with a new build number (CFBundleVersion)."
1716
+ ].join("\n"), { cause: error });
1684
1717
  throw new TestFlightBuildExistsError(info.shortVersion, info.bundleVersion, { cause: error });
1685
1718
  }
1686
1719
  if (!isRetryable(error) || attempt === COMMIT_ATTEMPTS) throw error;
1687
1720
  if (!isRateLimited(error)) unanswered = true;
1688
1721
  await sleep(retryDelayMs(error, attempt));
1689
1722
  }
1690
- _hot_updater_cli_tools.p.log.info(`Uploading ${ipaName} (version ${info.shortVersion}, build ${info.bundleVersion}) to App Store Connect`);
1723
+ _hot_updater_cli_tools.p.log.info(`Uploading ${ipaName} ${info.shortVersion} (${info.bundleVersion}) to App Store Connect`);
1691
1724
  const removeUpload = async () => {
1692
1725
  for (let attempt = 1;; attempt++) try {
1693
1726
  await request("Removing the build upload", "DELETE", `/v1/buildUploads/${buildUploadId}`);
@@ -1695,7 +1728,11 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1695
1728
  } catch (error) {
1696
1729
  if (error instanceof AppStoreConnectError && error.status === 404) return;
1697
1730
  if (!isRetryable(error) || attempt === COMMIT_ATTEMPTS) {
1698
- _hot_updater_cli_tools.p.log.warn(`App Store Connect kept the unfinished build upload ${buildUploadId}, which reserves build ${info.bundleVersion} of version ${info.shortVersion}: ${error.message} Upload with a new build number (CFBundleVersion).`);
1731
+ _hot_updater_cli_tools.p.log.warn([
1732
+ `App Store Connect kept the unfinished upload of build ${info.bundleVersion}.`,
1733
+ error.message,
1734
+ "Upload with a new build number (CFBundleVersion)."
1735
+ ].join("\n"));
1699
1736
  return;
1700
1737
  }
1701
1738
  await sleep(retryDelayMs(error, attempt));
@@ -1737,7 +1774,12 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1737
1774
  } });
1738
1775
  break;
1739
1776
  } catch (error) {
1740
- const unconfirmed = () => new Error(`App Store Connect did not confirm the upload of build ${info.bundleVersion}: ${error.message} Check the build in TestFlight before you upload it again: ${testFlightUrl}`, { cause: error });
1777
+ const unconfirmed = () => new Error([
1778
+ `App Store Connect didn't confirm the upload of build ${info.bundleVersion}.`,
1779
+ error.message,
1780
+ "Check TestFlight before you upload it again:",
1781
+ testFlightUrl
1782
+ ].join("\n"), { cause: error });
1741
1783
  if (!isRetryable(error)) {
1742
1784
  if (commitUnanswered) throw unconfirmed();
1743
1785
  await removeUpload();
@@ -1763,8 +1805,13 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1763
1805
  if (state.state === "PROCESSING") reportedProcessing = true;
1764
1806
  if (!state.state || IN_PROGRESS_STATES.has(state.state)) {
1765
1807
  if (Date.now() >= deadline) {
1766
- if (!reportedProcessing) throw new Error(`App Store Connect did not report build ${info.bundleVersion} processing${lastError instanceof Error ? `: ${lastError.message}` : state.state ? ` (${state.state}).` : "."} Check the build in TestFlight: ${testFlightUrl}`, { cause: lastError });
1767
- _hot_updater_cli_tools.p.log.warn(`App Store Connect is still processing build ${info.bundleVersion}. It appears in TestFlight once processing finishes: ${testFlightUrl}`);
1808
+ if (!reportedProcessing) throw new Error([
1809
+ `App Store Connect didn't report build ${info.bundleVersion} processing.`,
1810
+ lastError instanceof Error ? lastError.message : state.state ? `State: ${state.state}` : void 0,
1811
+ "Check the build in TestFlight:",
1812
+ testFlightUrl
1813
+ ].filter(Boolean).join("\n"), { cause: lastError });
1814
+ _hot_updater_cli_tools.p.log.warn(`App Store Connect is still processing build ${info.bundleVersion}.\nTestFlight lists it once processing finishes:\n${testFlightUrl}`);
1768
1815
  return {
1769
1816
  appId: app.id,
1770
1817
  buildUploadId,
@@ -1895,7 +1942,7 @@ const acquireWithLockf = async (lockPath) => {
1895
1942
  await fs.default.promises.mkdir(path.default.dirname(lockPath), { recursive: true });
1896
1943
  const release = await holdWithLockf(lockPath, false);
1897
1944
  if (release) return release;
1898
- _hot_updater_cli_tools.p.log.info("Waiting for another build that signs in a keychain of its own under this macOS user.");
1945
+ _hot_updater_cli_tools.p.log.info("Waiting for another build's signing keychain under this macOS user.");
1899
1946
  return await holdWithLockf(lockPath, true);
1900
1947
  };
1901
1948
  /**
@@ -1966,7 +2013,12 @@ const createSigningKeychain = async ({ security = runSecurity, directory = os.de
1966
2013
  live = void 0;
1967
2014
  process.off("exit", restoreLive);
1968
2015
  await releaseLock().catch(() => void 0);
1969
- if (failures.length > 0) _hot_updater_cli_tools.p.log.warn(`Restoring the keychains after the build failed (${failures.join("; ")}). Check \`security list-keychains -d user\` and \`security default-keychain -d user\`, and delete ${keychainPath} if it is left.`);
2016
+ if (failures.length > 0) _hot_updater_cli_tools.p.log.warn([
2017
+ "Couldn't restore the keychains after the build.",
2018
+ ...failures,
2019
+ "Check security list-keychains -d user and default-keychain -d user,",
2020
+ `and delete ${keychainPath} if it is left.`
2021
+ ].join("\n"));
1970
2022
  })();
1971
2023
  return disposal;
1972
2024
  };
@@ -2077,8 +2129,13 @@ const revokeKeychainCertificates = async ({ certificates, apiKey, privateKey, fe
2077
2129
  const reason = error instanceof Error ? error.message : String(error);
2078
2130
  for (const certificate of certificates) if (!revoked.includes(describe(certificate))) failed.push(`${describe(certificate)}: ${reason}`);
2079
2131
  }
2080
- if (revoked.length > 0) _hot_updater_cli_tools.p.log.info(`Revoked the signing certificates Xcode created for this build, whose keys the build's keychain held: ${revoked.join(", ")}.`);
2081
- if (failed.length > 0) _hot_updater_cli_tools.p.log.warn(`Revoking the signing certificates Xcode created for this build failed, and their keys are deleted with the build's keychain: ${failed.join("; ")}. Revoke them at ${CERTIFICATES_URL}, since App Store Connect stops creating certificates at a team's maximum.`);
2132
+ if (revoked.length > 0) _hot_updater_cli_tools.p.log.info(["Revoked the certificates Xcode created for this build:", ...revoked].join("\n"));
2133
+ if (failed.length > 0) _hot_updater_cli_tools.p.log.warn([
2134
+ "Couldn't revoke the certificates Xcode created for this build.",
2135
+ ...failed,
2136
+ "Revoke them at:",
2137
+ CERTIFICATES_URL
2138
+ ].join("\n"));
2082
2139
  return {
2083
2140
  revoked,
2084
2141
  failed
@@ -2106,7 +2163,7 @@ const withSigningKeychain = async ({ enabled, apiKey, run, security, lockPath, a
2106
2163
  lockPath,
2107
2164
  acquireLock
2108
2165
  });
2109
- _hot_updater_cli_tools.p.log.info("Signing in a keychain of this build's own, deleted when it ends (CI is set).");
2166
+ _hot_updater_cli_tools.p.log.info("Signing in a keychain of this build's own (CI is set).");
2110
2167
  let cleanup;
2111
2168
  const release = () => {
2112
2169
  cleanup ??= (async () => {
@@ -2244,7 +2301,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
2244
2301
  buildDirectory
2245
2302
  });
2246
2303
  const appStoreConnect = await exportsForAppStoreConnect(schemeConfig);
2247
- if ((0, _hot_updater_cli_tools.isCI)() && !appStoreConnect) _hot_updater_cli_tools.p.log.info("This export isn't for App Store Connect, so it signs in the runner's own keychains and keeps the certificates Xcode creates.");
2304
+ if ((0, _hot_updater_cli_tools.isCI)() && !appStoreConnect) _hot_updater_cli_tools.p.log.info("Signing in the runner's own keychains (not an App Store Connect export).");
2248
2305
  return withSigningKeychain({
2249
2306
  enabled: (0, _hot_updater_cli_tools.isCI)() && appStoreConnect,
2250
2307
  apiKey: signing.apiKey,
package/dist/index.mjs CHANGED
@@ -33,7 +33,7 @@ const ASC_RELEASE_ASSET = {
33
33
  x64: `asc_${ASC_CLI_VERSION}_macOS_amd64`
34
34
  };
35
35
  const ASC_RELEASE_URL = `https://github.com/rorkai/App-Store-Connect-CLI/releases/download/${ASC_CLI_VERSION}`;
36
- const INSTALL_HINT = `Install asc ${ASC_CLI_VERSION} or later with \`brew install asc\` (https://github.com/rorkai/App-Store-Connect-CLI), then run the command again.`;
36
+ const INSTALL_HINT = `Install asc ${ASC_CLI_VERSION} or later with brew install asc, then run this again.`;
37
37
  /** asc could not be found, downloaded, or verified. */
38
38
  var AscCliUnavailableError = class extends Error {
39
39
  constructor(message, options) {
@@ -65,7 +65,7 @@ const defaultAscCacheDir = () => path.join(os.homedir(), "Library", "Caches", "h
65
65
  * one: TestFlight builds need Xcode.
66
66
  */
67
67
  const findAscCli = async ({ cacheDir = defaultAscCacheDir(), fetch = globalThis.fetch, platform = process.platform, arch = process.arch, pathCommand = "asc" } = {}) => {
68
- if (platform !== "darwin") throw new AscCliUnavailableError("Signing in with an Apple Account to issue an App Store Connect API key runs on macOS, where iOS builds run.");
68
+ if (platform !== "darwin") throw new AscCliUnavailableError("Signing in to issue an App Store Connect API key needs macOS.");
69
69
  const minimum = parseVersion(ASC_CLI_VERSION);
70
70
  const onPath = await execa(pathCommand, ["--version"], {
71
71
  reject: false,
@@ -80,7 +80,7 @@ const findAscCli = async ({ cacheDir = defaultAscCacheDir(), fetch = globalThis.
80
80
  };
81
81
  const asset = ASC_RELEASE_ASSET[arch];
82
82
  const expected = ASC_CLI_SHA256[arch];
83
- if (!asset || !expected) throw new AscCliUnavailableError(`asc publishes no macOS binary for ${arch}. ${INSTALL_HINT}`);
83
+ if (!asset || !expected) throw new AscCliUnavailableError(`asc publishes no macOS binary for ${arch}.\n${INSTALL_HINT}`);
84
84
  const binary = path.join(cacheDir, "asc", ASC_CLI_VERSION, asset);
85
85
  if (await sha256Of(binary).catch(() => void 0) === expected) return {
86
86
  path: binary,
@@ -94,12 +94,17 @@ const findAscCli = async ({ cacheDir = defaultAscCacheDir(), fetch = globalThis.
94
94
  try {
95
95
  response = await fetch(`${ASC_RELEASE_URL}/${asset}`, { redirect: "follow" });
96
96
  } catch (error) {
97
- throw new AscCliUnavailableError(`Could not download asc ${ASC_CLI_VERSION} from GitHub (${error instanceof Error ? error.message : String(error)}). ${INSTALL_HINT}`, { cause: error });
97
+ throw new AscCliUnavailableError(`Couldn't download asc ${ASC_CLI_VERSION} from GitHub.\n${error instanceof Error ? error.message : String(error)}\n${INSTALL_HINT}`, { cause: error });
98
98
  }
99
- if (!response.ok || !response.body) throw new AscCliUnavailableError(`Could not download asc ${ASC_CLI_VERSION} from GitHub (HTTP ${response.status}). ${INSTALL_HINT}`);
99
+ if (!response.ok || !response.body) throw new AscCliUnavailableError(`Couldn't download asc ${ASC_CLI_VERSION} from GitHub (HTTP ${response.status}).\n${INSTALL_HINT}`);
100
100
  await fs.writeFile(partial, Buffer.from(await response.arrayBuffer()), { mode: 448 });
101
101
  const actual = await sha256Of(partial);
102
- if (actual !== expected) throw new AscCliUnavailableError(`The downloaded asc ${ASC_CLI_VERSION} does not match its published SHA-256 (expected ${expected}, got ${actual}), so it was deleted and not run. ${INSTALL_HINT}`);
102
+ if (actual !== expected) throw new AscCliUnavailableError([
103
+ `The downloaded asc ${ASC_CLI_VERSION} failed its SHA-256 check; it was deleted.`,
104
+ `Expected ${expected}`,
105
+ `Got ${actual}`,
106
+ INSTALL_HINT
107
+ ].join("\n"));
103
108
  await fs.chmod(partial, 493);
104
109
  await fs.rename(partial, binary);
105
110
  } finally {
@@ -202,7 +207,7 @@ const cliPrompts = {
202
207
  validate: (value) => value ? void 0 : "Enter the password."
203
208
  })),
204
209
  twoFactorCode: async (attempt) => answer(await p.text({
205
- message: attempt === 1 ? "Two-factor code from your trusted device (no device at hand? enter any wrong code once, and Apple texts one to your phone)" : "Verification code Apple sent to your phone",
210
+ message: attempt === 1 ? "Two-factor code (no trusted device? enter a wrong one for an SMS)" : "Verification code Apple sent to your phone",
206
211
  validate: (value) => /^\d{6}$/u.test(value?.trim() ?? "") ? void 0 : "Enter the 6-digit code."
207
212
  }))?.trim()
208
213
  };
@@ -257,9 +262,9 @@ const text = (value) => typeof value === "string" && value.trim() !== "" ? value
257
262
  const lastLine = (stderr) => stderr.split("\n").map((line) => line.trim()).filter(Boolean).at(-1);
258
263
  /** What to do about a key asc could not create, by what App Store Connect said. */
259
264
  const createFailureHint = (stderr) => {
260
- if (/agreement/iu.test(stderr)) return "The Apple Developer Program License Agreement has changed: the Account Holder accepts it at https://developer.apple.com/account, then run this again.";
261
- if (/limit|maximum/iu.test(stderr)) return `The team has as many active API keys as App Store Connect allows: revoke one you no longer use at ${ASC_INTEGRATIONS_URL}, then run this again.`;
262
- if (/account holder|admin|permission|forbidden|not allowed|403/iu.test(stderr)) return "Creating an App Store Connect API key needs the Account Holder or Admin role in the team: sign in with such an Apple Account, or ask one to run this.";
265
+ if (/agreement/iu.test(stderr)) return "The Account Holder must accept the new Apple Developer Program License\nAgreement at https://developer.apple.com/account, then run this again.";
266
+ if (/limit|maximum/iu.test(stderr)) return `The team has the most API keys App Store Connect allows. Revoke one at:\n${ASC_INTEGRATIONS_URL}`;
267
+ if (/account holder|admin|permission|forbidden|not allowed|403/iu.test(stderr)) return "Creating an API key needs the Account Holder or Admin role in the team.";
263
268
  };
264
269
  /** A P-256 private key, as App Store Connect issues one, or a thrown error. */
265
270
  const assertP8 = (p8) => {
@@ -295,7 +300,7 @@ const assertP8 = (p8) => {
295
300
  */
296
301
  const createAscCliKeyIssuer = ({ asc, env = process.env, prompts = cliPrompts } = {}) => async ({ name, role, appleId, teamId, chooseTeam }) => {
297
302
  const cli = asc ?? await findAscCli();
298
- p.log.info("Hot Updater signs in with asc, the open-source App Store Connect CLI. It keeps only the API key it issues: neither the password nor the session.");
303
+ p.log.info("Signing in with asc, the App Store Connect CLI. Only the issued key is kept.");
299
304
  const password = await prompts.password(appleId);
300
305
  if (password === void 0) throw new AscCommandError("Signing in was cancelled.", void 0);
301
306
  registerSecretValue(password);
@@ -647,6 +652,26 @@ const getDefaultDestination = ({ platform, useGeneric, deviceType }) => {
647
652
  });
648
653
  };
649
654
  //#endregion
655
+ //#region src/utils/formatCommand.ts
656
+ /**
657
+ * A command to print, broken at spaces with ` \` once a line would pass
658
+ * `width`, each continuation indented two spaces, so a terminal never wraps
659
+ * it mid-word and it still pastes as one command.
660
+ */
661
+ const formatCommand = (words, width = 74) => {
662
+ const lines = [];
663
+ let line = "";
664
+ for (const word of words) {
665
+ const next = line ? `${line} ${word}` : word;
666
+ if (line && next.length > width - 2) {
667
+ lines.push(`${line} \\`);
668
+ line = ` ${word}`;
669
+ } else line = next;
670
+ }
671
+ lines.push(line);
672
+ return lines.join("\n");
673
+ };
674
+ //#endregion
650
675
  //#region src/utils/parseXcodeProjectInfo.ts
651
676
  const parseXcodeProjectInfo = async (sourceDir) => {
652
677
  const files = await fs$1.promises.readdir(sourceDir);
@@ -669,8 +694,8 @@ const parseXcodeProjectInfo = async (sourceDir) => {
669
694
  const prettifyXcodebuildError = (error) => {
670
695
  if (error instanceof BuildFailedError) {
671
696
  const errorLines = error.summary.join("\n");
672
- const message = errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "Not valid provisioning profile is set in the project. Check out the Signing & Capabilities tab in Xcode." : error.message;
673
- return new BuildFailedError(`Xcodebuild failed: ${message}`, {
697
+ const message = errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "No valid provisioning profile is set in the project.\nCheck the Signing & Capabilities tab in Xcode." : error.message;
698
+ return new BuildFailedError(message, {
674
699
  logPath: error.logPath,
675
700
  summary: error.summary,
676
701
  cause: error.cause
@@ -933,12 +958,14 @@ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, inst
933
958
  xcconfig: build.xcconfig,
934
959
  xcodeScheme
935
960
  });
936
- p.log.info(`Xcode Archive Settings:
937
- Project ${xcodeProject.name}
938
- Scheme ${xcodeScheme}
939
- Platform ${platform}
940
- Command xcodebuild ${archiveArgs.join(" ")}
941
- `);
961
+ p.log.info([
962
+ "Xcode archive",
963
+ `Project ${xcodeProject.name}`,
964
+ `Scheme ${xcodeScheme}`,
965
+ `Platform ${platform}`,
966
+ "Command",
967
+ formatCommand(["xcodebuild", ...archiveArgs])
968
+ ].join("\n"));
942
969
  try {
943
970
  await runXcodebuildWithLogging({
944
971
  args: archiveArgs,
@@ -1029,14 +1056,16 @@ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeSc
1029
1056
  sourceDir,
1030
1057
  xcodeProject
1031
1058
  });
1032
- p.log.info(`Xcode Build Settings:
1033
- Project ${xcodeProject.name}
1034
- Scheme ${xcodeScheme}
1035
- Configuration ${configuration}
1036
- Platform ${platform}
1037
- Device Type ${deviceType}
1038
- Command xcodebuild ${buildArgs.join(" ")}
1039
- `);
1059
+ p.log.info([
1060
+ "Xcode build",
1061
+ `Project ${xcodeProject.name}`,
1062
+ `Scheme ${xcodeScheme}`,
1063
+ `Configuration ${configuration}`,
1064
+ `Platform ${platform}`,
1065
+ `Device type ${deviceType}`,
1066
+ "Command",
1067
+ formatCommand(["xcodebuild", ...buildArgs])
1068
+ ].join("\n"));
1040
1069
  let builtAppPath;
1041
1070
  try {
1042
1071
  await runXcodebuildWithLogging({
@@ -1317,7 +1346,7 @@ var TestFlightBuildExistsError = class extends Error {
1317
1346
  bundleVersion;
1318
1347
  name = "TestFlightBuildExistsError";
1319
1348
  constructor(shortVersion, bundleVersion, options) {
1320
- super(`App Store Connect already has build ${bundleVersion} of version ${shortVersion}. Each build of a version needs a new build number (CFBundleVersion).`, options);
1349
+ super(`App Store Connect already has build ${bundleVersion} of version ${shortVersion}.\nUse a new build number (CFBundleVersion).`, options);
1321
1350
  this.shortVersion = shortVersion;
1322
1351
  this.bundleVersion = bundleVersion;
1323
1352
  }
@@ -1504,15 +1533,15 @@ const codeSigningOff = async (iosScheme) => {
1504
1533
  * it (`assertTestFlightExportOptions`).
1505
1534
  */
1506
1535
  const resolveTestFlightUploadKey = async ({ scheme, iosScheme }) => {
1507
- if (iosScheme.simulator) throw new Error(`TestFlight takes an .ipa, and the scheme ${scheme} builds a simulator .app.`);
1536
+ if (iosScheme.simulator) throw new Error(`The scheme ${scheme} builds a simulator .app, not an .ipa for TestFlight.`);
1508
1537
  const configuration = iosScheme.configuration ?? "Release";
1509
- if (/debug$/i.test(configuration)) throw new Error(`TestFlight takes a release build, and the scheme ${scheme} builds the ${configuration} configuration, which loads its JavaScript from Metro.`);
1538
+ if (/debug$/i.test(configuration)) throw new Error(`The scheme ${scheme} builds ${configuration}; TestFlight takes a release build.`);
1510
1539
  const unsigned = await codeSigningOff(iosScheme);
1511
- if (unsigned) throw new Error(`TestFlight takes a signed .ipa, and the scheme ${scheme} turns code signing off (${unsigned}).`);
1512
- if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa. Set signing.ascApiKey, or an exportOptionsPlist with the method app-store-connect.`);
1540
+ if (unsigned) throw new Error(`The scheme ${scheme} turns code signing off:\n${unsigned}`);
1541
+ if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa.\nSet signing.ascApiKey, or exportOptionsPlist with method app-store-connect.`);
1513
1542
  if (iosScheme.exportOptionsPlist && fs$1.existsSync(schemeFile(iosScheme.exportOptionsPlist))) {
1514
1543
  const method = await readExportMethod(schemeFile(iosScheme.exportOptionsPlist));
1515
- if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`TestFlight takes an .ipa exported for App Store Connect, and the scheme ${scheme}'s exportOptionsPlist exports with the method ${method || "(none)"}. Set its method to app-store-connect.`);
1544
+ if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`The scheme ${scheme}'s exportOptionsPlist uses method ${method || "(none)"}.\nSet its method to app-store-connect for TestFlight.`);
1516
1545
  }
1517
1546
  return {
1518
1547
  kind: "asc-api-key",
@@ -1527,7 +1556,7 @@ const resolveTestFlightUploadKey = async ({ scheme, iosScheme }) => {
1527
1556
  const assertTestFlightExportOptions = async (schemeConfig) => {
1528
1557
  if (!schemeConfig.exportOptionsPlist) return;
1529
1558
  const method = await readExportMethod(schemeConfig.exportOptionsPlist);
1530
- if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`TestFlight takes an .ipa exported for App Store Connect, and the scheme ${schemeConfig.hotUpdaterSchemeName}'s exportOptionsPlist exports with the method ${method || "(none)"}. Set its method to app-store-connect.`);
1559
+ if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`The scheme ${schemeConfig.hotUpdaterSchemeName}'s exportOptionsPlist uses method ${method || "(none)"}.\nSet its method to app-store-connect for TestFlight.`);
1531
1560
  };
1532
1561
  /**
1533
1562
  * Requests to the App Store Connect API with `apiKey`: each carries a token
@@ -1562,8 +1591,8 @@ const createAppStoreConnectRequest = ({ apiKey, privateKey, fetch }) => {
1562
1591
  });
1563
1592
  if (!response.ok) {
1564
1593
  const error = new AppStoreConnectError(action, response.status, await readApiErrors(response), response.headers.get("retry-after"));
1565
- if (response.status === 401) throw new Error("App Store Connect refused the API key: check its key ID and issuer ID, and that the key is not revoked.", { cause: error });
1566
- if (response.status === 403) throw new Error(`${error.message} An API key with the Admin, App Manager, or Developer role uploads builds.`, { cause: error });
1594
+ if (response.status === 401) throw new Error("App Store Connect refused the API key.\nCheck its key ID and issuer ID, and that it isn't revoked.", { cause: error });
1595
+ if (response.status === 403) throw new Error(`${error.message}\nUploads need a key with the Admin, App Manager, or Developer role.`, { cause: error });
1567
1596
  throw error;
1568
1597
  }
1569
1598
  return response.status === 204 ? void 0 : await response.json();
@@ -1576,7 +1605,7 @@ const findApp = async (request, bundleId) => {
1576
1605
  "fields[apps]": "bundleId"
1577
1606
  })}`));
1578
1607
  const app = apps.find((app) => app.attributes?.bundleId === bundleId);
1579
- if (!app) throw new Error(`App Store Connect has no app with the bundle ID ${bundleId} that this API key can see. Create the app in App Store Connect, or use a key of its team.`);
1608
+ if (!app) throw new Error(`App Store Connect has no app ${bundleId} that this API key can see.\nCreate the app in App Store Connect, or use a key of its team.`);
1580
1609
  return {
1581
1610
  app,
1582
1611
  testFlightUrl: `https://appstoreconnect.apple.com/apps/${app.id}/testflight/ios`
@@ -1648,14 +1677,18 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1648
1677
  } }));
1649
1678
  } catch (error) {
1650
1679
  if (error instanceof AppStoreConnectError && error.status === 409 && error.errors.some((apiError) => apiError.code === DUPLICATE_ERROR_CODE)) {
1651
- if (unanswered) throw new Error(`App Store Connect already has build ${info.bundleVersion} of version ${info.shortVersion}, which a request of this upload it did not answer may have reserved. Upload with a new build number (CFBundleVersion).`, { cause: error });
1680
+ if (unanswered) throw new Error([
1681
+ `App Store Connect already has build ${info.bundleVersion} of version ${info.shortVersion}.`,
1682
+ "An unanswered request of this upload may have reserved it.",
1683
+ "Upload with a new build number (CFBundleVersion)."
1684
+ ].join("\n"), { cause: error });
1652
1685
  throw new TestFlightBuildExistsError(info.shortVersion, info.bundleVersion, { cause: error });
1653
1686
  }
1654
1687
  if (!isRetryable(error) || attempt === COMMIT_ATTEMPTS) throw error;
1655
1688
  if (!isRateLimited(error)) unanswered = true;
1656
1689
  await sleep(retryDelayMs(error, attempt));
1657
1690
  }
1658
- p.log.info(`Uploading ${ipaName} (version ${info.shortVersion}, build ${info.bundleVersion}) to App Store Connect`);
1691
+ p.log.info(`Uploading ${ipaName} ${info.shortVersion} (${info.bundleVersion}) to App Store Connect`);
1659
1692
  const removeUpload = async () => {
1660
1693
  for (let attempt = 1;; attempt++) try {
1661
1694
  await request("Removing the build upload", "DELETE", `/v1/buildUploads/${buildUploadId}`);
@@ -1663,7 +1696,11 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1663
1696
  } catch (error) {
1664
1697
  if (error instanceof AppStoreConnectError && error.status === 404) return;
1665
1698
  if (!isRetryable(error) || attempt === COMMIT_ATTEMPTS) {
1666
- p.log.warn(`App Store Connect kept the unfinished build upload ${buildUploadId}, which reserves build ${info.bundleVersion} of version ${info.shortVersion}: ${error.message} Upload with a new build number (CFBundleVersion).`);
1699
+ p.log.warn([
1700
+ `App Store Connect kept the unfinished upload of build ${info.bundleVersion}.`,
1701
+ error.message,
1702
+ "Upload with a new build number (CFBundleVersion)."
1703
+ ].join("\n"));
1667
1704
  return;
1668
1705
  }
1669
1706
  await sleep(retryDelayMs(error, attempt));
@@ -1705,7 +1742,12 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1705
1742
  } });
1706
1743
  break;
1707
1744
  } catch (error) {
1708
- const unconfirmed = () => new Error(`App Store Connect did not confirm the upload of build ${info.bundleVersion}: ${error.message} Check the build in TestFlight before you upload it again: ${testFlightUrl}`, { cause: error });
1745
+ const unconfirmed = () => new Error([
1746
+ `App Store Connect didn't confirm the upload of build ${info.bundleVersion}.`,
1747
+ error.message,
1748
+ "Check TestFlight before you upload it again:",
1749
+ testFlightUrl
1750
+ ].join("\n"), { cause: error });
1709
1751
  if (!isRetryable(error)) {
1710
1752
  if (commitUnanswered) throw unconfirmed();
1711
1753
  await removeUpload();
@@ -1731,8 +1773,13 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
1731
1773
  if (state.state === "PROCESSING") reportedProcessing = true;
1732
1774
  if (!state.state || IN_PROGRESS_STATES.has(state.state)) {
1733
1775
  if (Date.now() >= deadline) {
1734
- if (!reportedProcessing) throw new Error(`App Store Connect did not report build ${info.bundleVersion} processing${lastError instanceof Error ? `: ${lastError.message}` : state.state ? ` (${state.state}).` : "."} Check the build in TestFlight: ${testFlightUrl}`, { cause: lastError });
1735
- p.log.warn(`App Store Connect is still processing build ${info.bundleVersion}. It appears in TestFlight once processing finishes: ${testFlightUrl}`);
1776
+ if (!reportedProcessing) throw new Error([
1777
+ `App Store Connect didn't report build ${info.bundleVersion} processing.`,
1778
+ lastError instanceof Error ? lastError.message : state.state ? `State: ${state.state}` : void 0,
1779
+ "Check the build in TestFlight:",
1780
+ testFlightUrl
1781
+ ].filter(Boolean).join("\n"), { cause: lastError });
1782
+ p.log.warn(`App Store Connect is still processing build ${info.bundleVersion}.\nTestFlight lists it once processing finishes:\n${testFlightUrl}`);
1736
1783
  return {
1737
1784
  appId: app.id,
1738
1785
  buildUploadId,
@@ -1863,7 +1910,7 @@ const acquireWithLockf = async (lockPath) => {
1863
1910
  await fs$1.promises.mkdir(path$1.dirname(lockPath), { recursive: true });
1864
1911
  const release = await holdWithLockf(lockPath, false);
1865
1912
  if (release) return release;
1866
- p.log.info("Waiting for another build that signs in a keychain of its own under this macOS user.");
1913
+ p.log.info("Waiting for another build's signing keychain under this macOS user.");
1867
1914
  return await holdWithLockf(lockPath, true);
1868
1915
  };
1869
1916
  /**
@@ -1934,7 +1981,12 @@ const createSigningKeychain = async ({ security = runSecurity, directory = os$1.
1934
1981
  live = void 0;
1935
1982
  process.off("exit", restoreLive);
1936
1983
  await releaseLock().catch(() => void 0);
1937
- if (failures.length > 0) p.log.warn(`Restoring the keychains after the build failed (${failures.join("; ")}). Check \`security list-keychains -d user\` and \`security default-keychain -d user\`, and delete ${keychainPath} if it is left.`);
1984
+ if (failures.length > 0) p.log.warn([
1985
+ "Couldn't restore the keychains after the build.",
1986
+ ...failures,
1987
+ "Check security list-keychains -d user and default-keychain -d user,",
1988
+ `and delete ${keychainPath} if it is left.`
1989
+ ].join("\n"));
1938
1990
  })();
1939
1991
  return disposal;
1940
1992
  };
@@ -2045,8 +2097,13 @@ const revokeKeychainCertificates = async ({ certificates, apiKey, privateKey, fe
2045
2097
  const reason = error instanceof Error ? error.message : String(error);
2046
2098
  for (const certificate of certificates) if (!revoked.includes(describe(certificate))) failed.push(`${describe(certificate)}: ${reason}`);
2047
2099
  }
2048
- if (revoked.length > 0) p.log.info(`Revoked the signing certificates Xcode created for this build, whose keys the build's keychain held: ${revoked.join(", ")}.`);
2049
- if (failed.length > 0) p.log.warn(`Revoking the signing certificates Xcode created for this build failed, and their keys are deleted with the build's keychain: ${failed.join("; ")}. Revoke them at ${CERTIFICATES_URL}, since App Store Connect stops creating certificates at a team's maximum.`);
2100
+ if (revoked.length > 0) p.log.info(["Revoked the certificates Xcode created for this build:", ...revoked].join("\n"));
2101
+ if (failed.length > 0) p.log.warn([
2102
+ "Couldn't revoke the certificates Xcode created for this build.",
2103
+ ...failed,
2104
+ "Revoke them at:",
2105
+ CERTIFICATES_URL
2106
+ ].join("\n"));
2050
2107
  return {
2051
2108
  revoked,
2052
2109
  failed
@@ -2074,7 +2131,7 @@ const withSigningKeychain = async ({ enabled, apiKey, run, security, lockPath, a
2074
2131
  lockPath,
2075
2132
  acquireLock
2076
2133
  });
2077
- p.log.info("Signing in a keychain of this build's own, deleted when it ends (CI is set).");
2134
+ p.log.info("Signing in a keychain of this build's own (CI is set).");
2078
2135
  let cleanup;
2079
2136
  const release = () => {
2080
2137
  cleanup ??= (async () => {
@@ -2212,7 +2269,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
2212
2269
  buildDirectory
2213
2270
  });
2214
2271
  const appStoreConnect = await exportsForAppStoreConnect(schemeConfig);
2215
- if (isCI() && !appStoreConnect) p.log.info("This export isn't for App Store Connect, so it signs in the runner's own keychains and keeps the certificates Xcode creates.");
2272
+ if (isCI() && !appStoreConnect) p.log.info("Signing in the runner's own keychains (not an App Store Connect export).");
2216
2273
  return withSigningKeychain({
2217
2274
  enabled: isCI() && appStoreConnect,
2218
2275
  apiKey: signing.apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/apple-helper",
3
- "version": "1.2.0-rc.2",
3
+ "version": "1.2.0-rc.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -29,8 +29,8 @@
29
29
  "dependencies": {
30
30
  "execa": "9.5.2",
31
31
  "fast-xml-parser": "5.8.0",
32
- "@hot-updater/cli-tools": "1.2.0-rc.2",
33
- "@hot-updater/plugin-core": "1.2.0-rc.2"
32
+ "@hot-updater/plugin-core": "1.2.0-rc.3",
33
+ "@hot-updater/cli-tools": "1.2.0-rc.3"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20"