@hot-updater/android-helper 1.2.0-rc.6 → 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 CHANGED
@@ -1233,13 +1233,28 @@ const isConflict = (error) => error instanceof GoogleApiError && error.status ==
1233
1233
  /** The API is disabled in another project than the one set up: waiting does not help. */
1234
1234
  const consumerApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}\nEnable it in ${error.consumer ?? "that project"}, then run this again.`, { cause: error });
1235
1235
  const isDenied = (error) => error instanceof GoogleApiError && (error.status === 403 || error.apiStatus === "PERMISSION_DENIED");
1236
- /** The Google Account gcloud uses, if one is active. */
1237
- const activeAccount = async (run) => (await run("gcloud", [
1238
- "auth",
1239
- "list",
1240
- "--filter=status:ACTIVE",
1241
- "--format=value(account)"
1242
- ])).trim().split("\n")[0]?.trim() || void 0;
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
+ };
1243
1258
  /** Stops before anything else, a prompt included, when gcloud is not installed. */
1244
1259
  const requireGcloud = async (run) => {
1245
1260
  try {
@@ -1252,16 +1267,26 @@ const requireGcloud = async (run) => {
1252
1267
  ].join("\n"), { cause: error });
1253
1268
  }
1254
1269
  };
1255
- /** gcloud's access token, after `gcloud auth login` when no account is active. */
1256
- const signInWithGcloud = async ({ run, onStep }) => {
1257
- let account = await activeAccount(run);
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;
1258
1278
  if (account === void 0) {
1259
- await run("gcloud", ["auth", "login"], { interactive: true }).catch(() => {});
1260
- account = await activeAccount(run);
1261
- if (account === void 0) throw new PlayServiceAccountError("gcloud-not-signed-in", "gcloud has no active Google Account.\nRun gcloud auth login, then run this again.");
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.");
1262
1283
  }
1263
1284
  onStep(`Signed in to gcloud as ${account}`);
1264
- const accessToken = (await run("gcloud", ["auth", "print-access-token"])).trim();
1285
+ const accessToken = (await run("gcloud", [
1286
+ "auth",
1287
+ "print-access-token",
1288
+ account
1289
+ ])).trim();
1265
1290
  (0, _hot_updater_cli_tools.registerSecretValue)(accessToken);
1266
1291
  return accessToken;
1267
1292
  };
@@ -1269,9 +1294,9 @@ const signInWithGcloud = async ({ run, onStep }) => {
1269
1294
  * `IssuePlayServiceAccount` with the gcloud CLI's sign-in and Google's own
1270
1295
  * APIs:
1271
1296
  *
1272
- * 1. once gcloud is found, `askPackageName` names the app, and gcloud's
1273
- * Google Account, signed in with `gcloud auth login` when none is
1274
- * active, the access token the Google Cloud steps use;
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;
1275
1300
  * 2. the account's only project with the Play Developer API, else the one
1276
1301
  * `chooseProject` picks, else a new one, gets the IAM and Play Developer
1277
1302
  * APIs enabled, and the service account `hot-updater-play` its JSON key;
@@ -1289,6 +1314,7 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
1289
1314
  const packageName = await request.askPackageName();
1290
1315
  const accessToken = await signInWithGcloud({
1291
1316
  run,
1317
+ request,
1292
1318
  onStep
1293
1319
  });
1294
1320
  const google = createGoogleRequest({
@@ -1343,8 +1369,9 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
1343
1369
  const deleted = await revoke();
1344
1370
  throw new PlayServiceAccountError("play-access-pending", [
1345
1371
  `The key can't release ${packageName} yet.`,
1346
- "Invite the service account in Play Console with release access:",
1372
+ "Invite the service account in Play Console:",
1347
1373
  email,
1374
+ "App permission: Release apps to testing tracks",
1348
1375
  inviteUrl,
1349
1376
  "A new app needs its first .aab uploaded in Play Console.",
1350
1377
  ...deleted ? [] : [`Delete its unused key ${key.keyId} in Google Cloud:`, `https://console.cloud.google.com/iam-admin/serviceaccounts?project=${encodeURIComponent(projectId)}`],
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
- /** `gcloud auth login` ended with no active Google Account. */
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" |
@@ -190,6 +190,12 @@ interface GoogleCloudProject {
190
190
  /** Whether the Google Play Android Developer API is enabled in it. */
191
191
  readonly playApiEnabled: boolean;
192
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;
198
+ }
193
199
  interface IssuePlayServiceAccountRequest {
194
200
  /**
195
201
  * Asks for the app's package name in Google Play, which the service
@@ -197,6 +203,12 @@ interface IssuePlayServiceAccountRequest {
197
203
  * anything else.
198
204
  */
199
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>;
200
212
  /**
201
213
  * Sees Play Console's page that invites the service account `email`, to
202
214
  * print it and open it in a browser; the issuer then waits for the invite.
@@ -243,4 +255,4 @@ type IssuePlayServiceAccount = (request: IssuePlayServiceAccountRequest) => Prom
243
255
  */
244
256
  declare const issuePlayServiceAccount: IssuePlayServiceAccount;
245
257
  //#endregion
246
- 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
- /** `gcloud auth login` ended with no active Google Account. */
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" |
@@ -191,6 +191,12 @@ interface GoogleCloudProject {
191
191
  /** Whether the Google Play Android Developer API is enabled in it. */
192
192
  readonly playApiEnabled: boolean;
193
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;
199
+ }
194
200
  interface IssuePlayServiceAccountRequest {
195
201
  /**
196
202
  * Asks for the app's package name in Google Play, which the service
@@ -198,6 +204,12 @@ interface IssuePlayServiceAccountRequest {
198
204
  * anything else.
199
205
  */
200
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>;
201
213
  /**
202
214
  * Sees Play Console's page that invites the service account `email`, to
203
215
  * print it and open it in a browser; the issuer then waits for the invite.
@@ -244,4 +256,4 @@ type IssuePlayServiceAccount = (request: IssuePlayServiceAccountRequest) => Prom
244
256
  */
245
257
  declare const issuePlayServiceAccount: IssuePlayServiceAccount;
246
258
  //#endregion
247
- 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
@@ -1204,13 +1204,28 @@ const isConflict = (error) => error instanceof GoogleApiError && error.status ==
1204
1204
  /** The API is disabled in another project than the one set up: waiting does not help. */
1205
1205
  const consumerApiDisabled = (error) => new PlayServiceAccountError("api-not-enabled", `${error.apiMessage}\nEnable it in ${error.consumer ?? "that project"}, then run this again.`, { cause: error });
1206
1206
  const isDenied = (error) => error instanceof GoogleApiError && (error.status === 403 || error.apiStatus === "PERMISSION_DENIED");
1207
- /** The Google Account gcloud uses, if one is active. */
1208
- const activeAccount = async (run) => (await run("gcloud", [
1209
- "auth",
1210
- "list",
1211
- "--filter=status:ACTIVE",
1212
- "--format=value(account)"
1213
- ])).trim().split("\n")[0]?.trim() || void 0;
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
+ };
1214
1229
  /** Stops before anything else, a prompt included, when gcloud is not installed. */
1215
1230
  const requireGcloud = async (run) => {
1216
1231
  try {
@@ -1223,16 +1238,26 @@ const requireGcloud = async (run) => {
1223
1238
  ].join("\n"), { cause: error });
1224
1239
  }
1225
1240
  };
1226
- /** gcloud's access token, after `gcloud auth login` when no account is active. */
1227
- const signInWithGcloud = async ({ run, onStep }) => {
1228
- let account = await activeAccount(run);
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;
1229
1249
  if (account === void 0) {
1230
- await run("gcloud", ["auth", "login"], { interactive: true }).catch(() => {});
1231
- account = await activeAccount(run);
1232
- if (account === void 0) throw new PlayServiceAccountError("gcloud-not-signed-in", "gcloud has no active Google Account.\nRun gcloud auth login, then run this again.");
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.");
1233
1254
  }
1234
1255
  onStep(`Signed in to gcloud as ${account}`);
1235
- const accessToken = (await run("gcloud", ["auth", "print-access-token"])).trim();
1256
+ const accessToken = (await run("gcloud", [
1257
+ "auth",
1258
+ "print-access-token",
1259
+ account
1260
+ ])).trim();
1236
1261
  registerSecretValue(accessToken);
1237
1262
  return accessToken;
1238
1263
  };
@@ -1240,9 +1265,9 @@ const signInWithGcloud = async ({ run, onStep }) => {
1240
1265
  * `IssuePlayServiceAccount` with the gcloud CLI's sign-in and Google's own
1241
1266
  * APIs:
1242
1267
  *
1243
- * 1. once gcloud is found, `askPackageName` names the app, and gcloud's
1244
- * Google Account, signed in with `gcloud auth login` when none is
1245
- * active, the access token the Google Cloud steps use;
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;
1246
1271
  * 2. the account's only project with the Play Developer API, else the one
1247
1272
  * `chooseProject` picks, else a new one, gets the IAM and Play Developer
1248
1273
  * APIs enabled, and the service account `hot-updater-play` its JSON key;
@@ -1260,6 +1285,7 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
1260
1285
  const packageName = await request.askPackageName();
1261
1286
  const accessToken = await signInWithGcloud({
1262
1287
  run,
1288
+ request,
1263
1289
  onStep
1264
1290
  });
1265
1291
  const google = createGoogleRequest({
@@ -1314,8 +1340,9 @@ const createGoogleKeyIssuer = ({ fetch = globalThis.fetch, sleep = sleepFor, run
1314
1340
  const deleted = await revoke();
1315
1341
  throw new PlayServiceAccountError("play-access-pending", [
1316
1342
  `The key can't release ${packageName} yet.`,
1317
- "Invite the service account in Play Console with release access:",
1343
+ "Invite the service account in Play Console:",
1318
1344
  email,
1345
+ "App permission: Release apps to testing tracks",
1319
1346
  inviteUrl,
1320
1347
  "A new app needs its first .aab uploaded in Play Console.",
1321
1348
  ...deleted ? [] : [`Delete its unused key ${key.keyId} in Google Cloud:`, `https://console.cloud.google.com/iam-admin/serviceaccounts?project=${encodeURIComponent(projectId)}`],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/android-helper",
3
- "version": "1.2.0-rc.6",
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.6",
33
- "@hot-updater/plugin-core": "1.2.0-rc.6",
34
- "@hot-updater/protocol": "1.2.0-rc.6"
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"