@hot-updater/apple-helper 1.2.0-rc.2 → 1.2.0-rc.4
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.cjs +107 -54
- package/dist/index.mjs +107 -54
- 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
|
|
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
|
|
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}
|
|
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(`
|
|
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(`
|
|
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(
|
|
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
|
|
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
|
|
293
|
-
if (/limit|maximum/iu.test(stderr)) return `The team has
|
|
294
|
-
if (/account holder|admin|permission|forbidden|not allowed|403/iu.test(stderr)) return "Creating an
|
|
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("
|
|
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") ? "
|
|
705
|
-
return new _hot_updater_cli_tools.BuildFailedError(
|
|
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(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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(
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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}
|
|
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(`
|
|
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(`
|
|
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(`
|
|
1544
|
-
if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa
|
|
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(`
|
|
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(`
|
|
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
|
|
1598
|
-
if (response.status === 403) throw new Error(`${error.message}
|
|
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
|
|
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(
|
|
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}
|
|
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(
|
|
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(
|
|
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(
|
|
1767
|
-
|
|
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
|
|
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(
|
|
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(
|
|
2081
|
-
if (failed.length > 0) _hot_updater_cli_tools.p.log.warn(
|
|
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
|
|
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 () => {
|
|
@@ -2227,7 +2284,6 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2227
2284
|
useGenericDestination: true,
|
|
2228
2285
|
minBundleId
|
|
2229
2286
|
});
|
|
2230
|
-
_hot_updater_cli_tools.p.log.success(`.app file created: ${path.default.basename(appPath)}`);
|
|
2231
2287
|
return {
|
|
2232
2288
|
buildDirectory,
|
|
2233
2289
|
buildArtifactPath: appPath,
|
|
@@ -2244,7 +2300,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2244
2300
|
buildDirectory
|
|
2245
2301
|
});
|
|
2246
2302
|
const appStoreConnect = await exportsForAppStoreConnect(schemeConfig);
|
|
2247
|
-
if ((0, _hot_updater_cli_tools.isCI)() && !appStoreConnect) _hot_updater_cli_tools.p.log.info("
|
|
2303
|
+
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
2304
|
return withSigningKeychain({
|
|
2249
2305
|
enabled: (0, _hot_updater_cli_tools.isCI)() && appStoreConnect,
|
|
2250
2306
|
apiKey: signing.apiKey,
|
|
@@ -2282,10 +2338,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
|
|
|
2282
2338
|
});
|
|
2283
2339
|
const appFromArchive = extractAppFromXcarchive(archivePath);
|
|
2284
2340
|
const archivedAppPath = appFromArchive ? path.default.join(archiveDir, path.default.basename(appFromArchive)) : void 0;
|
|
2285
|
-
if (appFromArchive && archivedAppPath) {
|
|
2286
|
-
await fs.default.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
|
|
2287
|
-
_hot_updater_cli_tools.p.log.success(".app extracted from .xcarchive");
|
|
2288
|
-
}
|
|
2341
|
+
if (appFromArchive && archivedAppPath) await fs.default.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
|
|
2289
2342
|
if (!schemeConfig.exportOptionsPlist && !authenticationArgs) return {
|
|
2290
2343
|
buildDirectory,
|
|
2291
2344
|
buildArtifactPath: archivePath,
|
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
|
|
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
|
|
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}
|
|
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(`
|
|
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(`
|
|
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(
|
|
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
|
|
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
|
|
261
|
-
if (/limit|maximum/iu.test(stderr)) return `The team has
|
|
262
|
-
if (/account holder|admin|permission|forbidden|not allowed|403/iu.test(stderr)) return "Creating an
|
|
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("
|
|
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") ? "
|
|
673
|
-
return new BuildFailedError(
|
|
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(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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(
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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}
|
|
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(`
|
|
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(`
|
|
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(`
|
|
1512
|
-
if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa
|
|
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(`
|
|
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(`
|
|
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
|
|
1566
|
-
if (response.status === 403) throw new Error(`${error.message}
|
|
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
|
|
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(
|
|
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}
|
|
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(
|
|
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(
|
|
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(
|
|
1735
|
-
|
|
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
|
|
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(
|
|
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(
|
|
2049
|
-
if (failed.length > 0) p.log.warn(
|
|
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
|
|
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 () => {
|
|
@@ -2195,7 +2252,6 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2195
2252
|
useGenericDestination: true,
|
|
2196
2253
|
minBundleId
|
|
2197
2254
|
});
|
|
2198
|
-
p.log.success(`.app file created: ${path$1.basename(appPath)}`);
|
|
2199
2255
|
return {
|
|
2200
2256
|
buildDirectory,
|
|
2201
2257
|
buildArtifactPath: appPath,
|
|
@@ -2212,7 +2268,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2212
2268
|
buildDirectory
|
|
2213
2269
|
});
|
|
2214
2270
|
const appStoreConnect = await exportsForAppStoreConnect(schemeConfig);
|
|
2215
|
-
if (isCI() && !appStoreConnect) p.log.info("
|
|
2271
|
+
if (isCI() && !appStoreConnect) p.log.info("Signing in the runner's own keychains (not an App Store Connect export).");
|
|
2216
2272
|
return withSigningKeychain({
|
|
2217
2273
|
enabled: isCI() && appStoreConnect,
|
|
2218
2274
|
apiKey: signing.apiKey,
|
|
@@ -2250,10 +2306,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
|
|
|
2250
2306
|
});
|
|
2251
2307
|
const appFromArchive = extractAppFromXcarchive(archivePath);
|
|
2252
2308
|
const archivedAppPath = appFromArchive ? path$1.join(archiveDir, path$1.basename(appFromArchive)) : void 0;
|
|
2253
|
-
if (appFromArchive && archivedAppPath) {
|
|
2254
|
-
await fs$1.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
|
|
2255
|
-
p.log.success(".app extracted from .xcarchive");
|
|
2256
|
-
}
|
|
2309
|
+
if (appFromArchive && archivedAppPath) await fs$1.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
|
|
2257
2310
|
if (!schemeConfig.exportOptionsPlist && !authenticationArgs) return {
|
|
2258
2311
|
buildDirectory,
|
|
2259
2312
|
buildArtifactPath: archivePath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/apple-helper",
|
|
3
|
-
"version": "1.2.0-rc.
|
|
3
|
+
"version": "1.2.0-rc.4",
|
|
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.
|
|
33
|
-
"@hot-updater/plugin-core": "1.2.0-rc.
|
|
32
|
+
"@hot-updater/cli-tools": "1.2.0-rc.4",
|
|
33
|
+
"@hot-updater/plugin-core": "1.2.0-rc.4"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20"
|