@hot-updater/android-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.
- package/dist/index.cjs +95 -36
- package/dist/index.mjs +95 -36
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -164,6 +164,26 @@ const createGradleLogger = ({ logPrefix }) => new _hot_updater_cli_tools.BuildLo
|
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
166
|
//#endregion
|
|
167
|
+
//#region src/utils/formatCommand.ts
|
|
168
|
+
/**
|
|
169
|
+
* A command to print, broken at spaces with ` \` once a line would pass
|
|
170
|
+
* `width`, each continuation indented two spaces, so a terminal never wraps
|
|
171
|
+
* it mid-word and it still pastes as one command.
|
|
172
|
+
*/
|
|
173
|
+
const formatCommand = (words, width = 74) => {
|
|
174
|
+
const lines = [];
|
|
175
|
+
let line = "";
|
|
176
|
+
for (const word of words) {
|
|
177
|
+
const next = line ? `${line} ${word}` : word;
|
|
178
|
+
if (line && next.length > width - 2) {
|
|
179
|
+
lines.push(`${line} \\`);
|
|
180
|
+
line = ` ${word}`;
|
|
181
|
+
} else line = next;
|
|
182
|
+
}
|
|
183
|
+
lines.push(line);
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
};
|
|
186
|
+
//#endregion
|
|
167
187
|
//#region src/builder/runGradle.ts
|
|
168
188
|
/** Why Gradle failed, without its command line. */
|
|
169
189
|
const describeFailure = (error) => {
|
|
@@ -190,12 +210,14 @@ async function runGradle({ tasks, logPrefix, args, env, androidProjectPath, appM
|
|
|
190
210
|
const gradleArgs = getTaskNames(appModuleName, tasks);
|
|
191
211
|
gradleArgs.push("-x", "lint");
|
|
192
212
|
if (args.extraParams) gradleArgs.push(...args.extraParams);
|
|
193
|
-
_hot_updater_cli_tools.p.log.info(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
_hot_updater_cli_tools.p.log.info([
|
|
214
|
+
"Gradle build",
|
|
215
|
+
`Project ${path.default.relative((0, _hot_updater_cli_tools.getCwd)(), androidProjectPath) || "."}`,
|
|
216
|
+
`App module ${appModuleName}`,
|
|
217
|
+
`Tasks ${tasks.join(", ")}`,
|
|
218
|
+
"Command",
|
|
219
|
+
formatCommand([path.default.basename(getGradleWrapper()), ...gradleArgs])
|
|
220
|
+
].join("\n"));
|
|
199
221
|
if ("port" in args && args.port != null) gradleArgs.push(`-PreactNativeDevServerPort=${args.port}`);
|
|
200
222
|
const logger = createGradleLogger({ logPrefix });
|
|
201
223
|
await logger.start();
|
|
@@ -257,7 +279,7 @@ async function getBuildOutputFilePath({ aab, appModuleName, buildDirectory, vari
|
|
|
257
279
|
const outputFile = `${appModuleName}-${variant}.${aab ? "aab" : "apk"}`;
|
|
258
280
|
const outputFilePath = path.default.join(buildDirectory, outputFile);
|
|
259
281
|
if (fs.default.existsSync(outputFilePath)) return outputFilePath;
|
|
260
|
-
_hot_updater_cli_tools.p.log.error(`
|
|
282
|
+
_hot_updater_cli_tools.p.log.error(`Gradle wrote no ${outputFilePath}.`);
|
|
261
283
|
process.exit(1);
|
|
262
284
|
}
|
|
263
285
|
//#endregion
|
|
@@ -277,7 +299,7 @@ const resolveGradleSigningEnv = async ({ schemeConfig, credentials }) => {
|
|
|
277
299
|
const reference = schemeConfig.signing?.keystore;
|
|
278
300
|
if (!reference) return;
|
|
279
301
|
if (isDebugVariant(schemeConfig.variant)) {
|
|
280
|
-
_hot_updater_cli_tools.p.log.info(`${schemeConfig.variant} signs with the debug keystore
|
|
302
|
+
_hot_updater_cli_tools.p.log.info(`${schemeConfig.variant} signs with the debug keystore; signing.keystore is unused.`);
|
|
281
303
|
return;
|
|
282
304
|
}
|
|
283
305
|
const keystore = await (credentials ?? (0, _hot_updater_cli_tools.createCredentialResolver)()).resolve({
|
|
@@ -430,7 +452,7 @@ const writeBaseModuleApk = async (aabPath, directory) => {
|
|
|
430
452
|
const readBuiltApp = async ({ artifactPath, androidProjectPath }) => {
|
|
431
453
|
const aapt2 = findAapt2(androidProjectPath);
|
|
432
454
|
if (!aapt2) {
|
|
433
|
-
_hot_updater_cli_tools.p.log.warn("aapt2
|
|
455
|
+
_hot_updater_cli_tools.p.log.warn("No aapt2 in the Android SDK build-tools.\nThe build reports the project files' app version and channel.");
|
|
434
456
|
return;
|
|
435
457
|
}
|
|
436
458
|
const directory = await fs.default.promises.mkdtemp(path.default.join(os.default.tmpdir(), "android-helper-"));
|
|
@@ -455,7 +477,7 @@ const readBuiltApp = async ({ artifactPath, androidProjectPath }) => {
|
|
|
455
477
|
};
|
|
456
478
|
const appVersion = await resolve(parseManifestVersionName(xmltree));
|
|
457
479
|
const channel = await resolve(parseManifestMetaData(xmltree, CHANNEL_META_DATA_KEY));
|
|
458
|
-
if (appVersion === void 0 || channel === void 0) _hot_updater_cli_tools.p.log.warn("
|
|
480
|
+
if (appVersion === void 0 || channel === void 0) _hot_updater_cli_tools.p.log.warn("Couldn't read the artifact's versionName or com.hotupdater.CHANNEL exactly.\nThe build reports the project files' value instead.");
|
|
459
481
|
const versionCode = parseManifestVersionCode(xmltree);
|
|
460
482
|
return {
|
|
461
483
|
appVersion: appVersion ?? null,
|
|
@@ -586,7 +608,7 @@ const readServiceAccount = (json) => {
|
|
|
586
608
|
throw new Error("The Play service account is not a JSON key file.");
|
|
587
609
|
}
|
|
588
610
|
const { client_email, private_key, token_uri } = parsed;
|
|
589
|
-
if (typeof client_email !== "string" || typeof private_key !== "string") throw new Error("The Play service account has no client_email or private_key
|
|
611
|
+
if (typeof client_email !== "string" || typeof private_key !== "string") throw new Error("The Play service account has no client_email or private_key.\nUse the JSON key file Google Cloud creates for it.");
|
|
590
612
|
(0, _hot_updater_cli_tools.registerSecretValue)(private_key);
|
|
591
613
|
let privateKey;
|
|
592
614
|
try {
|
|
@@ -632,7 +654,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
632
654
|
var PlayVersionCodeUsedError = class extends Error {
|
|
633
655
|
name = "PlayVersionCodeUsedError";
|
|
634
656
|
constructor(options) {
|
|
635
|
-
super("Google Play already has this .aab's version code
|
|
657
|
+
super("Google Play already has this .aab's version code.\nRaise versionCode for every build you upload.", options);
|
|
636
658
|
}
|
|
637
659
|
};
|
|
638
660
|
/**
|
|
@@ -642,8 +664,12 @@ var PlayVersionCodeUsedError = class extends Error {
|
|
|
642
664
|
const explain = (error, packageName) => {
|
|
643
665
|
if (!(error instanceof GooglePlayError)) return error;
|
|
644
666
|
const { status, apiMessage } = error;
|
|
645
|
-
if (status === 401 || /does not have permission/i.test(apiMessage)) return new Error(
|
|
646
|
-
|
|
667
|
+
if (status === 401 || /does not have permission/i.test(apiMessage)) return new Error([
|
|
668
|
+
`The Play service account can't release ${packageName}.`,
|
|
669
|
+
"Invite its email in Play Console (Users and permissions) to release to",
|
|
670
|
+
"testing tracks, and enable the Google Play Android Developer API."
|
|
671
|
+
].join("\n"), { cause: error });
|
|
672
|
+
if (/package not found/i.test(apiMessage)) return new Error(`Google Play has no app ${packageName}.\nCreate it in Play Console and upload its first .aab there.`, { cause: error });
|
|
647
673
|
if (/version code .*(already been used|already used)/i.test(apiMessage)) return new PlayVersionCodeUsedError({ cause: error });
|
|
648
674
|
return error;
|
|
649
675
|
};
|
|
@@ -668,7 +694,7 @@ const createPlayClient = ({ json, packageName, fetch }) => {
|
|
|
668
694
|
assertion
|
|
669
695
|
}).toString()
|
|
670
696
|
});
|
|
671
|
-
if (!response.ok) throw new Error(`Google refused the Play service account (HTTP ${response.status})
|
|
697
|
+
if (!response.ok) throw new Error(`Google refused the Play service account (HTTP ${response.status}).\n${await readApiMessage(response) || "Check that its key is current and the service account is enabled."}`);
|
|
672
698
|
const { access_token } = await response.json();
|
|
673
699
|
if (typeof access_token !== "string" || !access_token) throw new Error("Google answered the Play service account with no access token.");
|
|
674
700
|
(0, _hot_updater_cli_tools.registerSecretValue)(access_token);
|
|
@@ -746,7 +772,11 @@ const assertPlaySigned = async (aabPath) => {
|
|
|
746
772
|
"-jarfile",
|
|
747
773
|
aabPath
|
|
748
774
|
], { reject: false });
|
|
749
|
-
if (exitCode === 0 && ANDROID_DEBUG_SIGNER.test(String(stdout))) throw new Error(
|
|
775
|
+
if (exitCode === 0 && ANDROID_DEBUG_SIGNER.test(String(stdout))) throw new Error([
|
|
776
|
+
"Google Play refuses an .aab signed with the Android debug keystore.",
|
|
777
|
+
"Set the scheme's signing.keystore, or a release signingConfig in",
|
|
778
|
+
"android/app/build.gradle, then build again."
|
|
779
|
+
].join("\n"));
|
|
750
780
|
};
|
|
751
781
|
/**
|
|
752
782
|
* Uploads an `.aab` to Google Play's internal testing track with the Google
|
|
@@ -797,7 +827,7 @@ const uploadToPlay = async ({ aabPath, packageName, serviceAccount: { json }, fe
|
|
|
797
827
|
};
|
|
798
828
|
} catch (error) {
|
|
799
829
|
if (!(error instanceof GooglePlayError) || !DRAFT_APP_ERRORS.some((message) => error.apiMessage.includes(message))) throw error;
|
|
800
|
-
_hot_updater_cli_tools.p.log.warn(
|
|
830
|
+
_hot_updater_cli_tools.p.log.warn(`${packageName} isn't published yet, so version code ${versionCode} is a draft.\nRoll it out in Play Console.`);
|
|
801
831
|
await release("draft");
|
|
802
832
|
return {
|
|
803
833
|
versionCode,
|
|
@@ -819,10 +849,14 @@ const commit = async ({ request, edit, retryDelayMs }) => {
|
|
|
819
849
|
let changesNotSentForReview = false;
|
|
820
850
|
for (let attempt = 1;; attempt++) try {
|
|
821
851
|
await request("Committing the Google Play edit", "POST", `${edit}:commit${changesNotSentForReview ? "?changesNotSentForReview=true" : ""}`);
|
|
822
|
-
if (changesNotSentForReview) _hot_updater_cli_tools.p.log.warn("Google Play saved the release without sending it for review
|
|
852
|
+
if (changesNotSentForReview) _hot_updater_cli_tools.p.log.warn("Google Play saved the release without sending it for review.\nSend the changes for review in Play Console.");
|
|
823
853
|
return;
|
|
824
854
|
} catch (error) {
|
|
825
|
-
if (!(error instanceof GooglePlayError) || error.status >= 500 || error.status === 429) throw new Error(
|
|
855
|
+
if (!(error instanceof GooglePlayError) || error.status >= 500 || error.status === 429) throw new Error([
|
|
856
|
+
"Google Play didn't confirm the release.",
|
|
857
|
+
error instanceof Error ? error.message : String(error),
|
|
858
|
+
"Check internal testing in Play Console before you upload it again."
|
|
859
|
+
].join("\n"), { cause: error });
|
|
826
860
|
if (error.status !== 400) throw error;
|
|
827
861
|
if (!changesNotSentForReview && error.apiMessage.includes(NEEDS_CHANGES_NOT_SENT_FOR_REVIEW)) {
|
|
828
862
|
changesNotSentForReview = true;
|
|
@@ -925,9 +959,9 @@ const confirmedOffset = (response) => {
|
|
|
925
959
|
* from new apps, or a debug variant, which Play refuses.
|
|
926
960
|
*/
|
|
927
961
|
const resolvePlayUploadServiceAccount = ({ scheme, androidScheme }) => {
|
|
928
|
-
if (androidScheme.aab === false) throw new Error(`
|
|
962
|
+
if (androidScheme.aab === false) throw new Error(`The scheme ${scheme} builds an .apk (aab: false); Google Play takes an .aab.`);
|
|
929
963
|
const variant = androidScheme.variant ?? "Release";
|
|
930
|
-
if (/debug$/i.test(variant)) throw new Error(`
|
|
964
|
+
if (/debug$/i.test(variant)) throw new Error(`The scheme ${scheme} builds ${variant}; Google Play takes a release build.`);
|
|
931
965
|
return {
|
|
932
966
|
kind: "play-service-account",
|
|
933
967
|
name: "play-service-account"
|
|
@@ -1231,7 +1265,7 @@ const signInWithGoogle = async ({ client, scopes, onUrl, fetch = globalThis.fetc
|
|
|
1231
1265
|
}).toString()
|
|
1232
1266
|
});
|
|
1233
1267
|
const token = await response.json().catch(() => ({}));
|
|
1234
|
-
if (!response.ok || typeof token.access_token !== "string") throw new PlayServiceAccountError(token.error === "invalid_client" ? "sign-in-not-configured" : "sign-in-cancelled", `Google refused the sign-in (HTTP ${response.status})
|
|
1268
|
+
if (!response.ok || typeof token.access_token !== "string") throw new PlayServiceAccountError(token.error === "invalid_client" ? "sign-in-not-configured" : "sign-in-cancelled", `Google refused the sign-in (HTTP ${response.status}).${typeof token.error_description === "string" ? `\n${token.error_description}` : typeof token.error === "string" ? `\n${token.error}` : ""}`);
|
|
1235
1269
|
(0, _hot_updater_cli_tools.registerSecretValue)(token.access_token);
|
|
1236
1270
|
return {
|
|
1237
1271
|
accessToken: token.access_token,
|
|
@@ -1286,11 +1320,11 @@ const newProjectId = () => `${PLAY_SERVICE_ACCOUNT_ID}-${node_crypto.default.ran
|
|
|
1286
1320
|
const isNotFound = (error) => error instanceof GoogleApiError && error.status === 404;
|
|
1287
1321
|
const isConflict = (error) => error instanceof GoogleApiError && error.status === 409;
|
|
1288
1322
|
/** The API is disabled in the OAuth client's own project: waiting does not help. */
|
|
1289
|
-
const clientApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}
|
|
1323
|
+
const clientApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}\nEnable it in the OAuth client's project (${GOOGLE_OAUTH_CLIENT_ENV_NAMES.clientId}).`, { cause: error });
|
|
1290
1324
|
/** Google's own words, after a message that names the likely cause. */
|
|
1291
|
-
const googleSays = (error) => error instanceof GoogleApiError && error.apiMessage ?
|
|
1325
|
+
const googleSays = (error) => error instanceof GoogleApiError && error.apiMessage ? `\nGoogle: ${error.apiMessage}` : "";
|
|
1292
1326
|
/** A Play Console refusal, as `not-play-admin`. */
|
|
1293
|
-
const notPlayAdmin = (developerId, error) => new PlayServiceAccountError("not-play-admin", `
|
|
1327
|
+
const notPlayAdmin = (developerId, error) => new PlayServiceAccountError("not-play-admin", `This Google Account isn't an admin of Play Console developer ${developerId}.${googleSays(error)}\nSign in with an account that has the Admin permission there.`, { cause: error });
|
|
1294
1328
|
const isDenied = (error) => error instanceof GoogleApiError && (error.status === 403 || error.apiStatus === "PERMISSION_DENIED");
|
|
1295
1329
|
/**
|
|
1296
1330
|
* `IssuePlayServiceAccount` with Google's own APIs, after a sign-in on
|
|
@@ -1311,7 +1345,11 @@ const isDenied = (error) => error instanceof GoogleApiError && (error.status ===
|
|
|
1311
1345
|
*/
|
|
1312
1346
|
const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor } = {}) => async (request) => {
|
|
1313
1347
|
const { packageName, oauthClient, onStep = () => {} } = request;
|
|
1314
|
-
if (!oauthClient?.clientId) throw new PlayServiceAccountError("sign-in-not-configured",
|
|
1348
|
+
if (!oauthClient?.clientId) throw new PlayServiceAccountError("sign-in-not-configured", [
|
|
1349
|
+
"This build of Hot Updater has no Google OAuth client to sign in with.",
|
|
1350
|
+
"Set these to a \"Desktop app\" OAuth client:",
|
|
1351
|
+
`${GOOGLE_OAUTH_CLIENT_ENV_NAMES.clientId}, ${GOOGLE_OAUTH_CLIENT_ENV_NAMES.clientSecret}`
|
|
1352
|
+
].join("\n"));
|
|
1315
1353
|
const signIn = await signInWithGoogle({
|
|
1316
1354
|
client: oauthClient,
|
|
1317
1355
|
scopes: SCOPES,
|
|
@@ -1323,14 +1361,14 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor } =
|
|
|
1323
1361
|
};
|
|
1324
1362
|
try {
|
|
1325
1363
|
const missing = SCOPES.filter((scope) => !signIn.scopes.includes(scope));
|
|
1326
|
-
if (missing.length > 0) throw new PlayServiceAccountError("scope-not-granted", `The sign-in
|
|
1364
|
+
if (missing.length > 0) throw new PlayServiceAccountError("scope-not-granted", `The sign-in didn't allow ${missing.map((scope) => scope === ANDROID_PUBLISHER_SCOPE ? "managing Google Play" : "managing Google Cloud").join(" or ")}.\nRun this again and keep every box on Google's consent screen ticked.`);
|
|
1327
1365
|
const google = createGoogleRequest({
|
|
1328
1366
|
accessToken: signIn.accessToken,
|
|
1329
1367
|
fetch
|
|
1330
1368
|
});
|
|
1331
1369
|
const developerInput = await request.askDeveloperId();
|
|
1332
1370
|
const developerId = parseDeveloperId(developerInput);
|
|
1333
|
-
if (developerId === void 0) throw new PlayServiceAccountError("developer-not-found", `"${developerInput.trim()}" is
|
|
1371
|
+
if (developerId === void 0) throw new PlayServiceAccountError("developer-not-found", `"${developerInput.trim()}" is not a developer account ID.\nIt is the number after /developers/ in Play Console's address.`);
|
|
1334
1372
|
onStep("Checking the Play Console developer account");
|
|
1335
1373
|
await checkPlayConsole({
|
|
1336
1374
|
google,
|
|
@@ -1402,7 +1440,7 @@ const checkPlayConsole = async ({ google, developerId, packageName }) => {
|
|
|
1402
1440
|
await google("Listing the Play Console users", "GET", `${PUBLISHER_API}/developers/${developerId}/users?pageSize=1`);
|
|
1403
1441
|
} catch (error) {
|
|
1404
1442
|
if (isServiceDisabled(error)) throw clientApiDisabled(error);
|
|
1405
|
-
if (isNotFound(error)) throw new PlayServiceAccountError("developer-not-found", `Play Console has no developer account ${developerId}
|
|
1443
|
+
if (isNotFound(error)) throw new PlayServiceAccountError("developer-not-found", `Play Console has no developer account ${developerId}.${googleSays(error)}\nIt is the number after /developers/ in Play Console's address.`, { cause: error });
|
|
1406
1444
|
if (isDenied(error)) throw notPlayAdmin(developerId, error);
|
|
1407
1445
|
throw error;
|
|
1408
1446
|
}
|
|
@@ -1463,9 +1501,18 @@ const chooseProject = async ({ google, request, sleep, onStep }) => {
|
|
|
1463
1501
|
};
|
|
1464
1502
|
} catch (error) {
|
|
1465
1503
|
if (!(error instanceof GoogleApiError)) throw error;
|
|
1466
|
-
if (error.status === 429 || error.apiStatus === "RESOURCE_EXHAUSTED" || /quota/iu.test(error.apiMessage)) throw new PlayServiceAccountError("project-quota",
|
|
1467
|
-
|
|
1468
|
-
|
|
1504
|
+
if (error.status === 429 || error.apiStatus === "RESOURCE_EXHAUSTED" || /quota/iu.test(error.apiMessage)) throw new PlayServiceAccountError("project-quota", [
|
|
1505
|
+
"Google Cloud has no project quota left for this account.",
|
|
1506
|
+
error.apiMessage || "Quota exceeded.",
|
|
1507
|
+
"Run this again and pick an existing project, or delete one at:",
|
|
1508
|
+
"https://console.cloud.google.com/cloud-resource-manager"
|
|
1509
|
+
].join("\n"), { cause: error });
|
|
1510
|
+
if (/terms of service/iu.test(error.apiMessage)) throw new PlayServiceAccountError("project-create-denied", "This Google Account hasn't accepted the Google Cloud Terms of Service.\nAccept them at https://console.cloud.google.com, then run this again.", { cause: error });
|
|
1511
|
+
if (isDenied(error) || error.apiStatus === "FAILED_PRECONDITION") throw new PlayServiceAccountError("project-create-denied", [
|
|
1512
|
+
"This Google Account may not create Google Cloud projects.",
|
|
1513
|
+
error.apiMessage || "Permission denied.",
|
|
1514
|
+
"Pick an existing project, or ask an admin for the Project Creator role."
|
|
1515
|
+
].join("\n"), { cause: error });
|
|
1469
1516
|
throw error;
|
|
1470
1517
|
}
|
|
1471
1518
|
};
|
|
@@ -1485,7 +1532,7 @@ const whilePropagating = async ({ run, project, sleep, notFound = false }) => {
|
|
|
1485
1532
|
const disabled = isServiceDisabled(error);
|
|
1486
1533
|
if (disabled && !consumerIs(error, project)) throw clientApiDisabled(error);
|
|
1487
1534
|
if (!disabled && !(notFound && isNotFound(error)) || attempt >= PROPAGATION_ATTEMPTS) {
|
|
1488
|
-
if (disabled) throw new PlayServiceAccountError("api-not-enabled", `Google is still enabling the APIs in ${project.projectId}
|
|
1535
|
+
if (disabled) throw new PlayServiceAccountError("api-not-enabled", `Google is still enabling the APIs in ${project.projectId}.\n${error.apiMessage}\nWait a few minutes, then run this again.`, { cause: error });
|
|
1489
1536
|
throw error;
|
|
1490
1537
|
}
|
|
1491
1538
|
await sleep(PROPAGATION_DELAY_MS);
|
|
@@ -1508,7 +1555,11 @@ const enableServices = async ({ google, projectId, sleep }) => {
|
|
|
1508
1555
|
sleep
|
|
1509
1556
|
});
|
|
1510
1557
|
} catch (error) {
|
|
1511
|
-
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied",
|
|
1558
|
+
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied", [
|
|
1559
|
+
`This Google Account may not enable APIs in ${projectId}.`,
|
|
1560
|
+
error.apiMessage || "Permission denied.",
|
|
1561
|
+
"Pick a project you own, or ask its owner for the Editor role."
|
|
1562
|
+
].join("\n"), { cause: error });
|
|
1512
1563
|
throw error;
|
|
1513
1564
|
}
|
|
1514
1565
|
};
|
|
@@ -1530,7 +1581,11 @@ const ensureServiceAccount = async ({ google, project, sleep }) => {
|
|
|
1530
1581
|
})).email ?? email;
|
|
1531
1582
|
} catch (error) {
|
|
1532
1583
|
if (isConflict(error)) return email;
|
|
1533
|
-
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied",
|
|
1584
|
+
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied", [
|
|
1585
|
+
`This Google Account may not create service accounts in ${projectId}.`,
|
|
1586
|
+
error.apiMessage || "Permission denied.",
|
|
1587
|
+
"Pick a project you own, or ask for the Service Account Admin role."
|
|
1588
|
+
].join("\n"), { cause: error });
|
|
1534
1589
|
throw error;
|
|
1535
1590
|
}
|
|
1536
1591
|
};
|
|
@@ -1602,7 +1657,11 @@ const createKey = async ({ google, project, email, sleep }) => {
|
|
|
1602
1657
|
})
|
|
1603
1658
|
});
|
|
1604
1659
|
} catch (error) {
|
|
1605
|
-
if (error instanceof GoogleApiError && (error.violations.includes(KEY_CREATION_CONSTRAINT) || /disableServiceAccountKeyCreation/u.test(error.apiMessage))) throw new PlayServiceAccountError("key-creation-disabled",
|
|
1660
|
+
if (error instanceof GoogleApiError && (error.violations.includes(KEY_CREATION_CONSTRAINT) || /disableServiceAccountKeyCreation/u.test(error.apiMessage))) throw new PlayServiceAccountError("key-creation-disabled", [
|
|
1661
|
+
`iam.disableServiceAccountKeyCreation forbids keys in ${projectId}.`,
|
|
1662
|
+
"Ask an organization policy admin to allow keys for this project,",
|
|
1663
|
+
"or run this again with a project outside the organization."
|
|
1664
|
+
].join("\n"), { cause: error });
|
|
1606
1665
|
throw error;
|
|
1607
1666
|
}
|
|
1608
1667
|
if (!key.name || !key.privateKeyData) throw new Error("Google Cloud created the key without its JSON file.");
|
package/dist/index.mjs
CHANGED
|
@@ -134,6 +134,26 @@ const createGradleLogger = ({ logPrefix }) => new BuildLogger({
|
|
|
134
134
|
}
|
|
135
135
|
});
|
|
136
136
|
//#endregion
|
|
137
|
+
//#region src/utils/formatCommand.ts
|
|
138
|
+
/**
|
|
139
|
+
* A command to print, broken at spaces with ` \` once a line would pass
|
|
140
|
+
* `width`, each continuation indented two spaces, so a terminal never wraps
|
|
141
|
+
* it mid-word and it still pastes as one command.
|
|
142
|
+
*/
|
|
143
|
+
const formatCommand = (words, width = 74) => {
|
|
144
|
+
const lines = [];
|
|
145
|
+
let line = "";
|
|
146
|
+
for (const word of words) {
|
|
147
|
+
const next = line ? `${line} ${word}` : word;
|
|
148
|
+
if (line && next.length > width - 2) {
|
|
149
|
+
lines.push(`${line} \\`);
|
|
150
|
+
line = ` ${word}`;
|
|
151
|
+
} else line = next;
|
|
152
|
+
}
|
|
153
|
+
lines.push(line);
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
137
157
|
//#region src/builder/runGradle.ts
|
|
138
158
|
/** Why Gradle failed, without its command line. */
|
|
139
159
|
const describeFailure = (error) => {
|
|
@@ -160,12 +180,14 @@ async function runGradle({ tasks, logPrefix, args, env, androidProjectPath, appM
|
|
|
160
180
|
const gradleArgs = getTaskNames(appModuleName, tasks);
|
|
161
181
|
gradleArgs.push("-x", "lint");
|
|
162
182
|
if (args.extraParams) gradleArgs.push(...args.extraParams);
|
|
163
|
-
p.log.info(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
p.log.info([
|
|
184
|
+
"Gradle build",
|
|
185
|
+
`Project ${path.relative(getCwd(), androidProjectPath) || "."}`,
|
|
186
|
+
`App module ${appModuleName}`,
|
|
187
|
+
`Tasks ${tasks.join(", ")}`,
|
|
188
|
+
"Command",
|
|
189
|
+
formatCommand([path.basename(getGradleWrapper()), ...gradleArgs])
|
|
190
|
+
].join("\n"));
|
|
169
191
|
if ("port" in args && args.port != null) gradleArgs.push(`-PreactNativeDevServerPort=${args.port}`);
|
|
170
192
|
const logger = createGradleLogger({ logPrefix });
|
|
171
193
|
await logger.start();
|
|
@@ -227,7 +249,7 @@ async function getBuildOutputFilePath({ aab, appModuleName, buildDirectory, vari
|
|
|
227
249
|
const outputFile = `${appModuleName}-${variant}.${aab ? "aab" : "apk"}`;
|
|
228
250
|
const outputFilePath = path.join(buildDirectory, outputFile);
|
|
229
251
|
if (fs.existsSync(outputFilePath)) return outputFilePath;
|
|
230
|
-
p.log.error(`
|
|
252
|
+
p.log.error(`Gradle wrote no ${outputFilePath}.`);
|
|
231
253
|
process.exit(1);
|
|
232
254
|
}
|
|
233
255
|
//#endregion
|
|
@@ -247,7 +269,7 @@ const resolveGradleSigningEnv = async ({ schemeConfig, credentials }) => {
|
|
|
247
269
|
const reference = schemeConfig.signing?.keystore;
|
|
248
270
|
if (!reference) return;
|
|
249
271
|
if (isDebugVariant(schemeConfig.variant)) {
|
|
250
|
-
p.log.info(`${schemeConfig.variant} signs with the debug keystore
|
|
272
|
+
p.log.info(`${schemeConfig.variant} signs with the debug keystore; signing.keystore is unused.`);
|
|
251
273
|
return;
|
|
252
274
|
}
|
|
253
275
|
const keystore = await (credentials ?? createCredentialResolver()).resolve({
|
|
@@ -400,7 +422,7 @@ const writeBaseModuleApk = async (aabPath, directory) => {
|
|
|
400
422
|
const readBuiltApp = async ({ artifactPath, androidProjectPath }) => {
|
|
401
423
|
const aapt2 = findAapt2(androidProjectPath);
|
|
402
424
|
if (!aapt2) {
|
|
403
|
-
p.log.warn("aapt2
|
|
425
|
+
p.log.warn("No aapt2 in the Android SDK build-tools.\nThe build reports the project files' app version and channel.");
|
|
404
426
|
return;
|
|
405
427
|
}
|
|
406
428
|
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "android-helper-"));
|
|
@@ -425,7 +447,7 @@ const readBuiltApp = async ({ artifactPath, androidProjectPath }) => {
|
|
|
425
447
|
};
|
|
426
448
|
const appVersion = await resolve(parseManifestVersionName(xmltree));
|
|
427
449
|
const channel = await resolve(parseManifestMetaData(xmltree, CHANNEL_META_DATA_KEY));
|
|
428
|
-
if (appVersion === void 0 || channel === void 0) p.log.warn("
|
|
450
|
+
if (appVersion === void 0 || channel === void 0) p.log.warn("Couldn't read the artifact's versionName or com.hotupdater.CHANNEL exactly.\nThe build reports the project files' value instead.");
|
|
429
451
|
const versionCode = parseManifestVersionCode(xmltree);
|
|
430
452
|
return {
|
|
431
453
|
appVersion: appVersion ?? null,
|
|
@@ -556,7 +578,7 @@ const readServiceAccount = (json) => {
|
|
|
556
578
|
throw new Error("The Play service account is not a JSON key file.");
|
|
557
579
|
}
|
|
558
580
|
const { client_email, private_key, token_uri } = parsed;
|
|
559
|
-
if (typeof client_email !== "string" || typeof private_key !== "string") throw new Error("The Play service account has no client_email or private_key
|
|
581
|
+
if (typeof client_email !== "string" || typeof private_key !== "string") throw new Error("The Play service account has no client_email or private_key.\nUse the JSON key file Google Cloud creates for it.");
|
|
560
582
|
registerSecretValue(private_key);
|
|
561
583
|
let privateKey;
|
|
562
584
|
try {
|
|
@@ -602,7 +624,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
602
624
|
var PlayVersionCodeUsedError = class extends Error {
|
|
603
625
|
name = "PlayVersionCodeUsedError";
|
|
604
626
|
constructor(options) {
|
|
605
|
-
super("Google Play already has this .aab's version code
|
|
627
|
+
super("Google Play already has this .aab's version code.\nRaise versionCode for every build you upload.", options);
|
|
606
628
|
}
|
|
607
629
|
};
|
|
608
630
|
/**
|
|
@@ -612,8 +634,12 @@ var PlayVersionCodeUsedError = class extends Error {
|
|
|
612
634
|
const explain = (error, packageName) => {
|
|
613
635
|
if (!(error instanceof GooglePlayError)) return error;
|
|
614
636
|
const { status, apiMessage } = error;
|
|
615
|
-
if (status === 401 || /does not have permission/i.test(apiMessage)) return new Error(
|
|
616
|
-
|
|
637
|
+
if (status === 401 || /does not have permission/i.test(apiMessage)) return new Error([
|
|
638
|
+
`The Play service account can't release ${packageName}.`,
|
|
639
|
+
"Invite its email in Play Console (Users and permissions) to release to",
|
|
640
|
+
"testing tracks, and enable the Google Play Android Developer API."
|
|
641
|
+
].join("\n"), { cause: error });
|
|
642
|
+
if (/package not found/i.test(apiMessage)) return new Error(`Google Play has no app ${packageName}.\nCreate it in Play Console and upload its first .aab there.`, { cause: error });
|
|
617
643
|
if (/version code .*(already been used|already used)/i.test(apiMessage)) return new PlayVersionCodeUsedError({ cause: error });
|
|
618
644
|
return error;
|
|
619
645
|
};
|
|
@@ -638,7 +664,7 @@ const createPlayClient = ({ json, packageName, fetch }) => {
|
|
|
638
664
|
assertion
|
|
639
665
|
}).toString()
|
|
640
666
|
});
|
|
641
|
-
if (!response.ok) throw new Error(`Google refused the Play service account (HTTP ${response.status})
|
|
667
|
+
if (!response.ok) throw new Error(`Google refused the Play service account (HTTP ${response.status}).\n${await readApiMessage(response) || "Check that its key is current and the service account is enabled."}`);
|
|
642
668
|
const { access_token } = await response.json();
|
|
643
669
|
if (typeof access_token !== "string" || !access_token) throw new Error("Google answered the Play service account with no access token.");
|
|
644
670
|
registerSecretValue(access_token);
|
|
@@ -716,7 +742,11 @@ const assertPlaySigned = async (aabPath) => {
|
|
|
716
742
|
"-jarfile",
|
|
717
743
|
aabPath
|
|
718
744
|
], { reject: false });
|
|
719
|
-
if (exitCode === 0 && ANDROID_DEBUG_SIGNER.test(String(stdout))) throw new Error(
|
|
745
|
+
if (exitCode === 0 && ANDROID_DEBUG_SIGNER.test(String(stdout))) throw new Error([
|
|
746
|
+
"Google Play refuses an .aab signed with the Android debug keystore.",
|
|
747
|
+
"Set the scheme's signing.keystore, or a release signingConfig in",
|
|
748
|
+
"android/app/build.gradle, then build again."
|
|
749
|
+
].join("\n"));
|
|
720
750
|
};
|
|
721
751
|
/**
|
|
722
752
|
* Uploads an `.aab` to Google Play's internal testing track with the Google
|
|
@@ -767,7 +797,7 @@ const uploadToPlay = async ({ aabPath, packageName, serviceAccount: { json }, fe
|
|
|
767
797
|
};
|
|
768
798
|
} catch (error) {
|
|
769
799
|
if (!(error instanceof GooglePlayError) || !DRAFT_APP_ERRORS.some((message) => error.apiMessage.includes(message))) throw error;
|
|
770
|
-
p.log.warn(
|
|
800
|
+
p.log.warn(`${packageName} isn't published yet, so version code ${versionCode} is a draft.\nRoll it out in Play Console.`);
|
|
771
801
|
await release("draft");
|
|
772
802
|
return {
|
|
773
803
|
versionCode,
|
|
@@ -789,10 +819,14 @@ const commit = async ({ request, edit, retryDelayMs }) => {
|
|
|
789
819
|
let changesNotSentForReview = false;
|
|
790
820
|
for (let attempt = 1;; attempt++) try {
|
|
791
821
|
await request("Committing the Google Play edit", "POST", `${edit}:commit${changesNotSentForReview ? "?changesNotSentForReview=true" : ""}`);
|
|
792
|
-
if (changesNotSentForReview) p.log.warn("Google Play saved the release without sending it for review
|
|
822
|
+
if (changesNotSentForReview) p.log.warn("Google Play saved the release without sending it for review.\nSend the changes for review in Play Console.");
|
|
793
823
|
return;
|
|
794
824
|
} catch (error) {
|
|
795
|
-
if (!(error instanceof GooglePlayError) || error.status >= 500 || error.status === 429) throw new Error(
|
|
825
|
+
if (!(error instanceof GooglePlayError) || error.status >= 500 || error.status === 429) throw new Error([
|
|
826
|
+
"Google Play didn't confirm the release.",
|
|
827
|
+
error instanceof Error ? error.message : String(error),
|
|
828
|
+
"Check internal testing in Play Console before you upload it again."
|
|
829
|
+
].join("\n"), { cause: error });
|
|
796
830
|
if (error.status !== 400) throw error;
|
|
797
831
|
if (!changesNotSentForReview && error.apiMessage.includes(NEEDS_CHANGES_NOT_SENT_FOR_REVIEW)) {
|
|
798
832
|
changesNotSentForReview = true;
|
|
@@ -895,9 +929,9 @@ const confirmedOffset = (response) => {
|
|
|
895
929
|
* from new apps, or a debug variant, which Play refuses.
|
|
896
930
|
*/
|
|
897
931
|
const resolvePlayUploadServiceAccount = ({ scheme, androidScheme }) => {
|
|
898
|
-
if (androidScheme.aab === false) throw new Error(`
|
|
932
|
+
if (androidScheme.aab === false) throw new Error(`The scheme ${scheme} builds an .apk (aab: false); Google Play takes an .aab.`);
|
|
899
933
|
const variant = androidScheme.variant ?? "Release";
|
|
900
|
-
if (/debug$/i.test(variant)) throw new Error(`
|
|
934
|
+
if (/debug$/i.test(variant)) throw new Error(`The scheme ${scheme} builds ${variant}; Google Play takes a release build.`);
|
|
901
935
|
return {
|
|
902
936
|
kind: "play-service-account",
|
|
903
937
|
name: "play-service-account"
|
|
@@ -1201,7 +1235,7 @@ const signInWithGoogle = async ({ client, scopes, onUrl, fetch = globalThis.fetc
|
|
|
1201
1235
|
}).toString()
|
|
1202
1236
|
});
|
|
1203
1237
|
const token = await response.json().catch(() => ({}));
|
|
1204
|
-
if (!response.ok || typeof token.access_token !== "string") throw new PlayServiceAccountError(token.error === "invalid_client" ? "sign-in-not-configured" : "sign-in-cancelled", `Google refused the sign-in (HTTP ${response.status})
|
|
1238
|
+
if (!response.ok || typeof token.access_token !== "string") throw new PlayServiceAccountError(token.error === "invalid_client" ? "sign-in-not-configured" : "sign-in-cancelled", `Google refused the sign-in (HTTP ${response.status}).${typeof token.error_description === "string" ? `\n${token.error_description}` : typeof token.error === "string" ? `\n${token.error}` : ""}`);
|
|
1205
1239
|
registerSecretValue(token.access_token);
|
|
1206
1240
|
return {
|
|
1207
1241
|
accessToken: token.access_token,
|
|
@@ -1256,11 +1290,11 @@ const newProjectId = () => `${PLAY_SERVICE_ACCOUNT_ID}-${crypto$1.randomBytes(3)
|
|
|
1256
1290
|
const isNotFound = (error) => error instanceof GoogleApiError && error.status === 404;
|
|
1257
1291
|
const isConflict = (error) => error instanceof GoogleApiError && error.status === 409;
|
|
1258
1292
|
/** The API is disabled in the OAuth client's own project: waiting does not help. */
|
|
1259
|
-
const clientApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}
|
|
1293
|
+
const clientApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}\nEnable it in the OAuth client's project (${GOOGLE_OAUTH_CLIENT_ENV_NAMES.clientId}).`, { cause: error });
|
|
1260
1294
|
/** Google's own words, after a message that names the likely cause. */
|
|
1261
|
-
const googleSays = (error) => error instanceof GoogleApiError && error.apiMessage ?
|
|
1295
|
+
const googleSays = (error) => error instanceof GoogleApiError && error.apiMessage ? `\nGoogle: ${error.apiMessage}` : "";
|
|
1262
1296
|
/** A Play Console refusal, as `not-play-admin`. */
|
|
1263
|
-
const notPlayAdmin = (developerId, error) => new PlayServiceAccountError("not-play-admin", `
|
|
1297
|
+
const notPlayAdmin = (developerId, error) => new PlayServiceAccountError("not-play-admin", `This Google Account isn't an admin of Play Console developer ${developerId}.${googleSays(error)}\nSign in with an account that has the Admin permission there.`, { cause: error });
|
|
1264
1298
|
const isDenied = (error) => error instanceof GoogleApiError && (error.status === 403 || error.apiStatus === "PERMISSION_DENIED");
|
|
1265
1299
|
/**
|
|
1266
1300
|
* `IssuePlayServiceAccount` with Google's own APIs, after a sign-in on
|
|
@@ -1281,7 +1315,11 @@ const isDenied = (error) => error instanceof GoogleApiError && (error.status ===
|
|
|
1281
1315
|
*/
|
|
1282
1316
|
const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor } = {}) => async (request) => {
|
|
1283
1317
|
const { packageName, oauthClient, onStep = () => {} } = request;
|
|
1284
|
-
if (!oauthClient?.clientId) throw new PlayServiceAccountError("sign-in-not-configured",
|
|
1318
|
+
if (!oauthClient?.clientId) throw new PlayServiceAccountError("sign-in-not-configured", [
|
|
1319
|
+
"This build of Hot Updater has no Google OAuth client to sign in with.",
|
|
1320
|
+
"Set these to a \"Desktop app\" OAuth client:",
|
|
1321
|
+
`${GOOGLE_OAUTH_CLIENT_ENV_NAMES.clientId}, ${GOOGLE_OAUTH_CLIENT_ENV_NAMES.clientSecret}`
|
|
1322
|
+
].join("\n"));
|
|
1285
1323
|
const signIn = await signInWithGoogle({
|
|
1286
1324
|
client: oauthClient,
|
|
1287
1325
|
scopes: SCOPES,
|
|
@@ -1293,14 +1331,14 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor } =
|
|
|
1293
1331
|
};
|
|
1294
1332
|
try {
|
|
1295
1333
|
const missing = SCOPES.filter((scope) => !signIn.scopes.includes(scope));
|
|
1296
|
-
if (missing.length > 0) throw new PlayServiceAccountError("scope-not-granted", `The sign-in
|
|
1334
|
+
if (missing.length > 0) throw new PlayServiceAccountError("scope-not-granted", `The sign-in didn't allow ${missing.map((scope) => scope === ANDROID_PUBLISHER_SCOPE ? "managing Google Play" : "managing Google Cloud").join(" or ")}.\nRun this again and keep every box on Google's consent screen ticked.`);
|
|
1297
1335
|
const google = createGoogleRequest({
|
|
1298
1336
|
accessToken: signIn.accessToken,
|
|
1299
1337
|
fetch
|
|
1300
1338
|
});
|
|
1301
1339
|
const developerInput = await request.askDeveloperId();
|
|
1302
1340
|
const developerId = parseDeveloperId(developerInput);
|
|
1303
|
-
if (developerId === void 0) throw new PlayServiceAccountError("developer-not-found", `"${developerInput.trim()}" is
|
|
1341
|
+
if (developerId === void 0) throw new PlayServiceAccountError("developer-not-found", `"${developerInput.trim()}" is not a developer account ID.\nIt is the number after /developers/ in Play Console's address.`);
|
|
1304
1342
|
onStep("Checking the Play Console developer account");
|
|
1305
1343
|
await checkPlayConsole({
|
|
1306
1344
|
google,
|
|
@@ -1372,7 +1410,7 @@ const checkPlayConsole = async ({ google, developerId, packageName }) => {
|
|
|
1372
1410
|
await google("Listing the Play Console users", "GET", `${PUBLISHER_API}/developers/${developerId}/users?pageSize=1`);
|
|
1373
1411
|
} catch (error) {
|
|
1374
1412
|
if (isServiceDisabled(error)) throw clientApiDisabled(error);
|
|
1375
|
-
if (isNotFound(error)) throw new PlayServiceAccountError("developer-not-found", `Play Console has no developer account ${developerId}
|
|
1413
|
+
if (isNotFound(error)) throw new PlayServiceAccountError("developer-not-found", `Play Console has no developer account ${developerId}.${googleSays(error)}\nIt is the number after /developers/ in Play Console's address.`, { cause: error });
|
|
1376
1414
|
if (isDenied(error)) throw notPlayAdmin(developerId, error);
|
|
1377
1415
|
throw error;
|
|
1378
1416
|
}
|
|
@@ -1433,9 +1471,18 @@ const chooseProject = async ({ google, request, sleep, onStep }) => {
|
|
|
1433
1471
|
};
|
|
1434
1472
|
} catch (error) {
|
|
1435
1473
|
if (!(error instanceof GoogleApiError)) throw error;
|
|
1436
|
-
if (error.status === 429 || error.apiStatus === "RESOURCE_EXHAUSTED" || /quota/iu.test(error.apiMessage)) throw new PlayServiceAccountError("project-quota",
|
|
1437
|
-
|
|
1438
|
-
|
|
1474
|
+
if (error.status === 429 || error.apiStatus === "RESOURCE_EXHAUSTED" || /quota/iu.test(error.apiMessage)) throw new PlayServiceAccountError("project-quota", [
|
|
1475
|
+
"Google Cloud has no project quota left for this account.",
|
|
1476
|
+
error.apiMessage || "Quota exceeded.",
|
|
1477
|
+
"Run this again and pick an existing project, or delete one at:",
|
|
1478
|
+
"https://console.cloud.google.com/cloud-resource-manager"
|
|
1479
|
+
].join("\n"), { cause: error });
|
|
1480
|
+
if (/terms of service/iu.test(error.apiMessage)) throw new PlayServiceAccountError("project-create-denied", "This Google Account hasn't accepted the Google Cloud Terms of Service.\nAccept them at https://console.cloud.google.com, then run this again.", { cause: error });
|
|
1481
|
+
if (isDenied(error) || error.apiStatus === "FAILED_PRECONDITION") throw new PlayServiceAccountError("project-create-denied", [
|
|
1482
|
+
"This Google Account may not create Google Cloud projects.",
|
|
1483
|
+
error.apiMessage || "Permission denied.",
|
|
1484
|
+
"Pick an existing project, or ask an admin for the Project Creator role."
|
|
1485
|
+
].join("\n"), { cause: error });
|
|
1439
1486
|
throw error;
|
|
1440
1487
|
}
|
|
1441
1488
|
};
|
|
@@ -1455,7 +1502,7 @@ const whilePropagating = async ({ run, project, sleep, notFound = false }) => {
|
|
|
1455
1502
|
const disabled = isServiceDisabled(error);
|
|
1456
1503
|
if (disabled && !consumerIs(error, project)) throw clientApiDisabled(error);
|
|
1457
1504
|
if (!disabled && !(notFound && isNotFound(error)) || attempt >= PROPAGATION_ATTEMPTS) {
|
|
1458
|
-
if (disabled) throw new PlayServiceAccountError("api-not-enabled", `Google is still enabling the APIs in ${project.projectId}
|
|
1505
|
+
if (disabled) throw new PlayServiceAccountError("api-not-enabled", `Google is still enabling the APIs in ${project.projectId}.\n${error.apiMessage}\nWait a few minutes, then run this again.`, { cause: error });
|
|
1459
1506
|
throw error;
|
|
1460
1507
|
}
|
|
1461
1508
|
await sleep(PROPAGATION_DELAY_MS);
|
|
@@ -1478,7 +1525,11 @@ const enableServices = async ({ google, projectId, sleep }) => {
|
|
|
1478
1525
|
sleep
|
|
1479
1526
|
});
|
|
1480
1527
|
} catch (error) {
|
|
1481
|
-
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied",
|
|
1528
|
+
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied", [
|
|
1529
|
+
`This Google Account may not enable APIs in ${projectId}.`,
|
|
1530
|
+
error.apiMessage || "Permission denied.",
|
|
1531
|
+
"Pick a project you own, or ask its owner for the Editor role."
|
|
1532
|
+
].join("\n"), { cause: error });
|
|
1482
1533
|
throw error;
|
|
1483
1534
|
}
|
|
1484
1535
|
};
|
|
@@ -1500,7 +1551,11 @@ const ensureServiceAccount = async ({ google, project, sleep }) => {
|
|
|
1500
1551
|
})).email ?? email;
|
|
1501
1552
|
} catch (error) {
|
|
1502
1553
|
if (isConflict(error)) return email;
|
|
1503
|
-
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied",
|
|
1554
|
+
if (isDenied(error) && !isServiceDisabled(error)) throw new PlayServiceAccountError("project-access-denied", [
|
|
1555
|
+
`This Google Account may not create service accounts in ${projectId}.`,
|
|
1556
|
+
error.apiMessage || "Permission denied.",
|
|
1557
|
+
"Pick a project you own, or ask for the Service Account Admin role."
|
|
1558
|
+
].join("\n"), { cause: error });
|
|
1504
1559
|
throw error;
|
|
1505
1560
|
}
|
|
1506
1561
|
};
|
|
@@ -1572,7 +1627,11 @@ const createKey = async ({ google, project, email, sleep }) => {
|
|
|
1572
1627
|
})
|
|
1573
1628
|
});
|
|
1574
1629
|
} catch (error) {
|
|
1575
|
-
if (error instanceof GoogleApiError && (error.violations.includes(KEY_CREATION_CONSTRAINT) || /disableServiceAccountKeyCreation/u.test(error.apiMessage))) throw new PlayServiceAccountError("key-creation-disabled",
|
|
1630
|
+
if (error instanceof GoogleApiError && (error.violations.includes(KEY_CREATION_CONSTRAINT) || /disableServiceAccountKeyCreation/u.test(error.apiMessage))) throw new PlayServiceAccountError("key-creation-disabled", [
|
|
1631
|
+
`iam.disableServiceAccountKeyCreation forbids keys in ${projectId}.`,
|
|
1632
|
+
"Ask an organization policy admin to allow keys for this project,",
|
|
1633
|
+
"or run this again with a project outside the organization."
|
|
1634
|
+
].join("\n"), { cause: error });
|
|
1576
1635
|
throw error;
|
|
1577
1636
|
}
|
|
1578
1637
|
if (!key.name || !key.privateKeyData) throw new Error("Google Cloud created the key without its JSON file.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/android-helper",
|
|
3
|
-
"version": "1.2.0-rc.
|
|
3
|
+
"version": "1.2.0-rc.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"execa": "9.5.2",
|
|
31
31
|
"jszip": "^3.10.1",
|
|
32
|
-
"@hot-updater/cli-tools": "1.2.0-rc.
|
|
33
|
-
"@hot-updater/plugin-core": "1.2.0-rc.
|
|
34
|
-
"@hot-updater/protocol": "1.2.0-rc.
|
|
32
|
+
"@hot-updater/cli-tools": "1.2.0-rc.3",
|
|
33
|
+
"@hot-updater/plugin-core": "1.2.0-rc.3",
|
|
34
|
+
"@hot-updater/protocol": "1.2.0-rc.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20"
|