@hot-updater/android-helper 1.2.0-rc.5 → 1.2.0-rc.7
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 +100 -47
- package/dist/index.d.cts +27 -8
- package/dist/index.d.mts +27 -8
- package/dist/index.mjs +100 -47
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1208,8 +1208,10 @@ const RESOURCE_MANAGER_API = "https://cloudresourcemanager.googleapis.com";
|
|
|
1208
1208
|
const SERVICE_USAGE_API = "https://serviceusage.googleapis.com";
|
|
1209
1209
|
const IAM_API = "https://iam.googleapis.com/v1";
|
|
1210
1210
|
const GCLOUD_INSTALL_URL = "https://cloud.google.com/sdk/docs/install";
|
|
1211
|
+
/** The Google Play Android Developer API, which the service account calls. */
|
|
1212
|
+
const PLAY_API = "androidpublisher.googleapis.com";
|
|
1211
1213
|
/** IAM creates the service account; the Play Developer API is what it calls. */
|
|
1212
|
-
const SERVICES = ["iam.googleapis.com",
|
|
1214
|
+
const SERVICES = ["iam.googleapis.com", PLAY_API];
|
|
1213
1215
|
const KEY_CREATION_CONSTRAINT = "constraints/iam.disableServiceAccountKeyCreation";
|
|
1214
1216
|
/** Retries while Google enables an API or creates a service account. */
|
|
1215
1217
|
const PROPAGATION_ATTEMPTS = 12;
|
|
@@ -1231,18 +1233,30 @@ const isConflict = (error) => error instanceof GoogleApiError && error.status ==
|
|
|
1231
1233
|
/** The API is disabled in another project than the one set up: waiting does not help. */
|
|
1232
1234
|
const consumerApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}\nEnable it in ${error.consumer ?? "that project"}, then run this again.`, { cause: error });
|
|
1233
1235
|
const isDenied = (error) => error instanceof GoogleApiError && (error.status === 403 || error.apiStatus === "PERMISSION_DENIED");
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
"
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1236
|
+
const lines = (output) => output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1237
|
+
/** The Google Accounts gcloud holds, the active one, if any, first. */
|
|
1238
|
+
const credentialedAccounts = async (run) => {
|
|
1239
|
+
const accounts = lines(await run("gcloud", [
|
|
1240
|
+
"auth",
|
|
1241
|
+
"list",
|
|
1242
|
+
"--format=value(account)"
|
|
1243
|
+
]));
|
|
1244
|
+
const [active] = lines(await run("gcloud", [
|
|
1245
|
+
"auth",
|
|
1246
|
+
"list",
|
|
1247
|
+
"--filter=status:ACTIVE",
|
|
1248
|
+
"--format=value(account)"
|
|
1249
|
+
]));
|
|
1250
|
+
return active !== void 0 && accounts.includes(active) ? [active, ...accounts.filter((account) => account !== active)].map((email) => ({
|
|
1251
|
+
email,
|
|
1252
|
+
active: email === active
|
|
1253
|
+
})) : accounts.map((email) => ({
|
|
1254
|
+
email,
|
|
1255
|
+
active: false
|
|
1256
|
+
}));
|
|
1257
|
+
};
|
|
1258
|
+
/** Stops before anything else, a prompt included, when gcloud is not installed. */
|
|
1259
|
+
const requireGcloud = async (run) => {
|
|
1246
1260
|
try {
|
|
1247
1261
|
await run("gcloud", ["--version"]);
|
|
1248
1262
|
} catch (error) {
|
|
@@ -1252,14 +1266,27 @@ const signInWithGcloud = async ({ run, onStep }) => {
|
|
|
1252
1266
|
GCLOUD_INSTALL_URL
|
|
1253
1267
|
].join("\n"), { cause: error });
|
|
1254
1268
|
}
|
|
1255
|
-
|
|
1269
|
+
};
|
|
1270
|
+
/**
|
|
1271
|
+
* The access token of the Google Account the developer picks among those
|
|
1272
|
+
* gcloud holds, or of the one `gcloud auth login` adds, when they pick
|
|
1273
|
+
* another or gcloud holds none. gcloud's active account stays as it was.
|
|
1274
|
+
*/
|
|
1275
|
+
const signInWithGcloud = async ({ run, request, onStep }) => {
|
|
1276
|
+
const before = await credentialedAccounts(run);
|
|
1277
|
+
let account = before.length > 0 ? await request.chooseGoogleAccount(before) ?? void 0 : void 0;
|
|
1256
1278
|
if (account === void 0) {
|
|
1257
|
-
await run("gcloud", ["auth", "login"], { interactive: true }).
|
|
1258
|
-
|
|
1259
|
-
|
|
1279
|
+
const signedIn = await run("gcloud", ["auth", "login"], { interactive: true }).then(() => true, () => false);
|
|
1280
|
+
const after = await credentialedAccounts(run);
|
|
1281
|
+
account = after.find(({ email }) => !before.some((known) => known.email === email))?.email ?? (signedIn ? after.find(({ active }) => active)?.email : void 0);
|
|
1282
|
+
if (account === void 0) throw new PlayServiceAccountError("gcloud-not-signed-in", before.length === 0 ? "gcloud has no active Google Account.\nRun gcloud auth login, then run this again." : "gcloud auth login didn't finish.\nRun this again to choose a Google Account.");
|
|
1260
1283
|
}
|
|
1261
1284
|
onStep(`Signed in to gcloud as ${account}`);
|
|
1262
|
-
const accessToken = (await run("gcloud", [
|
|
1285
|
+
const accessToken = (await run("gcloud", [
|
|
1286
|
+
"auth",
|
|
1287
|
+
"print-access-token",
|
|
1288
|
+
account
|
|
1289
|
+
])).trim();
|
|
1263
1290
|
(0, _hot_updater_cli_tools.registerSecretValue)(accessToken);
|
|
1264
1291
|
return accessToken;
|
|
1265
1292
|
};
|
|
@@ -1267,11 +1294,12 @@ const signInWithGcloud = async ({ run, onStep }) => {
|
|
|
1267
1294
|
* `IssuePlayServiceAccount` with the gcloud CLI's sign-in and Google's own
|
|
1268
1295
|
* APIs:
|
|
1269
1296
|
*
|
|
1270
|
-
* 1. gcloud
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1297
|
+
* 1. once gcloud is found, `askPackageName` names the app, and the Google
|
|
1298
|
+
* Account `chooseGoogleAccount` picks among gcloud's, or the one
|
|
1299
|
+
* `gcloud auth login` adds, the access token the Google Cloud steps use;
|
|
1300
|
+
* 2. the account's only project with the Play Developer API, else the one
|
|
1301
|
+
* `chooseProject` picks, else a new one, gets the IAM and Play Developer
|
|
1302
|
+
* APIs enabled, and the service account `hot-updater-play` its JSON key;
|
|
1275
1303
|
* 3. the developer invites the service account in Play Console, which
|
|
1276
1304
|
* `onInvite` opens: gcloud's token may not manage Google Play users;
|
|
1277
1305
|
* 4. the key itself opens an edit of the app until Google Play lets it,
|
|
@@ -1281,9 +1309,12 @@ const signInWithGcloud = async ({ run, onStep }) => {
|
|
|
1281
1309
|
* service account.
|
|
1282
1310
|
*/
|
|
1283
1311
|
const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run = runWithExeca } = {}) => async (request) => {
|
|
1284
|
-
const {
|
|
1312
|
+
const { onStep = () => {} } = request;
|
|
1313
|
+
await requireGcloud(run);
|
|
1314
|
+
const packageName = await request.askPackageName();
|
|
1285
1315
|
const accessToken = await signInWithGcloud({
|
|
1286
1316
|
run,
|
|
1317
|
+
request,
|
|
1287
1318
|
onStep
|
|
1288
1319
|
});
|
|
1289
1320
|
const google = createGoogleRequest({
|
|
@@ -1297,7 +1328,7 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
|
|
|
1297
1328
|
onStep
|
|
1298
1329
|
});
|
|
1299
1330
|
const { projectId } = project;
|
|
1300
|
-
onStep(`Enabling the
|
|
1331
|
+
onStep(`Enabling the APIs in ${projectId}`);
|
|
1301
1332
|
await enableServices({
|
|
1302
1333
|
google,
|
|
1303
1334
|
projectId,
|
|
@@ -1338,8 +1369,9 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
|
|
|
1338
1369
|
const deleted = await revoke();
|
|
1339
1370
|
throw new PlayServiceAccountError("play-access-pending", [
|
|
1340
1371
|
`The key can't release ${packageName} yet.`,
|
|
1341
|
-
"Invite the service account in Play Console
|
|
1372
|
+
"Invite the service account in Play Console:",
|
|
1342
1373
|
email,
|
|
1374
|
+
"App permission: Release apps to testing tracks",
|
|
1343
1375
|
inviteUrl,
|
|
1344
1376
|
"A new app needs its first .aab uploaded in Play Console.",
|
|
1345
1377
|
...deleted ? [] : [`Delete its unused key ${key.keyId} in Google Cloud:`, `https://console.cloud.google.com/iam-admin/serviceaccounts?project=${encodeURIComponent(projectId)}`],
|
|
@@ -1408,32 +1440,53 @@ const listProjects = async (google) => {
|
|
|
1408
1440
|
} while (pageToken);
|
|
1409
1441
|
return projects;
|
|
1410
1442
|
};
|
|
1411
|
-
/**
|
|
1443
|
+
/** Service Usage reads of the projects' Play Developer API, at most this many at once. */
|
|
1444
|
+
const API_CHECK_CONCURRENCY = 8;
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether each project has the Google Play Developer API enabled, by one
|
|
1447
|
+
* Service Usage read each; a project this account may not read counts as
|
|
1448
|
+
* without it.
|
|
1449
|
+
*/
|
|
1450
|
+
const withPlayApiState = async (google, projects) => {
|
|
1451
|
+
const states = [];
|
|
1452
|
+
for (let start = 0; start < projects.length; start += API_CHECK_CONCURRENCY) states.push(...await Promise.all(projects.slice(start, start + API_CHECK_CONCURRENCY).map(({ projectId }) => google("Reading the project's APIs", "GET", `${SERVICE_USAGE_API}/v1/projects/${encodeURIComponent(projectId)}/services/${PLAY_API}`).then(({ state }) => state === "ENABLED", () => false))));
|
|
1453
|
+
return projects.map((project, index) => ({
|
|
1454
|
+
...project,
|
|
1455
|
+
playApiEnabled: states[index]
|
|
1456
|
+
}));
|
|
1457
|
+
};
|
|
1458
|
+
/** The display name of the project Hot Updater creates. */
|
|
1412
1459
|
const PROJECT_DISPLAY_NAME = "Hot Updater Play";
|
|
1413
1460
|
/**
|
|
1414
|
-
* The project the service account goes in
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1461
|
+
* The project the service account goes in: a new one for an account
|
|
1462
|
+
* without projects; the only project with the Play Developer API, without
|
|
1463
|
+
* asking; else the one the developer picks, or a new one.
|
|
1417
1464
|
*/
|
|
1418
1465
|
const chooseProject = async ({ google, request, sleep, onStep }) => {
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
projectId,
|
|
1432
|
-
...displayName ? { displayName } : {}
|
|
1433
|
-
})));
|
|
1434
|
-
if (chosen === void 0) throw error;
|
|
1435
|
-
return projects.find((project) => project.projectId === chosen) ?? { projectId: chosen };
|
|
1466
|
+
const listed = await listProjects(google);
|
|
1467
|
+
if (listed.length === 0) return createProject({
|
|
1468
|
+
google,
|
|
1469
|
+
sleep,
|
|
1470
|
+
onStep
|
|
1471
|
+
});
|
|
1472
|
+
const projects = await withPlayApiState(google, listed);
|
|
1473
|
+
const enabled = projects.filter(({ playApiEnabled }) => playApiEnabled);
|
|
1474
|
+
if (enabled.length === 1) {
|
|
1475
|
+
const [only] = enabled;
|
|
1476
|
+
onStep(`Using ${only.projectId}, the project with the Play Developer API`);
|
|
1477
|
+
return only;
|
|
1436
1478
|
}
|
|
1479
|
+
const chosen = await request.chooseProject(projects.map(({ projectId, displayName, playApiEnabled }) => ({
|
|
1480
|
+
projectId,
|
|
1481
|
+
...displayName ? { displayName } : {},
|
|
1482
|
+
playApiEnabled
|
|
1483
|
+
})));
|
|
1484
|
+
if (chosen === null) return createProject({
|
|
1485
|
+
google,
|
|
1486
|
+
sleep,
|
|
1487
|
+
onStep
|
|
1488
|
+
});
|
|
1489
|
+
return projects.find((project) => project.projectId === chosen) ?? { projectId: chosen };
|
|
1437
1490
|
};
|
|
1438
1491
|
/** A new project, `hot-updater-play-<random>`, once Google has created it. */
|
|
1439
1492
|
const createProject = async ({ google, sleep, onStep }) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -158,7 +158,7 @@ declare const readBuiltVersionCode: ({ artifactPath, androidProjectPath }: {
|
|
|
158
158
|
type PlayServiceAccountErrorCode =
|
|
159
159
|
/** The gcloud CLI, which signs in to Google, is not installed. */
|
|
160
160
|
"gcloud-not-installed" |
|
|
161
|
-
/**
|
|
161
|
+
/** gcloud ended with no Google Account to use: none signed in, or the sign-in was cancelled. */
|
|
162
162
|
"gcloud-not-signed-in" |
|
|
163
163
|
/** The Google Account may not create a Google Cloud project. */
|
|
164
164
|
"project-create-denied" |
|
|
@@ -187,21 +187,40 @@ declare const PLAY_SERVICE_ACCOUNT_ID = "hot-updater-play";
|
|
|
187
187
|
interface GoogleCloudProject {
|
|
188
188
|
readonly projectId: string;
|
|
189
189
|
readonly displayName?: string;
|
|
190
|
+
/** Whether the Google Play Android Developer API is enabled in it. */
|
|
191
|
+
readonly playApiEnabled: boolean;
|
|
192
|
+
}
|
|
193
|
+
/** A Google Account the gcloud CLI holds credentials for. */
|
|
194
|
+
interface GoogleAccount {
|
|
195
|
+
readonly email: string;
|
|
196
|
+
/** Whether it is gcloud's active account. */
|
|
197
|
+
readonly active: boolean;
|
|
190
198
|
}
|
|
191
199
|
interface IssuePlayServiceAccountRequest {
|
|
192
|
-
/**
|
|
193
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Asks for the app's package name in Google Play, which the service
|
|
202
|
+
* account may release: after the sign-in's own tool is found, before
|
|
203
|
+
* anything else.
|
|
204
|
+
*/
|
|
205
|
+
readonly askPackageName: () => Promise<string>;
|
|
206
|
+
/**
|
|
207
|
+
* Sees the Google Accounts gcloud already holds, the active one first,
|
|
208
|
+
* after the package name, and returns the one to use, or null to sign in
|
|
209
|
+
* with another. Never called when gcloud holds none: it signs in.
|
|
210
|
+
*/
|
|
211
|
+
readonly chooseGoogleAccount: (accounts: readonly GoogleAccount[]) => Promise<string | null>;
|
|
194
212
|
/**
|
|
195
213
|
* Sees Play Console's page that invites the service account `email`, to
|
|
196
214
|
* print it and open it in a browser; the issuer then waits for the invite.
|
|
197
215
|
*/
|
|
198
216
|
readonly onInvite: (url: string, email: string) => Promise<void> | void;
|
|
199
217
|
/**
|
|
200
|
-
* Sees the account's projects when
|
|
201
|
-
*
|
|
202
|
-
*
|
|
218
|
+
* Sees the account's projects when no one of them is the obvious home of
|
|
219
|
+
* the service account, the only one with the Google Play Developer API,
|
|
220
|
+
* and returns the project ID to create it in, or null for a new project.
|
|
221
|
+
* Never called for an account without projects: a new one is created.
|
|
203
222
|
*/
|
|
204
|
-
readonly chooseProject: (projects: readonly GoogleCloudProject[]) => Promise<string |
|
|
223
|
+
readonly chooseProject: (projects: readonly GoogleCloudProject[]) => Promise<string | null>;
|
|
205
224
|
/** Reports each step, such as "Creating the service account". */
|
|
206
225
|
readonly onStep?: (message: string) => void;
|
|
207
226
|
}
|
|
@@ -236,4 +255,4 @@ type IssuePlayServiceAccount = (request: IssuePlayServiceAccountRequest) => Prom
|
|
|
236
255
|
*/
|
|
237
256
|
declare const issuePlayServiceAccount: IssuePlayServiceAccount;
|
|
238
257
|
//#endregion
|
|
239
|
-
export { type EnrichedNativeBuildAndroidScheme, type GoogleCloudProject, type IssuePlayServiceAccount, type IssuePlayServiceAccountRequest, type IssuedPlayServiceAccount, PLAY_SERVICE_ACCOUNT_ID, PlayServiceAccountError, type PlayServiceAccountErrorCode, PlayVersionCodeUsedError, type UploadToPlayResult, assertPlaySigned, buildAndroid, enrichNativeBuildAndroidScheme, extractBuiltInBundle, findPlayInternalRelease, hasPlayReleases, issuePlayServiceAccount, readBuiltApp, readBuiltPackageName, readBuiltVersionCode, resolvePlayUploadServiceAccount, uploadToPlay };
|
|
258
|
+
export { type EnrichedNativeBuildAndroidScheme, type GoogleAccount, type GoogleCloudProject, type IssuePlayServiceAccount, type IssuePlayServiceAccountRequest, type IssuedPlayServiceAccount, PLAY_SERVICE_ACCOUNT_ID, PlayServiceAccountError, type PlayServiceAccountErrorCode, PlayVersionCodeUsedError, type UploadToPlayResult, assertPlaySigned, buildAndroid, enrichNativeBuildAndroidScheme, extractBuiltInBundle, findPlayInternalRelease, hasPlayReleases, issuePlayServiceAccount, readBuiltApp, readBuiltPackageName, readBuiltVersionCode, resolvePlayUploadServiceAccount, uploadToPlay };
|
package/dist/index.d.mts
CHANGED
|
@@ -159,7 +159,7 @@ declare const readBuiltVersionCode: ({ artifactPath, androidProjectPath }: {
|
|
|
159
159
|
type PlayServiceAccountErrorCode =
|
|
160
160
|
/** The gcloud CLI, which signs in to Google, is not installed. */
|
|
161
161
|
"gcloud-not-installed" |
|
|
162
|
-
/**
|
|
162
|
+
/** gcloud ended with no Google Account to use: none signed in, or the sign-in was cancelled. */
|
|
163
163
|
"gcloud-not-signed-in" |
|
|
164
164
|
/** The Google Account may not create a Google Cloud project. */
|
|
165
165
|
"project-create-denied" |
|
|
@@ -188,21 +188,40 @@ declare const PLAY_SERVICE_ACCOUNT_ID = "hot-updater-play";
|
|
|
188
188
|
interface GoogleCloudProject {
|
|
189
189
|
readonly projectId: string;
|
|
190
190
|
readonly displayName?: string;
|
|
191
|
+
/** Whether the Google Play Android Developer API is enabled in it. */
|
|
192
|
+
readonly playApiEnabled: boolean;
|
|
193
|
+
}
|
|
194
|
+
/** A Google Account the gcloud CLI holds credentials for. */
|
|
195
|
+
interface GoogleAccount {
|
|
196
|
+
readonly email: string;
|
|
197
|
+
/** Whether it is gcloud's active account. */
|
|
198
|
+
readonly active: boolean;
|
|
191
199
|
}
|
|
192
200
|
interface IssuePlayServiceAccountRequest {
|
|
193
|
-
/**
|
|
194
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Asks for the app's package name in Google Play, which the service
|
|
203
|
+
* account may release: after the sign-in's own tool is found, before
|
|
204
|
+
* anything else.
|
|
205
|
+
*/
|
|
206
|
+
readonly askPackageName: () => Promise<string>;
|
|
207
|
+
/**
|
|
208
|
+
* Sees the Google Accounts gcloud already holds, the active one first,
|
|
209
|
+
* after the package name, and returns the one to use, or null to sign in
|
|
210
|
+
* with another. Never called when gcloud holds none: it signs in.
|
|
211
|
+
*/
|
|
212
|
+
readonly chooseGoogleAccount: (accounts: readonly GoogleAccount[]) => Promise<string | null>;
|
|
195
213
|
/**
|
|
196
214
|
* Sees Play Console's page that invites the service account `email`, to
|
|
197
215
|
* print it and open it in a browser; the issuer then waits for the invite.
|
|
198
216
|
*/
|
|
199
217
|
readonly onInvite: (url: string, email: string) => Promise<void> | void;
|
|
200
218
|
/**
|
|
201
|
-
* Sees the account's projects when
|
|
202
|
-
*
|
|
203
|
-
*
|
|
219
|
+
* Sees the account's projects when no one of them is the obvious home of
|
|
220
|
+
* the service account, the only one with the Google Play Developer API,
|
|
221
|
+
* and returns the project ID to create it in, or null for a new project.
|
|
222
|
+
* Never called for an account without projects: a new one is created.
|
|
204
223
|
*/
|
|
205
|
-
readonly chooseProject: (projects: readonly GoogleCloudProject[]) => Promise<string |
|
|
224
|
+
readonly chooseProject: (projects: readonly GoogleCloudProject[]) => Promise<string | null>;
|
|
206
225
|
/** Reports each step, such as "Creating the service account". */
|
|
207
226
|
readonly onStep?: (message: string) => void;
|
|
208
227
|
}
|
|
@@ -237,4 +256,4 @@ type IssuePlayServiceAccount = (request: IssuePlayServiceAccountRequest) => Prom
|
|
|
237
256
|
*/
|
|
238
257
|
declare const issuePlayServiceAccount: IssuePlayServiceAccount;
|
|
239
258
|
//#endregion
|
|
240
|
-
export { type EnrichedNativeBuildAndroidScheme, type GoogleCloudProject, type IssuePlayServiceAccount, type IssuePlayServiceAccountRequest, type IssuedPlayServiceAccount, PLAY_SERVICE_ACCOUNT_ID, PlayServiceAccountError, type PlayServiceAccountErrorCode, PlayVersionCodeUsedError, type UploadToPlayResult, assertPlaySigned, buildAndroid, enrichNativeBuildAndroidScheme, extractBuiltInBundle, findPlayInternalRelease, hasPlayReleases, issuePlayServiceAccount, readBuiltApp, readBuiltPackageName, readBuiltVersionCode, resolvePlayUploadServiceAccount, uploadToPlay };
|
|
259
|
+
export { type EnrichedNativeBuildAndroidScheme, type GoogleAccount, type GoogleCloudProject, type IssuePlayServiceAccount, type IssuePlayServiceAccountRequest, type IssuedPlayServiceAccount, PLAY_SERVICE_ACCOUNT_ID, PlayServiceAccountError, type PlayServiceAccountErrorCode, PlayVersionCodeUsedError, type UploadToPlayResult, assertPlaySigned, buildAndroid, enrichNativeBuildAndroidScheme, extractBuiltInBundle, findPlayInternalRelease, hasPlayReleases, issuePlayServiceAccount, readBuiltApp, readBuiltPackageName, readBuiltVersionCode, resolvePlayUploadServiceAccount, uploadToPlay };
|
package/dist/index.mjs
CHANGED
|
@@ -1179,8 +1179,10 @@ const RESOURCE_MANAGER_API = "https://cloudresourcemanager.googleapis.com";
|
|
|
1179
1179
|
const SERVICE_USAGE_API = "https://serviceusage.googleapis.com";
|
|
1180
1180
|
const IAM_API = "https://iam.googleapis.com/v1";
|
|
1181
1181
|
const GCLOUD_INSTALL_URL = "https://cloud.google.com/sdk/docs/install";
|
|
1182
|
+
/** The Google Play Android Developer API, which the service account calls. */
|
|
1183
|
+
const PLAY_API = "androidpublisher.googleapis.com";
|
|
1182
1184
|
/** IAM creates the service account; the Play Developer API is what it calls. */
|
|
1183
|
-
const SERVICES = ["iam.googleapis.com",
|
|
1185
|
+
const SERVICES = ["iam.googleapis.com", PLAY_API];
|
|
1184
1186
|
const KEY_CREATION_CONSTRAINT = "constraints/iam.disableServiceAccountKeyCreation";
|
|
1185
1187
|
/** Retries while Google enables an API or creates a service account. */
|
|
1186
1188
|
const PROPAGATION_ATTEMPTS = 12;
|
|
@@ -1202,18 +1204,30 @@ const isConflict = (error) => error instanceof GoogleApiError && error.status ==
|
|
|
1202
1204
|
/** The API is disabled in another project than the one set up: waiting does not help. */
|
|
1203
1205
|
const consumerApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}\nEnable it in ${error.consumer ?? "that project"}, then run this again.`, { cause: error });
|
|
1204
1206
|
const isDenied = (error) => error instanceof GoogleApiError && (error.status === 403 || error.apiStatus === "PERMISSION_DENIED");
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
"
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1207
|
+
const lines = (output) => output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1208
|
+
/** The Google Accounts gcloud holds, the active one, if any, first. */
|
|
1209
|
+
const credentialedAccounts = async (run) => {
|
|
1210
|
+
const accounts = lines(await run("gcloud", [
|
|
1211
|
+
"auth",
|
|
1212
|
+
"list",
|
|
1213
|
+
"--format=value(account)"
|
|
1214
|
+
]));
|
|
1215
|
+
const [active] = lines(await run("gcloud", [
|
|
1216
|
+
"auth",
|
|
1217
|
+
"list",
|
|
1218
|
+
"--filter=status:ACTIVE",
|
|
1219
|
+
"--format=value(account)"
|
|
1220
|
+
]));
|
|
1221
|
+
return active !== void 0 && accounts.includes(active) ? [active, ...accounts.filter((account) => account !== active)].map((email) => ({
|
|
1222
|
+
email,
|
|
1223
|
+
active: email === active
|
|
1224
|
+
})) : accounts.map((email) => ({
|
|
1225
|
+
email,
|
|
1226
|
+
active: false
|
|
1227
|
+
}));
|
|
1228
|
+
};
|
|
1229
|
+
/** Stops before anything else, a prompt included, when gcloud is not installed. */
|
|
1230
|
+
const requireGcloud = async (run) => {
|
|
1217
1231
|
try {
|
|
1218
1232
|
await run("gcloud", ["--version"]);
|
|
1219
1233
|
} catch (error) {
|
|
@@ -1223,14 +1237,27 @@ const signInWithGcloud = async ({ run, onStep }) => {
|
|
|
1223
1237
|
GCLOUD_INSTALL_URL
|
|
1224
1238
|
].join("\n"), { cause: error });
|
|
1225
1239
|
}
|
|
1226
|
-
|
|
1240
|
+
};
|
|
1241
|
+
/**
|
|
1242
|
+
* The access token of the Google Account the developer picks among those
|
|
1243
|
+
* gcloud holds, or of the one `gcloud auth login` adds, when they pick
|
|
1244
|
+
* another or gcloud holds none. gcloud's active account stays as it was.
|
|
1245
|
+
*/
|
|
1246
|
+
const signInWithGcloud = async ({ run, request, onStep }) => {
|
|
1247
|
+
const before = await credentialedAccounts(run);
|
|
1248
|
+
let account = before.length > 0 ? await request.chooseGoogleAccount(before) ?? void 0 : void 0;
|
|
1227
1249
|
if (account === void 0) {
|
|
1228
|
-
await run("gcloud", ["auth", "login"], { interactive: true }).
|
|
1229
|
-
|
|
1230
|
-
|
|
1250
|
+
const signedIn = await run("gcloud", ["auth", "login"], { interactive: true }).then(() => true, () => false);
|
|
1251
|
+
const after = await credentialedAccounts(run);
|
|
1252
|
+
account = after.find(({ email }) => !before.some((known) => known.email === email))?.email ?? (signedIn ? after.find(({ active }) => active)?.email : void 0);
|
|
1253
|
+
if (account === void 0) throw new PlayServiceAccountError("gcloud-not-signed-in", before.length === 0 ? "gcloud has no active Google Account.\nRun gcloud auth login, then run this again." : "gcloud auth login didn't finish.\nRun this again to choose a Google Account.");
|
|
1231
1254
|
}
|
|
1232
1255
|
onStep(`Signed in to gcloud as ${account}`);
|
|
1233
|
-
const accessToken = (await run("gcloud", [
|
|
1256
|
+
const accessToken = (await run("gcloud", [
|
|
1257
|
+
"auth",
|
|
1258
|
+
"print-access-token",
|
|
1259
|
+
account
|
|
1260
|
+
])).trim();
|
|
1234
1261
|
registerSecretValue(accessToken);
|
|
1235
1262
|
return accessToken;
|
|
1236
1263
|
};
|
|
@@ -1238,11 +1265,12 @@ const signInWithGcloud = async ({ run, onStep }) => {
|
|
|
1238
1265
|
* `IssuePlayServiceAccount` with the gcloud CLI's sign-in and Google's own
|
|
1239
1266
|
* APIs:
|
|
1240
1267
|
*
|
|
1241
|
-
* 1. gcloud
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1268
|
+
* 1. once gcloud is found, `askPackageName` names the app, and the Google
|
|
1269
|
+
* Account `chooseGoogleAccount` picks among gcloud's, or the one
|
|
1270
|
+
* `gcloud auth login` adds, the access token the Google Cloud steps use;
|
|
1271
|
+
* 2. the account's only project with the Play Developer API, else the one
|
|
1272
|
+
* `chooseProject` picks, else a new one, gets the IAM and Play Developer
|
|
1273
|
+
* APIs enabled, and the service account `hot-updater-play` its JSON key;
|
|
1246
1274
|
* 3. the developer invites the service account in Play Console, which
|
|
1247
1275
|
* `onInvite` opens: gcloud's token may not manage Google Play users;
|
|
1248
1276
|
* 4. the key itself opens an edit of the app until Google Play lets it,
|
|
@@ -1252,9 +1280,12 @@ const signInWithGcloud = async ({ run, onStep }) => {
|
|
|
1252
1280
|
* service account.
|
|
1253
1281
|
*/
|
|
1254
1282
|
const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run = runWithExeca } = {}) => async (request) => {
|
|
1255
|
-
const {
|
|
1283
|
+
const { onStep = () => {} } = request;
|
|
1284
|
+
await requireGcloud(run);
|
|
1285
|
+
const packageName = await request.askPackageName();
|
|
1256
1286
|
const accessToken = await signInWithGcloud({
|
|
1257
1287
|
run,
|
|
1288
|
+
request,
|
|
1258
1289
|
onStep
|
|
1259
1290
|
});
|
|
1260
1291
|
const google = createGoogleRequest({
|
|
@@ -1268,7 +1299,7 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
|
|
|
1268
1299
|
onStep
|
|
1269
1300
|
});
|
|
1270
1301
|
const { projectId } = project;
|
|
1271
|
-
onStep(`Enabling the
|
|
1302
|
+
onStep(`Enabling the APIs in ${projectId}`);
|
|
1272
1303
|
await enableServices({
|
|
1273
1304
|
google,
|
|
1274
1305
|
projectId,
|
|
@@ -1309,8 +1340,9 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
|
|
|
1309
1340
|
const deleted = await revoke();
|
|
1310
1341
|
throw new PlayServiceAccountError("play-access-pending", [
|
|
1311
1342
|
`The key can't release ${packageName} yet.`,
|
|
1312
|
-
"Invite the service account in Play Console
|
|
1343
|
+
"Invite the service account in Play Console:",
|
|
1313
1344
|
email,
|
|
1345
|
+
"App permission: Release apps to testing tracks",
|
|
1314
1346
|
inviteUrl,
|
|
1315
1347
|
"A new app needs its first .aab uploaded in Play Console.",
|
|
1316
1348
|
...deleted ? [] : [`Delete its unused key ${key.keyId} in Google Cloud:`, `https://console.cloud.google.com/iam-admin/serviceaccounts?project=${encodeURIComponent(projectId)}`],
|
|
@@ -1379,32 +1411,53 @@ const listProjects = async (google) => {
|
|
|
1379
1411
|
} while (pageToken);
|
|
1380
1412
|
return projects;
|
|
1381
1413
|
};
|
|
1382
|
-
/**
|
|
1414
|
+
/** Service Usage reads of the projects' Play Developer API, at most this many at once. */
|
|
1415
|
+
const API_CHECK_CONCURRENCY = 8;
|
|
1416
|
+
/**
|
|
1417
|
+
* Whether each project has the Google Play Developer API enabled, by one
|
|
1418
|
+
* Service Usage read each; a project this account may not read counts as
|
|
1419
|
+
* without it.
|
|
1420
|
+
*/
|
|
1421
|
+
const withPlayApiState = async (google, projects) => {
|
|
1422
|
+
const states = [];
|
|
1423
|
+
for (let start = 0; start < projects.length; start += API_CHECK_CONCURRENCY) states.push(...await Promise.all(projects.slice(start, start + API_CHECK_CONCURRENCY).map(({ projectId }) => google("Reading the project's APIs", "GET", `${SERVICE_USAGE_API}/v1/projects/${encodeURIComponent(projectId)}/services/${PLAY_API}`).then(({ state }) => state === "ENABLED", () => false))));
|
|
1424
|
+
return projects.map((project, index) => ({
|
|
1425
|
+
...project,
|
|
1426
|
+
playApiEnabled: states[index]
|
|
1427
|
+
}));
|
|
1428
|
+
};
|
|
1429
|
+
/** The display name of the project Hot Updater creates. */
|
|
1383
1430
|
const PROJECT_DISPLAY_NAME = "Hot Updater Play";
|
|
1384
1431
|
/**
|
|
1385
|
-
* The project the service account goes in
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1432
|
+
* The project the service account goes in: a new one for an account
|
|
1433
|
+
* without projects; the only project with the Play Developer API, without
|
|
1434
|
+
* asking; else the one the developer picks, or a new one.
|
|
1388
1435
|
*/
|
|
1389
1436
|
const chooseProject = async ({ google, request, sleep, onStep }) => {
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
projectId,
|
|
1403
|
-
...displayName ? { displayName } : {}
|
|
1404
|
-
})));
|
|
1405
|
-
if (chosen === void 0) throw error;
|
|
1406
|
-
return projects.find((project) => project.projectId === chosen) ?? { projectId: chosen };
|
|
1437
|
+
const listed = await listProjects(google);
|
|
1438
|
+
if (listed.length === 0) return createProject({
|
|
1439
|
+
google,
|
|
1440
|
+
sleep,
|
|
1441
|
+
onStep
|
|
1442
|
+
});
|
|
1443
|
+
const projects = await withPlayApiState(google, listed);
|
|
1444
|
+
const enabled = projects.filter(({ playApiEnabled }) => playApiEnabled);
|
|
1445
|
+
if (enabled.length === 1) {
|
|
1446
|
+
const [only] = enabled;
|
|
1447
|
+
onStep(`Using ${only.projectId}, the project with the Play Developer API`);
|
|
1448
|
+
return only;
|
|
1407
1449
|
}
|
|
1450
|
+
const chosen = await request.chooseProject(projects.map(({ projectId, displayName, playApiEnabled }) => ({
|
|
1451
|
+
projectId,
|
|
1452
|
+
...displayName ? { displayName } : {},
|
|
1453
|
+
playApiEnabled
|
|
1454
|
+
})));
|
|
1455
|
+
if (chosen === null) return createProject({
|
|
1456
|
+
google,
|
|
1457
|
+
sleep,
|
|
1458
|
+
onStep
|
|
1459
|
+
});
|
|
1460
|
+
return projects.find((project) => project.projectId === chosen) ?? { projectId: chosen };
|
|
1408
1461
|
};
|
|
1409
1462
|
/** A new project, `hot-updater-play-<random>`, once Google has created it. */
|
|
1410
1463
|
const createProject = async ({ google, sleep, onStep }) => {
|
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.7",
|
|
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/
|
|
34
|
-
"@hot-updater/
|
|
32
|
+
"@hot-updater/cli-tools": "1.2.0-rc.7",
|
|
33
|
+
"@hot-updater/protocol": "1.2.0-rc.7",
|
|
34
|
+
"@hot-updater/plugin-core": "1.2.0-rc.7"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20"
|