firebase-tools 9.16.6 → 9.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +1 -3
  2. package/lib/api.js +1 -0
  3. package/lib/apiv2.js +1 -1
  4. package/lib/appdistribution/client.js +84 -72
  5. package/lib/appdistribution/distribution.js +8 -26
  6. package/lib/appdistribution/options-parser-util.js +51 -0
  7. package/lib/command.js +8 -6
  8. package/lib/commands/appdistribution-distribute.js +74 -91
  9. package/lib/commands/appdistribution-testers-add.js +18 -0
  10. package/lib/commands/appdistribution-testers-remove.js +32 -0
  11. package/lib/commands/crashlytics-symbols-upload.js +146 -0
  12. package/lib/commands/ext-configure.js +9 -1
  13. package/lib/commands/ext-dev-extension-delete.js +2 -1
  14. package/lib/commands/ext-dev-init.js +18 -9
  15. package/lib/commands/ext-dev-publish.js +11 -4
  16. package/lib/commands/ext-dev-unpublish.js +2 -1
  17. package/lib/commands/ext-install.js +115 -48
  18. package/lib/commands/ext-uninstall.js +6 -0
  19. package/lib/commands/ext-update.js +67 -43
  20. package/lib/commands/functions-config-export.js +115 -0
  21. package/lib/commands/functions-delete.js +44 -35
  22. package/lib/commands/functions-list.js +54 -0
  23. package/lib/commands/functions-log.js +5 -22
  24. package/lib/commands/hosting-channel-deploy.js +6 -4
  25. package/lib/commands/index.js +12 -0
  26. package/lib/deploy/functions/backend.js +47 -12
  27. package/lib/deploy/functions/containerCleaner.js +5 -1
  28. package/lib/deploy/functions/deploy.js +7 -5
  29. package/lib/deploy/functions/prepare.js +9 -7
  30. package/lib/deploy/functions/prompts.js +3 -21
  31. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +2 -1
  32. package/lib/deploy/functions/runtimes/index.js +2 -1
  33. package/lib/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.js +4 -3
  34. package/lib/deploy/functions/runtimes/node/parseTriggers.js +14 -9
  35. package/lib/deploy/functions/triggerRegionHelper.js +32 -0
  36. package/lib/downloadUtils.js +37 -0
  37. package/lib/emulator/auth/apiSpec.js +1758 -404
  38. package/lib/emulator/auth/handlers.js +6 -5
  39. package/lib/emulator/auth/operations.js +429 -40
  40. package/lib/emulator/auth/server.js +18 -11
  41. package/lib/emulator/auth/state.js +186 -5
  42. package/lib/emulator/auth/widget_ui.js +2 -2
  43. package/lib/emulator/download.js +2 -31
  44. package/lib/emulator/downloadableEmulators.js +7 -7
  45. package/lib/emulator/emulatorLogger.js +0 -3
  46. package/lib/emulator/events/types.js +16 -0
  47. package/lib/emulator/functionsEmulator.js +102 -17
  48. package/lib/emulator/functionsEmulatorRuntime.js +46 -121
  49. package/lib/emulator/functionsEmulatorShared.js +51 -7
  50. package/lib/emulator/functionsEmulatorShell.js +1 -1
  51. package/lib/emulator/pubsubEmulator.js +61 -40
  52. package/lib/extensions/askUserForConsent.js +16 -13
  53. package/lib/extensions/askUserForParam.js +72 -3
  54. package/lib/extensions/billingMigrationHelper.js +1 -11
  55. package/lib/extensions/changelog.js +93 -0
  56. package/lib/extensions/displayExtensionInfo.js +38 -38
  57. package/lib/extensions/emulator/optionsHelper.js +3 -3
  58. package/lib/extensions/emulator/triggerHelper.js +2 -32
  59. package/lib/extensions/extensionsApi.js +69 -95
  60. package/lib/extensions/extensionsHelper.js +75 -50
  61. package/lib/extensions/paramHelper.js +79 -36
  62. package/lib/extensions/refs.js +59 -0
  63. package/lib/extensions/resolveSource.js +2 -20
  64. package/lib/extensions/secretsUtils.js +58 -0
  65. package/lib/extensions/updateHelper.js +39 -105
  66. package/lib/extensions/warnings.js +1 -7
  67. package/lib/functional.js +64 -0
  68. package/lib/functions/env.js +26 -13
  69. package/lib/functions/functionslog.js +40 -0
  70. package/lib/functions/listFunctions.js +10 -0
  71. package/lib/functions/runtimeConfigExport.js +137 -0
  72. package/lib/gcp/cloudfunctions.js +84 -9
  73. package/lib/gcp/cloudfunctionsv2.js +99 -7
  74. package/lib/gcp/cloudlogging.js +27 -21
  75. package/lib/gcp/secretManager.js +111 -0
  76. package/lib/gcp/storage.js +16 -0
  77. package/lib/previews.js +1 -1
  78. package/lib/requireInteractive.js +12 -0
  79. package/package.json +5 -4
  80. package/schema/firebase-config.json +2 -1
  81. package/templates/extensions/CHANGELOG.md +7 -0
  82. package/templates/init/hosting/index.html +10 -10
package/CHANGELOG.md CHANGED
@@ -1,3 +1 @@
1
- - Fixes bug where functions packaged as ES module failed to load on Windows. (#3692)
2
- - Fixes bug parsing dotenv files with multiple quoted values (#3703)
3
- - Tracks use of runtime config and environment variables on function deploys. (#3704)
1
+ - `ext:install`, `ext:update` and `ext:configure` now support param type `secret`.
package/lib/api.js CHANGED
@@ -115,6 +115,7 @@ var api = {
115
115
  serviceUsageOrigin: utils.envOverride("FIREBASE_SERVICE_USAGE_URL", "https://serviceusage.googleapis.com"),
116
116
  githubOrigin: utils.envOverride("GITHUB_URL", "https://github.com"),
117
117
  githubApiOrigin: utils.envOverride("GITHUB_API_URL", "https://api.github.com"),
118
+ secretManagerOrigin: utils.envOverride("CLOUD_SECRET_MANAGER_URL", "https://secretmanager.googleapis.com"),
118
119
  githubClientId: utils.envOverride("GITHUB_CLIENT_ID", "89cf50f02ac6aaed3484"),
119
120
  githubClientSecret: utils.envOverride("GITHUB_CLIENT_SECRET", "3330d14abc895d9a74d5f17cd7a00711fa2c5bf0"),
120
121
  setRefreshToken: function (token) {
package/lib/apiv2.js CHANGED
@@ -12,7 +12,7 @@ const error_1 = require("./error");
12
12
  const logger_1 = require("./logger");
13
13
  const responseToError = require("./responseToError");
14
14
  const pkg = require("../package.json");
15
- const CLI_VERSION = pkg.CLI_VERSION;
15
+ const CLI_VERSION = pkg.version;
16
16
  let accessToken = "";
17
17
  let refreshToken = "";
18
18
  function setRefreshToken(token = "") {
@@ -1,119 +1,104 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppDistributionClient = exports.AppView = exports.AabState = exports.UploadStatus = void 0;
3
+ exports.AppDistributionClient = exports.UploadReleaseResult = exports.IntegrationState = void 0;
4
4
  const _ = require("lodash");
5
5
  const api = require("../api");
6
6
  const utils = require("../utils");
7
+ const operationPoller = require("../operation-poller");
7
8
  const error_1 = require("../error");
8
- const pkg = require("../../package.json");
9
- var UploadStatus;
10
- (function (UploadStatus) {
11
- UploadStatus["SUCCESS"] = "SUCCESS";
12
- UploadStatus["IN_PROGRESS"] = "IN_PROGRESS";
13
- UploadStatus["ERROR"] = "ERROR";
14
- })(UploadStatus = exports.UploadStatus || (exports.UploadStatus = {}));
15
- var AabState;
16
- (function (AabState) {
17
- AabState["AAB_STATE_UNSPECIFIED"] = "AAB_STATE_UNSPECIFIED";
18
- AabState["ACTIVE"] = "ACTIVE";
19
- AabState["PLAY_ACCOUNT_NOT_LINKED"] = "PLAY_ACCOUNT_NOT_LINKED";
20
- AabState["NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT"] = "NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT";
21
- AabState["APP_NOT_PUBLISHED"] = "APP_NOT_PUBLISHED";
22
- AabState["AAB_STATE_UNAVAILABLE"] = "AAB_STATE_UNAVAILABLE";
23
- AabState["PLAY_IAS_TERMS_NOT_ACCEPTED"] = "PLAY_IAS_TERMS_NOT_ACCEPTED";
24
- })(AabState = exports.AabState || (exports.AabState = {}));
25
- var AppView;
26
- (function (AppView) {
27
- AppView["BASIC"] = "BASIC";
28
- AppView["FULL"] = "FULL";
29
- })(AppView = exports.AppView || (exports.AppView = {}));
9
+ const apiv2_1 = require("../apiv2");
10
+ var IntegrationState;
11
+ (function (IntegrationState) {
12
+ IntegrationState["AAB_INTEGRATION_STATE_UNSPECIFIED"] = "AAB_INTEGRATION_STATE_UNSPECIFIED";
13
+ IntegrationState["INTEGRATED"] = "INTEGRATED";
14
+ IntegrationState["PLAY_ACCOUNT_NOT_LINKED"] = "PLAY_ACCOUNT_NOT_LINKED";
15
+ IntegrationState["NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT"] = "NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT";
16
+ IntegrationState["APP_NOT_PUBLISHED"] = "APP_NOT_PUBLISHED";
17
+ IntegrationState["AAB_STATE_UNAVAILABLE"] = "AAB_STATE_UNAVAILABLE";
18
+ IntegrationState["PLAY_IAS_TERMS_NOT_ACCEPTED"] = "PLAY_IAS_TERMS_NOT_ACCEPTED";
19
+ })(IntegrationState = exports.IntegrationState || (exports.IntegrationState = {}));
20
+ var UploadReleaseResult;
21
+ (function (UploadReleaseResult) {
22
+ UploadReleaseResult["UPLOAD_RELEASE_RESULT_UNSPECIFIED"] = "UPLOAD_RELEASE_RESULT_UNSPECIFIED";
23
+ UploadReleaseResult["RELEASE_CREATED"] = "RELEASE_CREATED";
24
+ UploadReleaseResult["RELEASE_UPDATED"] = "RELEASE_UPDATED";
25
+ UploadReleaseResult["RELEASE_UNMODIFIED"] = "RELEASE_UNMODIFIED";
26
+ })(UploadReleaseResult = exports.UploadReleaseResult || (exports.UploadReleaseResult = {}));
30
27
  class AppDistributionClient {
31
- constructor(appId) {
32
- this.appId = appId;
28
+ constructor() {
29
+ this.appDistroV2Client = new apiv2_1.Client({
30
+ urlPrefix: api.appDistributionOrigin,
31
+ apiVersion: "v1",
32
+ });
33
33
  }
34
- async getApp(appView = AppView.BASIC) {
35
- const apiResponse = await api.request("GET", `/v1alpha/apps/${this.appId}?appView=${appView}`, {
34
+ async getAabInfo(appName) {
35
+ const apiResponse = await api.request("GET", `/v1/${appName}/aabInfo`, {
36
36
  origin: api.appDistributionOrigin,
37
37
  auth: true,
38
38
  });
39
39
  return _.get(apiResponse, "body");
40
40
  }
41
- async uploadDistribution(distribution) {
42
- const apiResponse = await api.request("POST", `/app-binary-uploads?app_id=${this.appId}`, {
41
+ async uploadRelease(appName, distribution) {
42
+ const apiResponse = await api.request("POST", `/upload/v1/${appName}/releases:upload`, {
43
43
  auth: true,
44
44
  origin: api.appDistributionOrigin,
45
45
  headers: {
46
- "X-APP-DISTRO-API-CLIENT-ID": pkg.name,
47
- "X-APP-DISTRO-API-CLIENT-TYPE": distribution.platform(),
48
- "X-APP-DISTRO-API-CLIENT-VERSION": pkg.version,
49
- "X-GOOG-UPLOAD-FILE-NAME": distribution.getFileName(),
50
- "X-GOOG-UPLOAD-PROTOCOL": "raw",
46
+ "X-Goog-Upload-File-Name": distribution.getFileName(),
47
+ "X-Goog-Upload-Protocol": "raw",
51
48
  "Content-Type": "application/octet-stream",
52
49
  },
53
50
  data: distribution.readStream(),
54
51
  json: false,
55
52
  });
56
- return _.get(JSON.parse(apiResponse.body), "token");
57
- }
58
- async pollUploadStatus(binaryName, retryCount = 0) {
59
- const uploadStatus = await this.getUploadStatus(binaryName);
60
- if (uploadStatus.status === UploadStatus.IN_PROGRESS) {
61
- if (retryCount >= AppDistributionClient.MAX_POLLING_RETRIES) {
62
- throw new error_1.FirebaseError("it took longer than expected to process your binary, please try again", { exit: 1 });
63
- }
64
- await new Promise((resolve) => setTimeout(resolve, AppDistributionClient.POLLING_INTERVAL_MS));
65
- return this.pollUploadStatus(binaryName, retryCount + 1);
66
- }
67
- else if (uploadStatus.status === UploadStatus.SUCCESS) {
68
- return uploadStatus.release.id;
69
- }
70
- else {
71
- throw new error_1.FirebaseError(`error processing your binary: ${uploadStatus.message} (Code: ${uploadStatus.errorCode})`);
72
- }
53
+ return _.get(JSON.parse(apiResponse.body), "name");
73
54
  }
74
- async getUploadStatus(binaryName) {
75
- const encodedBinaryName = encodeURIComponent(binaryName);
76
- const apiResponse = await api.request("GET", `/v1alpha/apps/${this.appId}/upload_status/${encodedBinaryName}`, {
77
- origin: api.appDistributionOrigin,
78
- auth: true,
55
+ async pollUploadStatus(operationName) {
56
+ return operationPoller.pollOperation({
57
+ pollerName: "App Distribution Upload Poller",
58
+ apiOrigin: api.appDistributionOrigin,
59
+ apiVersion: "v1",
60
+ operationResourceName: operationName,
61
+ masterTimeout: 5 * 60 * 1000,
62
+ backoff: 1000,
63
+ maxBackoff: 10 * 1000,
79
64
  });
80
- return _.get(apiResponse, "body");
81
65
  }
82
- async addReleaseNotes(releaseId, releaseNotes) {
66
+ async updateReleaseNotes(releaseName, releaseNotes) {
83
67
  if (!releaseNotes) {
84
68
  utils.logWarning("no release notes specified, skipping");
85
69
  return;
86
70
  }
87
- utils.logBullet("adding release notes...");
71
+ utils.logBullet("updating release notes...");
88
72
  const data = {
73
+ name: releaseName,
89
74
  releaseNotes: {
90
- releaseNotes,
75
+ text: releaseNotes,
91
76
  },
92
77
  };
93
78
  try {
94
- await api.request("POST", `/v1alpha/apps/${this.appId}/releases/${releaseId}/notes`, {
79
+ await api.request("PATCH", `/v1/${releaseName}?updateMask=release_notes.text`, {
95
80
  origin: api.appDistributionOrigin,
96
81
  auth: true,
97
82
  data,
98
83
  });
99
84
  }
100
85
  catch (err) {
101
- throw new error_1.FirebaseError(`failed to add release notes with ${err.message}`, { exit: 1 });
86
+ throw new error_1.FirebaseError(`failed to update release notes with ${err.message}`, { exit: 1 });
102
87
  }
103
88
  utils.logSuccess("added release notes successfully");
104
89
  }
105
- async enableAccess(releaseId, emails = [], groupIds = []) {
106
- if (emails.length === 0 && groupIds.length === 0) {
90
+ async distribute(releaseName, testerEmails = [], groupAliases = []) {
91
+ if (testerEmails.length === 0 && groupAliases.length === 0) {
107
92
  utils.logWarning("no testers or groups specified, skipping");
108
93
  return;
109
94
  }
110
- utils.logBullet("adding testers/groups...");
95
+ utils.logBullet("distributing to testers/groups...");
111
96
  const data = {
112
- emails,
113
- groupIds,
97
+ testerEmails,
98
+ groupAliases,
114
99
  };
115
100
  try {
116
- await api.request("POST", `/v1alpha/apps/${this.appId}/releases/${releaseId}/enable_access`, {
101
+ await api.request("POST", `/v1/${releaseName}:distribute`, {
117
102
  origin: api.appDistributionOrigin,
118
103
  auth: true,
119
104
  data,
@@ -130,11 +115,38 @@ class AppDistributionClient {
130
115
  errorMessage = "invalid groups";
131
116
  }
132
117
  }
133
- throw new error_1.FirebaseError(`failed to add testers/groups: ${errorMessage}`, { exit: 1 });
118
+ throw new error_1.FirebaseError(`failed to distribute to testers/groups: ${errorMessage}`, {
119
+ exit: 1,
120
+ });
121
+ }
122
+ utils.logSuccess("distributed to testers/groups successfully");
123
+ }
124
+ async addTesters(projectName, emails) {
125
+ try {
126
+ await this.appDistroV2Client.request({
127
+ method: "POST",
128
+ path: `${projectName}/testers:batchAdd`,
129
+ body: { emails: emails },
130
+ });
131
+ }
132
+ catch (err) {
133
+ throw new error_1.FirebaseError(`Failed to add testers ${err}`);
134
+ }
135
+ utils.logSuccess(`Testers created successfully`);
136
+ }
137
+ async removeTesters(projectName, emails) {
138
+ let apiResponse;
139
+ try {
140
+ apiResponse = await this.appDistroV2Client.request({
141
+ method: "POST",
142
+ path: `${projectName}/testers:batchRemove`,
143
+ body: { emails: emails },
144
+ });
145
+ }
146
+ catch (err) {
147
+ throw new error_1.FirebaseError(`Failed to remove testers ${err}`);
134
148
  }
135
- utils.logSuccess("added testers/groups successfully");
149
+ return apiResponse.body;
136
150
  }
137
151
  }
138
152
  exports.AppDistributionClient = AppDistributionClient;
139
- AppDistributionClient.MAX_POLLING_RETRIES = 60;
140
- AppDistributionClient.POLLING_INTERVAL_MS = 2000;
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Distribution = exports.DistributionFileType = void 0;
4
4
  const fs = require("fs-extra");
5
5
  const error_1 = require("../error");
6
- const crypto = require("crypto");
7
6
  const logger_1 = require("../logger");
8
7
  const pathUtil = require("path");
9
8
  var DistributionFileType;
@@ -16,20 +15,24 @@ class Distribution {
16
15
  constructor(path) {
17
16
  this.path = path;
18
17
  if (!path) {
19
- throw new error_1.FirebaseError("must specify a distribution file");
18
+ throw new error_1.FirebaseError("must specify a release binary file");
20
19
  }
21
20
  const distributionType = path.split(".").pop();
22
21
  if (distributionType !== DistributionFileType.IPA &&
23
22
  distributionType !== DistributionFileType.APK &&
24
23
  distributionType !== DistributionFileType.AAB) {
25
- throw new error_1.FirebaseError("Unsupported distribution file format, should be .ipa, .apk or .aab");
24
+ throw new error_1.FirebaseError("Unsupported file format, should be .ipa, .apk or .aab");
26
25
  }
26
+ let stat;
27
27
  try {
28
- fs.ensureFileSync(path);
28
+ stat = fs.statSync(path);
29
29
  }
30
30
  catch (err) {
31
31
  logger_1.logger.info(err);
32
- throw new error_1.FirebaseError(`${path} is not a file. Verify that it points to a distribution binary.`);
32
+ throw new error_1.FirebaseError(`File ${path} does not exist: verify that file points to a binary`);
33
+ }
34
+ if (!stat.isFile()) {
35
+ throw new error_1.FirebaseError(`${path} is not a file. Verify that it points to a binary.`);
33
36
  }
34
37
  this.path = path;
35
38
  this.fileType = distributionType;
@@ -41,29 +44,8 @@ class Distribution {
41
44
  readStream() {
42
45
  return fs.createReadStream(this.path);
43
46
  }
44
- platform() {
45
- switch (this.fileType) {
46
- case DistributionFileType.IPA:
47
- return "ios";
48
- case DistributionFileType.AAB:
49
- case DistributionFileType.APK:
50
- return "android";
51
- default:
52
- throw new error_1.FirebaseError("Unsupported distribution file format, should be .ipa, .apk or .aab");
53
- }
54
- }
55
47
  getFileName() {
56
48
  return this.fileName;
57
49
  }
58
- binaryName(app) {
59
- return new Promise((resolve) => {
60
- const hash = crypto.createHash("sha256");
61
- const stream = this.readStream();
62
- stream.on("data", (data) => hash.update(data));
63
- stream.on("end", () => {
64
- return resolve(`projects/${app.projectNumber}/apps/${app.appId}/releases/-/binaries/${hash.digest("hex")}`);
65
- });
66
- });
67
- }
68
50
  }
69
51
  exports.Distribution = Distribution;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAppName = exports.getProjectName = exports.ensureFileExists = exports.getEmails = exports.getTestersOrGroups = void 0;
4
+ const fs = require("fs-extra");
5
+ const error_1 = require("../error");
6
+ const projectUtils_1 = require("../projectUtils");
7
+ function getTestersOrGroups(value, file) {
8
+ if (!value && file) {
9
+ ensureFileExists(file);
10
+ value = fs.readFileSync(file, "utf8");
11
+ }
12
+ if (value) {
13
+ return splitter(value);
14
+ }
15
+ return [];
16
+ }
17
+ exports.getTestersOrGroups = getTestersOrGroups;
18
+ function getEmails(emails, file) {
19
+ if (emails.length == 0) {
20
+ ensureFileExists(file);
21
+ const readFile = fs.readFileSync(file, "utf8");
22
+ return splitter(readFile);
23
+ }
24
+ return emails;
25
+ }
26
+ exports.getEmails = getEmails;
27
+ function ensureFileExists(file, message = "") {
28
+ if (!fs.existsSync(file)) {
29
+ throw new error_1.FirebaseError(`File ${file} does not exist: ${message}`);
30
+ }
31
+ }
32
+ exports.ensureFileExists = ensureFileExists;
33
+ function splitter(value) {
34
+ return value
35
+ .split(/[,\n]/)
36
+ .map((entry) => entry.trim())
37
+ .filter((entry) => !!entry);
38
+ }
39
+ async function getProjectName(options) {
40
+ const projectNumber = await projectUtils_1.needProjectNumber(options);
41
+ return `projects/${projectNumber}`;
42
+ }
43
+ exports.getProjectName = getProjectName;
44
+ function getAppName(options) {
45
+ if (!options.app) {
46
+ throw new error_1.FirebaseError("set the --app option to a valid Firebase app id and try again");
47
+ }
48
+ const appId = options.app;
49
+ return `projects/${appId.split(":")[1]}/apps/${appId}`;
50
+ }
51
+ exports.getAppName = getAppName;
package/lib/command.js CHANGED
@@ -13,6 +13,7 @@ const track = require("./track");
13
13
  const clc = require("cli-color");
14
14
  const auth_1 = require("./auth");
15
15
  const projects_1 = require("./management/projects");
16
+ const requireAuth_1 = require("./requireAuth");
16
17
  class Command {
17
18
  constructor(cmd) {
18
19
  this.cmd = cmd;
@@ -128,19 +129,19 @@ class Command {
128
129
  catch (e) {
129
130
  options.configError = e;
130
131
  }
131
- options.projectRoot = detectProjectRoot_1.detectProjectRoot(options);
132
- this.applyRC(options);
133
- if (options.project) {
134
- await this.resolveProjectIdentifiers(options);
135
- validateProjectId(options.projectId);
136
- }
137
132
  const account = utils_1.getInheritedOption(options, "account");
138
133
  options.account = account;
134
+ options.projectRoot = detectProjectRoot_1.detectProjectRoot(options);
139
135
  const projectRoot = options.projectRoot;
140
136
  const activeAccount = auth_1.selectAccount(account, projectRoot);
141
137
  if (activeAccount) {
142
138
  auth_1.setActiveAccount(options, activeAccount);
143
139
  }
140
+ this.applyRC(options);
141
+ if (options.project) {
142
+ await this.resolveProjectIdentifiers(options);
143
+ validateProjectId(options.projectId);
144
+ }
144
145
  }
145
146
  applyRC(options) {
146
147
  const rc = rc_1.loadRC(options);
@@ -164,6 +165,7 @@ class Command {
164
165
  async resolveProjectIdentifiers(options) {
165
166
  var _a;
166
167
  if ((_a = options.project) === null || _a === void 0 ? void 0 : _a.match(/^\d+$/)) {
168
+ await requireAuth_1.requireAuth(options);
167
169
  const { projectId, projectNumber } = await projects_1.getFirebaseProject(options.project);
168
170
  options.projectId = projectId;
169
171
  options.projectNumber = projectNumber;
@@ -7,128 +7,111 @@ const requireAuth_1 = require("../requireAuth");
7
7
  const client_1 = require("../appdistribution/client");
8
8
  const error_1 = require("../error");
9
9
  const distribution_1 = require("../appdistribution/distribution");
10
- function ensureFileExists(file, message = "") {
11
- if (!fs.existsSync(file)) {
12
- throw new error_1.FirebaseError(`File ${file} does not exist: ${message}`);
13
- }
14
- }
15
- function getAppId(appId) {
16
- if (!appId) {
17
- throw new error_1.FirebaseError("set the --app option to a valid Firebase app id and try again");
18
- }
19
- return appId;
20
- }
10
+ const options_parser_util_1 = require("../appdistribution/options-parser-util");
21
11
  function getReleaseNotes(releaseNotes, releaseNotesFile) {
22
12
  if (releaseNotes) {
23
13
  return releaseNotes.replace(/\\n/g, "\n");
24
14
  }
25
15
  else if (releaseNotesFile) {
26
- ensureFileExists(releaseNotesFile);
16
+ options_parser_util_1.ensureFileExists(releaseNotesFile);
27
17
  return fs.readFileSync(releaseNotesFile, "utf8");
28
18
  }
29
19
  return "";
30
20
  }
31
- function getTestersOrGroups(value, file) {
32
- if (!value && file) {
33
- ensureFileExists(file);
34
- value = fs.readFileSync(file, "utf8");
35
- }
36
- if (value) {
37
- return value
38
- .split(/,|\n/)
39
- .map((entry) => entry.trim())
40
- .filter((entry) => !!entry);
41
- }
42
- return [];
43
- }
44
- module.exports = new command_1.Command("appdistribution:distribute <distribution-file>")
45
- .description("upload a distribution")
21
+ module.exports = new command_1.Command("appdistribution:distribute <release-binary-file>")
22
+ .description("upload a release binary")
46
23
  .option("--app <app_id>", "the app id of your Firebase app")
47
- .option("--release-notes <string>", "release notes to include with this distribution")
48
- .option("--release-notes-file <file>", "path to file with release notes to include with this distribution")
24
+ .option("--release-notes <string>", "release notes to include")
25
+ .option("--release-notes-file <file>", "path to file with release notes")
49
26
  .option("--testers <string>", "a comma separated list of tester emails to distribute to")
50
27
  .option("--testers-file <file>", "path to file with a comma separated list of tester emails to distribute to")
51
28
  .option("--groups <string>", "a comma separated list of group aliases to distribute to")
52
29
  .option("--groups-file <file>", "path to file with a comma separated list of group aliases to distribute to")
53
30
  .before(requireAuth_1.requireAuth)
54
31
  .action(async (file, options) => {
55
- const appId = getAppId(options.app);
32
+ const appName = options_parser_util_1.getAppName(options);
56
33
  const distribution = new distribution_1.Distribution(file);
57
34
  const releaseNotes = getReleaseNotes(options.releaseNotes, options.releaseNotesFile);
58
- const testers = getTestersOrGroups(options.testers, options.testersFile);
59
- const groups = getTestersOrGroups(options.groups, options.groupsFile);
60
- const requests = new client_1.AppDistributionClient(appId);
61
- let app;
35
+ const testers = options_parser_util_1.getTestersOrGroups(options.testers, options.testersFile);
36
+ const groups = options_parser_util_1.getTestersOrGroups(options.groups, options.groupsFile);
37
+ const requests = new client_1.AppDistributionClient();
38
+ let aabInfo;
39
+ if (distribution.distributionFileType() === distribution_1.DistributionFileType.AAB) {
40
+ try {
41
+ aabInfo = await requests.getAabInfo(appName);
42
+ }
43
+ catch (err) {
44
+ if (err.status === 404) {
45
+ throw new error_1.FirebaseError(`App Distribution could not find your app ${options.app}. ` +
46
+ `Make sure to onboard your app by pressing the "Get started" ` +
47
+ "button on the App Distribution page in the Firebase console: " +
48
+ "https://console.firebase.google.com/project/_/appdistribution", { exit: 1 });
49
+ }
50
+ throw new error_1.FirebaseError(`failed to determine AAB info. ${err.message}`, { exit: 1 });
51
+ }
52
+ if (aabInfo.integrationState !== client_1.IntegrationState.INTEGRATED &&
53
+ aabInfo.integrationState !== client_1.IntegrationState.AAB_STATE_UNAVAILABLE) {
54
+ switch (aabInfo.integrationState) {
55
+ case client_1.IntegrationState.PLAY_ACCOUNT_NOT_LINKED: {
56
+ throw new error_1.FirebaseError("This project is not linked to a Google Play account.");
57
+ }
58
+ case client_1.IntegrationState.APP_NOT_PUBLISHED: {
59
+ throw new error_1.FirebaseError('"This app is not published in the Google Play console.');
60
+ }
61
+ case client_1.IntegrationState.NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT: {
62
+ throw new error_1.FirebaseError("App with matching package name does not exist in Google Play.");
63
+ }
64
+ case client_1.IntegrationState.PLAY_IAS_TERMS_NOT_ACCEPTED: {
65
+ throw new error_1.FirebaseError("You must accept the Play Internal App Sharing (IAS) terms to upload AABs.");
66
+ }
67
+ default: {
68
+ throw new error_1.FirebaseError("App Distribution failed to process the AAB: " + aabInfo.integrationState);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ utils.logBullet("uploading binary...");
74
+ let releaseName;
62
75
  try {
63
- const appView = distribution.distributionFileType() === distribution_1.DistributionFileType.AAB
64
- ? client_1.AppView.FULL
65
- : client_1.AppView.BASIC;
66
- app = await requests.getApp(appView);
76
+ const operationName = await requests.uploadRelease(appName, distribution);
77
+ const uploadResponse = await requests.pollUploadStatus(operationName);
78
+ const release = uploadResponse.release;
79
+ switch (uploadResponse.result) {
80
+ case client_1.UploadReleaseResult.RELEASE_CREATED:
81
+ utils.logSuccess(`uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!`);
82
+ break;
83
+ case client_1.UploadReleaseResult.RELEASE_UPDATED:
84
+ utils.logSuccess(`uploaded update to existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
85
+ break;
86
+ case client_1.UploadReleaseResult.RELEASE_UNMODIFIED:
87
+ utils.logSuccess(`re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
88
+ break;
89
+ default:
90
+ utils.logSuccess(`uploaded release ${release.displayVersion} (${release.buildVersion}) successfully!`);
91
+ }
92
+ releaseName = uploadResponse.release.name;
67
93
  }
68
94
  catch (err) {
69
95
  if (err.status === 404) {
70
- throw new error_1.FirebaseError(`App Distribution could not find your app ${appId}. ` +
96
+ throw new error_1.FirebaseError(`App Distribution could not find your app ${options.app}. ` +
71
97
  `Make sure to onboard your app by pressing the "Get started" ` +
72
98
  "button on the App Distribution page in the Firebase console: " +
73
99
  "https://console.firebase.google.com/project/_/appdistribution", { exit: 1 });
74
100
  }
75
- throw new error_1.FirebaseError(`failed to fetch app information. ${err.message}`, { exit: 1 });
76
- }
77
- if (!app.contactEmail) {
78
- throw new error_1.FirebaseError(`We could not find a contact email for app ${appId}. Please visit App Distribution within ` +
79
- "the Firebase Console to set one up.", { exit: 1 });
80
- }
81
- if (distribution.distributionFileType() === distribution_1.DistributionFileType.AAB &&
82
- app.aabState !== client_1.AabState.ACTIVE &&
83
- app.aabState !== client_1.AabState.AAB_STATE_UNAVAILABLE) {
84
- switch (app.aabState) {
85
- case client_1.AabState.PLAY_ACCOUNT_NOT_LINKED: {
86
- throw new error_1.FirebaseError("This project is not linked to a Google Play account.");
87
- }
88
- case client_1.AabState.APP_NOT_PUBLISHED: {
89
- throw new error_1.FirebaseError('"This app is not published in the Google Play console.');
90
- }
91
- case client_1.AabState.NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT: {
92
- throw new error_1.FirebaseError("App with matching package name does not exist in Google Play.");
93
- }
94
- case client_1.AabState.PLAY_IAS_TERMS_NOT_ACCEPTED: {
95
- throw new error_1.FirebaseError("You must accept the Play Internal App Sharing (IAS) terms to upload AABs.");
96
- }
97
- default: {
98
- throw new error_1.FirebaseError("App Distribution failed to process the AAB: " + app.aabState);
99
- }
100
- }
101
- }
102
- let binaryName = await distribution.binaryName(app);
103
- let releaseId;
104
- const uploadStatus = await requests.getUploadStatus(binaryName);
105
- if (uploadStatus.status === client_1.UploadStatus.SUCCESS) {
106
- utils.logWarning("this distribution has been uploaded before, skipping upload");
107
- releaseId = uploadStatus.release.id;
108
- }
109
- else {
110
- utils.logBullet("uploading distribution...");
111
- try {
112
- binaryName = await requests.uploadDistribution(distribution);
113
- releaseId = await requests.pollUploadStatus(binaryName);
114
- utils.logSuccess("uploaded distribution successfully!");
115
- }
116
- catch (err) {
117
- throw new error_1.FirebaseError(`failed to upload distribution. ${err.message}`, { exit: 1 });
118
- }
101
+ throw new error_1.FirebaseError(`failed to upload release. ${err.message}`, { exit: 1 });
119
102
  }
120
- if (distribution.distributionFileType() === distribution_1.DistributionFileType.AAB && !app.aabCertificate) {
121
- const updatedApp = await requests.getApp();
122
- if (updatedApp.aabCertificate) {
103
+ if (aabInfo && !aabInfo.testCertificate) {
104
+ aabInfo = await requests.getAabInfo(appName);
105
+ if (aabInfo.testCertificate) {
123
106
  utils.logBullet("After you upload an AAB for the first time, App Distribution " +
124
107
  "generates a new test certificate. All AAB uploads are re-signed with this test " +
125
108
  "certificate. Use the certificate fingerprints below to register your app " +
126
109
  "signing key with API providers, such as Google Sign-In and Google Maps.\n" +
127
- `MD-1 certificate fingerprint: ${updatedApp.aabCertificate.certificateHashMd5}\n` +
128
- `SHA-1 certificate fingerprint: ${updatedApp.aabCertificate.certificateHashSha1}\n` +
129
- `SHA-256 certificate fingerprint: ${updatedApp.aabCertificate.certificateHashSha256}`);
110
+ `MD-1 certificate fingerprint: ${aabInfo.testCertificate.hashMd5}\n` +
111
+ `SHA-1 certificate fingerprint: ${aabInfo.testCertificate.hashSha1}\n` +
112
+ `SHA-256 certificate fingerprint: ${aabInfo.testCertificate.hashSha256}`);
130
113
  }
131
114
  }
132
- await requests.addReleaseNotes(releaseId, releaseNotes);
133
- await requests.enableAccess(releaseId, testers, groups);
115
+ await requests.updateReleaseNotes(releaseName, releaseNotes);
116
+ await requests.distribute(releaseName, testers, groups);
134
117
  });
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const command_1 = require("../command");
4
+ const utils = require("../utils");
5
+ const requireAuth_1 = require("../requireAuth");
6
+ const client_1 = require("../appdistribution/client");
7
+ const options_parser_util_1 = require("../appdistribution/options-parser-util");
8
+ module.exports = new command_1.Command("appdistribution:testers:add [emails...]")
9
+ .description("add testers to project")
10
+ .option("--file <file>", "a path to a file containing a list of tester emails to be added")
11
+ .before(requireAuth_1.requireAuth)
12
+ .action(async (emails, options) => {
13
+ const projectName = await options_parser_util_1.getProjectName(options);
14
+ const appDistroClient = new client_1.AppDistributionClient();
15
+ const emailsToAdd = options_parser_util_1.getEmails(emails, options.file);
16
+ utils.logBullet(`Adding ${emailsToAdd.length} testers to project`);
17
+ await appDistroClient.addTesters(projectName, emailsToAdd);
18
+ });