firebase-tools 13.2.1 → 13.3.1
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/lib/auth.js +11 -2
- package/lib/commands/apphosting-backends-create.js +4 -1
- package/lib/commands/apphosting-backends-delete.js +3 -5
- package/lib/commands/apphosting-backends-list.js +3 -3
- package/lib/commands/apphosting-builds-create.js +3 -2
- package/lib/commands/index.js +1 -1
- package/lib/commands/init.js +3 -3
- package/lib/deploy/functions/runtimes/index.js +2 -0
- package/lib/deploy/functions/runtimes/python/index.js +4 -1
- package/lib/deploy/functions/services/firestore.js +1 -1
- package/lib/emulator/downloadableEmulators.js +3 -3
- package/lib/emulator/storage/rules/runtime.js +1 -1
- package/lib/experiments.js +2 -2
- package/lib/firestore/delete.js +3 -3
- package/lib/gcp/apphosting.js +10 -9
- package/lib/gcp/cloudbuild.js +7 -2
- package/lib/gcp/firestore.js +14 -5
- package/lib/hosting/options.js +2 -1
- package/lib/init/features/apphosting/index.js +130 -74
- package/lib/init/features/apphosting/repo.js +83 -35
- package/lib/init/features/hosting/github.js +5 -0
- package/lib/metaprogramming.js +3 -0
- package/lib/prompt.js +1 -0
- package/package.json +4 -2
package/lib/auth.js
CHANGED
|
@@ -307,7 +307,7 @@ async function loginRemotely() {
|
|
|
307
307
|
const tokens = await getTokensFromAuthorizationCode(code, `${api_1.authProxyOrigin}/complete`, codeVerifier);
|
|
308
308
|
void (0, track_1.trackGA4)("login", { method: "google_remote" });
|
|
309
309
|
return {
|
|
310
|
-
user: jwt.decode(tokens.id_token),
|
|
310
|
+
user: jwt.decode(tokens.id_token, { json: true }),
|
|
311
311
|
tokens: tokens,
|
|
312
312
|
scopes: SCOPES,
|
|
313
313
|
};
|
|
@@ -323,7 +323,7 @@ async function loginWithLocalhostGoogle(port, userHint) {
|
|
|
323
323
|
const tokens = await loginWithLocalhost(port, callbackUrl, authUrl, successTemplate, getTokensFromAuthorizationCode);
|
|
324
324
|
void (0, track_1.trackGA4)("login", { method: "google_localhost" });
|
|
325
325
|
return {
|
|
326
|
-
user: jwt.decode(tokens.id_token),
|
|
326
|
+
user: jwt.decode(tokens.id_token, { json: true }),
|
|
327
327
|
tokens: tokens,
|
|
328
328
|
scopes: tokens.scopes,
|
|
329
329
|
};
|
|
@@ -477,6 +477,15 @@ async function refreshTokens(refreshToken, authScopes) {
|
|
|
477
477
|
skipLog: { body: true, queryParams: true, resBody: true },
|
|
478
478
|
resolveOnHTTPError: true,
|
|
479
479
|
});
|
|
480
|
+
const forceReauthErrs = [
|
|
481
|
+
{ error: "invalid_grant", error_subtype: "invalid_rapt" },
|
|
482
|
+
];
|
|
483
|
+
const matches = (a, b) => {
|
|
484
|
+
return a.error === b.error && a.error_subtype === b.error_subtype;
|
|
485
|
+
};
|
|
486
|
+
if (forceReauthErrs.some((a) => matches(a, res.body))) {
|
|
487
|
+
throw invalidCredentialError();
|
|
488
|
+
}
|
|
480
489
|
if (res.status === 401 || res.status === 400) {
|
|
481
490
|
return { access_token: refreshToken };
|
|
482
491
|
}
|
|
@@ -9,9 +9,12 @@ const apphosting_2 = require("../gcp/apphosting");
|
|
|
9
9
|
exports.command = new command_1.Command("apphosting:backends:create")
|
|
10
10
|
.description("Create a backend in a Firebase project")
|
|
11
11
|
.option("-l, --location <location>", "Specify the region of the backend", "")
|
|
12
|
+
.option("-s, --service-account <serviceAccount>", "Specify the service account used to run the server", "")
|
|
12
13
|
.before(apphosting_2.ensureApiEnabled)
|
|
13
14
|
.before(requireInteractive_1.default)
|
|
14
15
|
.action(async (options) => {
|
|
15
16
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
16
|
-
|
|
17
|
+
const location = options.location;
|
|
18
|
+
const serviceAccount = options.serviceAccount;
|
|
19
|
+
await (0, apphosting_1.doSetup)(projectId, location, serviceAccount);
|
|
17
20
|
});
|
|
@@ -19,16 +19,14 @@ const TABLE_HEAD = [
|
|
|
19
19
|
"Created Date",
|
|
20
20
|
"Updated Date",
|
|
21
21
|
];
|
|
22
|
-
exports.command = new command_1.Command("apphosting:backends:delete")
|
|
23
|
-
.description("
|
|
22
|
+
exports.command = new command_1.Command("apphosting:backends:delete <backend>")
|
|
23
|
+
.description("delete a backend from a Firebase project")
|
|
24
24
|
.option("-l, --location <location>", "App Backend location", "")
|
|
25
|
-
.option("-s, --backend <backend>", "Backend Id", "")
|
|
26
25
|
.withForce()
|
|
27
26
|
.before(apphosting.ensureApiEnabled)
|
|
28
|
-
.action(async (options) => {
|
|
27
|
+
.action(async (backendId, options) => {
|
|
29
28
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
30
29
|
let location = options.location;
|
|
31
|
-
const backendId = options.backend;
|
|
32
30
|
if (!backendId) {
|
|
33
31
|
throw new error_1.FirebaseError("Backend id can't be empty.");
|
|
34
32
|
}
|
|
@@ -14,7 +14,7 @@ exports.command = new command_1.Command("apphosting:backends:list")
|
|
|
14
14
|
.option("-l, --location <location>", "App Backend location", "-")
|
|
15
15
|
.before(apphosting.ensureApiEnabled)
|
|
16
16
|
.action(async (options) => {
|
|
17
|
-
var _a, _b, _c;
|
|
17
|
+
var _a, _b, _c, _d;
|
|
18
18
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
19
19
|
const location = options.location;
|
|
20
20
|
const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
|
|
@@ -25,12 +25,12 @@ exports.command = new command_1.Command("apphosting:backends:list")
|
|
|
25
25
|
catch (err) {
|
|
26
26
|
throw new error_1.FirebaseError(`Unable to list backends present for project: ${projectId}. Please check the parameters you have provided.`, { original: err });
|
|
27
27
|
}
|
|
28
|
-
const backends = backendRes
|
|
28
|
+
const backends = (_a = backendRes.backends) !== null && _a !== void 0 ? _a : [];
|
|
29
29
|
for (const backend of backends) {
|
|
30
30
|
const [backendLocation, , backendId] = backend.name.split("/").slice(3, 6);
|
|
31
31
|
table.push([
|
|
32
32
|
backendId,
|
|
33
|
-
(
|
|
33
|
+
(_d = (_c = (_b = backend.codebase) === null || _b === void 0 ? void 0 : _b.repository) === null || _c === void 0 ? void 0 : _c.split("/").pop()) !== null && _d !== void 0 ? _d : "",
|
|
34
34
|
backendLocation,
|
|
35
35
|
backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
|
|
36
36
|
(0, utils_1.datetimeString)(new Date(backend.createTime)),
|
|
@@ -12,15 +12,16 @@ exports.command = new command_1.Command("apphosting:builds:create <backendId>")
|
|
|
12
12
|
.option("-b, --branch <branch>", "Repository branch to deploy. Defaults to 'main'", "main")
|
|
13
13
|
.before(apphosting.ensureApiEnabled)
|
|
14
14
|
.action(async (backendId, options) => {
|
|
15
|
+
var _a;
|
|
15
16
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
16
17
|
const location = options.location;
|
|
17
18
|
const buildId = options.buildId ||
|
|
18
19
|
(await apphosting.getNextRolloutId(projectId, location, backendId));
|
|
19
|
-
const branch = options.branch;
|
|
20
|
+
const branch = (_a = options.branch) !== null && _a !== void 0 ? _a : "main";
|
|
20
21
|
const op = await apphosting.createBuild(projectId, location, backendId, buildId, {
|
|
21
22
|
source: {
|
|
22
23
|
codebase: {
|
|
23
|
-
branch
|
|
24
|
+
branch,
|
|
24
25
|
},
|
|
25
26
|
},
|
|
26
27
|
});
|
package/lib/commands/index.js
CHANGED
|
@@ -144,7 +144,7 @@ function load(client) {
|
|
|
144
144
|
client.internaltesting.functions = {};
|
|
145
145
|
client.internaltesting.functions.discover = loadCommand("internaltesting-functions-discover");
|
|
146
146
|
}
|
|
147
|
-
if (experiments.isEnabled("
|
|
147
|
+
if (experiments.isEnabled("apphosting")) {
|
|
148
148
|
client.apphosting = {};
|
|
149
149
|
client.apphosting.backends = {};
|
|
150
150
|
client.apphosting.backends.list = loadCommand("apphosting-backends-list");
|
package/lib/commands/init.js
CHANGED
|
@@ -69,10 +69,10 @@ const choices = [
|
|
|
69
69
|
checked: false,
|
|
70
70
|
},
|
|
71
71
|
];
|
|
72
|
-
if ((0, experiments_1.isEnabled)("
|
|
72
|
+
if ((0, experiments_1.isEnabled)("apphosting")) {
|
|
73
73
|
choices.push({
|
|
74
|
-
value: "
|
|
75
|
-
name: "
|
|
74
|
+
value: "apphosting",
|
|
75
|
+
name: "App Hosting: Get started with App Hosting projects.",
|
|
76
76
|
checked: false,
|
|
77
77
|
});
|
|
78
78
|
}
|
|
@@ -14,6 +14,7 @@ const RUNTIMES = [
|
|
|
14
14
|
"nodejs20",
|
|
15
15
|
"python310",
|
|
16
16
|
"python311",
|
|
17
|
+
"python312",
|
|
17
18
|
];
|
|
18
19
|
const EXPERIMENTAL_RUNTIMES = [];
|
|
19
20
|
const DEPRECATED_RUNTIMES = ["nodejs6", "nodejs8"];
|
|
@@ -36,6 +37,7 @@ const MESSAGE_FRIENDLY_RUNTIMES = {
|
|
|
36
37
|
nodejs20: "Node.js 20",
|
|
37
38
|
python310: "Python 3.10",
|
|
38
39
|
python311: "Python 3.11",
|
|
40
|
+
python312: "Python 3.12",
|
|
39
41
|
};
|
|
40
42
|
function getHumanFriendlyRuntimeName(runtime) {
|
|
41
43
|
return MESSAGE_FRIENDLY_RUNTIMES[runtime] || runtime;
|
|
@@ -11,7 +11,7 @@ const discovery = require("../discovery");
|
|
|
11
11
|
const logger_1 = require("../../../../logger");
|
|
12
12
|
const python_1 = require("../../../../functions/python");
|
|
13
13
|
const error_1 = require("../../../../error");
|
|
14
|
-
exports.LATEST_VERSION = "
|
|
14
|
+
exports.LATEST_VERSION = "python312";
|
|
15
15
|
async function tryCreateDelegate(context) {
|
|
16
16
|
const requirementsTextPath = path.join(context.sourceDir, "requirements.txt");
|
|
17
17
|
if (!(await (0, util_1.promisify)(fs.exists)(requirementsTextPath))) {
|
|
@@ -35,6 +35,9 @@ function getPythonBinary(runtime) {
|
|
|
35
35
|
else if (runtime === "python311") {
|
|
36
36
|
return "python3.11";
|
|
37
37
|
}
|
|
38
|
+
else if (runtime === "python312") {
|
|
39
|
+
return "python3.12";
|
|
40
|
+
}
|
|
38
41
|
return "python";
|
|
39
42
|
}
|
|
40
43
|
exports.getPythonBinary = getPythonBinary;
|
|
@@ -9,7 +9,7 @@ async function getDatabase(project, databaseId) {
|
|
|
9
9
|
if (dbCache.has(key)) {
|
|
10
10
|
return dbCache.get(key);
|
|
11
11
|
}
|
|
12
|
-
const db = await firestore.getDatabase(project, databaseId);
|
|
12
|
+
const db = await firestore.getDatabase(project, databaseId, false);
|
|
13
13
|
dbCache.set(key, db);
|
|
14
14
|
return db;
|
|
15
15
|
}
|
|
@@ -23,9 +23,9 @@ const EMULATOR_UPDATE_DETAILS = {
|
|
|
23
23
|
expectedChecksum: "2fd771101c0e1f7898c04c9204f2ce63",
|
|
24
24
|
},
|
|
25
25
|
firestore: {
|
|
26
|
-
version: "1.
|
|
27
|
-
expectedSize:
|
|
28
|
-
expectedChecksum: "
|
|
26
|
+
version: "1.19.1",
|
|
27
|
+
expectedSize: 67187672,
|
|
28
|
+
expectedChecksum: "859b1ac2a6040cccddd993c43586347c",
|
|
29
29
|
},
|
|
30
30
|
storage: {
|
|
31
31
|
version: "1.1.3",
|
|
@@ -331,7 +331,7 @@ function createAuthExpressionValue(opts) {
|
|
|
331
331
|
return toExpressionValue(null);
|
|
332
332
|
}
|
|
333
333
|
else {
|
|
334
|
-
const tokenPayload = jwt.decode(opts.token);
|
|
334
|
+
const tokenPayload = jwt.decode(opts.token, { json: true });
|
|
335
335
|
const jsonValue = {
|
|
336
336
|
uid: tokenPayload.user_id,
|
|
337
337
|
token: tokenPayload,
|
package/lib/experiments.js
CHANGED
|
@@ -72,9 +72,9 @@ exports.ALL_EXPERIMENTS = experiments({
|
|
|
72
72
|
"These commands are not meant for public consumption and may break or disappear " +
|
|
73
73
|
"without a notice.",
|
|
74
74
|
},
|
|
75
|
-
|
|
75
|
+
apphosting: {
|
|
76
76
|
shortDescription: "Allow CLI option for Frameworks",
|
|
77
|
-
default:
|
|
77
|
+
default: false,
|
|
78
78
|
public: false,
|
|
79
79
|
},
|
|
80
80
|
});
|
package/lib/firestore/delete.js
CHANGED
|
@@ -219,7 +219,7 @@ class FirestoreDelete {
|
|
|
219
219
|
}
|
|
220
220
|
numPendingDeletes++;
|
|
221
221
|
firestore
|
|
222
|
-
.deleteDocuments(this.project, toDelete)
|
|
222
|
+
.deleteDocuments(this.project, toDelete, true)
|
|
223
223
|
.then((numDeleted) => {
|
|
224
224
|
FirestoreDelete.progressBar.tick(numDeleted);
|
|
225
225
|
numDocsDeleted += numDeleted;
|
|
@@ -280,7 +280,7 @@ class FirestoreDelete {
|
|
|
280
280
|
let initialDelete;
|
|
281
281
|
if (this.isDocumentPath) {
|
|
282
282
|
const doc = { name: this.root + "/" + this.path };
|
|
283
|
-
initialDelete = firestore.deleteDocument(doc).catch((err) => {
|
|
283
|
+
initialDelete = firestore.deleteDocument(doc, true).catch((err) => {
|
|
284
284
|
logger_1.logger.debug("deletePath:initialDelete:error", err);
|
|
285
285
|
if (this.allDescendants) {
|
|
286
286
|
return Promise.resolve();
|
|
@@ -297,7 +297,7 @@ class FirestoreDelete {
|
|
|
297
297
|
}
|
|
298
298
|
deleteDatabase() {
|
|
299
299
|
return firestore
|
|
300
|
-
.listCollectionIds(this.project)
|
|
300
|
+
.listCollectionIds(this.project, true)
|
|
301
301
|
.catch((err) => {
|
|
302
302
|
logger_1.logger.debug("deleteDatabase:listCollectionIds:error", err);
|
|
303
303
|
return utils.reject("Unable to list collection IDs");
|
package/lib/gcp/apphosting.js
CHANGED
|
@@ -8,6 +8,7 @@ const api_1 = require("../api");
|
|
|
8
8
|
const ensureApiEnabled_1 = require("../ensureApiEnabled");
|
|
9
9
|
const deploymentTool = require("../deploymentTool");
|
|
10
10
|
const error_1 = require("../error");
|
|
11
|
+
const metaprogramming_1 = require("../metaprogramming");
|
|
11
12
|
exports.API_HOST = new URL(api_1.apphostingOrigin).host;
|
|
12
13
|
exports.API_VERSION = "v1alpha";
|
|
13
14
|
exports.client = new apiv2_1.Client({
|
|
@@ -15,6 +16,10 @@ exports.client = new apiv2_1.Client({
|
|
|
15
16
|
auth: true,
|
|
16
17
|
apiVersion: exports.API_VERSION,
|
|
17
18
|
});
|
|
19
|
+
(0, metaprogramming_1.assertImplements)();
|
|
20
|
+
(0, metaprogramming_1.assertImplements)();
|
|
21
|
+
(0, metaprogramming_1.assertImplements)();
|
|
22
|
+
(0, metaprogramming_1.assertImplements)();
|
|
18
23
|
async function createBackend(projectId, location, backendReqBoby, backendId) {
|
|
19
24
|
const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends`, Object.assign(Object.assign({}, backendReqBoby), { labels: Object.assign(Object.assign({}, backendReqBoby.labels), deploymentTool.labels()) }), { queryParams: { backendId } });
|
|
20
25
|
return res.body;
|
|
@@ -68,8 +73,8 @@ async function createBuild(projectId, location, backendId, buildId, buildInput)
|
|
|
68
73
|
return res.body;
|
|
69
74
|
}
|
|
70
75
|
exports.createBuild = createBuild;
|
|
71
|
-
async function createRollout(projectId, location, backendId, rolloutId, rollout) {
|
|
72
|
-
const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`, Object.assign(Object.assign({}, rollout), { labels: Object.assign(Object.assign({}, rollout.labels), deploymentTool.labels()) }), { queryParams: { rolloutId } });
|
|
76
|
+
async function createRollout(projectId, location, backendId, rolloutId, rollout, validateOnly = false) {
|
|
77
|
+
const res = await exports.client.post(`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`, Object.assign(Object.assign({}, rollout), { labels: Object.assign(Object.assign({}, rollout.labels), deploymentTool.labels()) }), { queryParams: { rolloutId, validateOnly: validateOnly ? "true" : "false" } });
|
|
73
78
|
return res.body;
|
|
74
79
|
}
|
|
75
80
|
exports.createRollout = createRollout;
|
|
@@ -92,11 +97,7 @@ async function listRollouts(projectId, location, backendId) {
|
|
|
92
97
|
}
|
|
93
98
|
exports.listRollouts = listRollouts;
|
|
94
99
|
async function updateTraffic(projectId, location, backendId, traffic) {
|
|
95
|
-
const
|
|
96
|
-
if ("rolloutPolicy" in traffic) {
|
|
97
|
-
trafficCopy.rolloutPolicy = {};
|
|
98
|
-
}
|
|
99
|
-
const fieldMasks = proto.fieldMasks(trafficCopy);
|
|
100
|
+
const fieldMasks = proto.fieldMasks(traffic, "rolloutPolicy");
|
|
100
101
|
const queryParams = {
|
|
101
102
|
updateMask: fieldMasks.join(","),
|
|
102
103
|
};
|
|
@@ -125,7 +126,7 @@ async function listLocations(projectId) {
|
|
|
125
126
|
exports.listLocations = listLocations;
|
|
126
127
|
async function ensureApiEnabled(options) {
|
|
127
128
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
128
|
-
return await (0, ensureApiEnabled_1.ensure)(projectId, exports.API_HOST, "
|
|
129
|
+
return await (0, ensureApiEnabled_1.ensure)(projectId, exports.API_HOST, "app hosting", true);
|
|
129
130
|
}
|
|
130
131
|
exports.ensureApiEnabled = ensureApiEnabled;
|
|
131
132
|
async function getNextRolloutId(projectId, location, backendId, counter) {
|
|
@@ -133,7 +134,7 @@ async function getNextRolloutId(projectId, location, backendId, counter) {
|
|
|
133
134
|
const date = new Date();
|
|
134
135
|
const year = date.getUTCFullYear();
|
|
135
136
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
136
|
-
const day = String(date.
|
|
137
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
137
138
|
if (counter) {
|
|
138
139
|
return `build-${year}-${month}-${day}-${String(counter).padStart(3, "0")}`;
|
|
139
140
|
}
|
package/lib/gcp/cloudbuild.js
CHANGED
|
@@ -46,9 +46,14 @@ async function deleteConnection(projectId, location, connectionId) {
|
|
|
46
46
|
return res.body;
|
|
47
47
|
}
|
|
48
48
|
exports.deleteConnection = deleteConnection;
|
|
49
|
-
async function fetchLinkableRepositories(projectId, location, connectionId) {
|
|
49
|
+
async function fetchLinkableRepositories(projectId, location, connectionId, pageToken = "", pageSize = 1000) {
|
|
50
50
|
const name = `projects/${projectId}/locations/${location}/connections/${connectionId}:fetchLinkableRepositories`;
|
|
51
|
-
const res = await client.get(name
|
|
51
|
+
const res = await client.get(name, {
|
|
52
|
+
queryParams: {
|
|
53
|
+
pageSize,
|
|
54
|
+
pageToken,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
52
57
|
return res.body;
|
|
53
58
|
}
|
|
54
59
|
exports.fetchLinkableRepositories = fetchLinkableRepositories;
|
package/lib/gcp/firestore.js
CHANGED
|
@@ -4,12 +4,18 @@ exports.deleteDocuments = exports.deleteDocument = exports.listCollectionIds = e
|
|
|
4
4
|
const api_1 = require("../api");
|
|
5
5
|
const apiv2_1 = require("../apiv2");
|
|
6
6
|
const logger_1 = require("../logger");
|
|
7
|
-
const
|
|
7
|
+
const prodOnlyClient = new apiv2_1.Client({
|
|
8
8
|
auth: true,
|
|
9
9
|
apiVersion: "v1",
|
|
10
10
|
urlPrefix: api_1.firestoreOrigin,
|
|
11
11
|
});
|
|
12
|
-
|
|
12
|
+
const emuOrProdClient = new apiv2_1.Client({
|
|
13
|
+
auth: true,
|
|
14
|
+
apiVersion: "v1",
|
|
15
|
+
urlPrefix: api_1.firestoreOriginOrEmulator,
|
|
16
|
+
});
|
|
17
|
+
async function getDatabase(project, database, allowEmulator = false) {
|
|
18
|
+
const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
|
|
13
19
|
const url = `projects/${project}/databases/${database}`;
|
|
14
20
|
try {
|
|
15
21
|
const resp = await apiClient.get(url);
|
|
@@ -21,7 +27,8 @@ async function getDatabase(project, database) {
|
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
exports.getDatabase = getDatabase;
|
|
24
|
-
function listCollectionIds(project) {
|
|
30
|
+
function listCollectionIds(project, allowEmulator = false) {
|
|
31
|
+
const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
|
|
25
32
|
const url = "projects/" + project + "/databases/(default)/documents:listCollectionIds";
|
|
26
33
|
const data = {
|
|
27
34
|
pageSize: 2147483647,
|
|
@@ -31,11 +38,13 @@ function listCollectionIds(project) {
|
|
|
31
38
|
});
|
|
32
39
|
}
|
|
33
40
|
exports.listCollectionIds = listCollectionIds;
|
|
34
|
-
async function deleteDocument(doc) {
|
|
41
|
+
async function deleteDocument(doc, allowEmulator = false) {
|
|
42
|
+
const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
|
|
35
43
|
return apiClient.delete(doc.name);
|
|
36
44
|
}
|
|
37
45
|
exports.deleteDocument = deleteDocument;
|
|
38
|
-
async function deleteDocuments(project, docs) {
|
|
46
|
+
async function deleteDocuments(project, docs, allowEmulator = false) {
|
|
47
|
+
const apiClient = allowEmulator ? emuOrProdClient : prodOnlyClient;
|
|
39
48
|
const url = "projects/" + project + "/databases/(default)/documents:commit";
|
|
40
49
|
const writes = docs.map((doc) => {
|
|
41
50
|
return { delete: doc.name };
|
package/lib/hosting/options.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.orchestrateRollout = exports.setDefaultTrafficPolicy = exports.createBackend = exports.doSetup = void 0;
|
|
4
4
|
const clc = require("colorette");
|
|
5
5
|
const repo = require("./repo");
|
|
6
6
|
const poller = require("../../../operation-poller");
|
|
@@ -8,18 +8,21 @@ const apphosting = require("../../../gcp/apphosting");
|
|
|
8
8
|
const utils_1 = require("../../../utils");
|
|
9
9
|
const api_1 = require("../../../api");
|
|
10
10
|
const apphosting_1 = require("../../../gcp/apphosting");
|
|
11
|
+
const resourceManager_1 = require("../../../gcp/resourceManager");
|
|
12
|
+
const iam_1 = require("../../../gcp/iam");
|
|
11
13
|
const error_1 = require("../../../error");
|
|
12
14
|
const prompt_1 = require("../../../prompt");
|
|
13
15
|
const constants_1 = require("./constants");
|
|
14
16
|
const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
|
|
15
17
|
const deploymentTool = require("../../../deploymentTool");
|
|
18
|
+
const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
|
|
16
19
|
const apphostingPollerOptions = {
|
|
17
20
|
apiOrigin: api_1.apphostingOrigin,
|
|
18
21
|
apiVersion: apphosting_1.API_VERSION,
|
|
19
22
|
masterTimeout: 25 * 60 * 1000,
|
|
20
23
|
maxBackoff: 10000,
|
|
21
24
|
};
|
|
22
|
-
async function doSetup(
|
|
25
|
+
async function doSetup(projectId, location, serviceAccount) {
|
|
23
26
|
await Promise.all([
|
|
24
27
|
(0, ensureApiEnabled_1.ensure)(projectId, "cloudbuild.googleapis.com", "apphosting", true),
|
|
25
28
|
(0, ensureApiEnabled_1.ensure)(projectId, "secretmanager.googleapis.com", "apphosting", true),
|
|
@@ -27,53 +30,38 @@ async function doSetup(setup, projectId) {
|
|
|
27
30
|
(0, ensureApiEnabled_1.ensure)(projectId, "artifactregistry.googleapis.com", "apphosting", true),
|
|
28
31
|
]);
|
|
29
32
|
const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);
|
|
30
|
-
if (
|
|
31
|
-
if (!allowedLocations.includes(
|
|
32
|
-
throw new error_1.FirebaseError(`Invalid location ${
|
|
33
|
+
if (location) {
|
|
34
|
+
if (!allowedLocations.includes(location)) {
|
|
35
|
+
throw new error_1.FirebaseError(`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`);
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
(0, utils_1.logBullet)("First we need a few details to create your backend.");
|
|
36
|
-
|
|
39
|
+
location =
|
|
40
|
+
location ||
|
|
41
|
+
(await (0, prompt_1.promptOnce)({
|
|
42
|
+
name: "region",
|
|
43
|
+
type: "list",
|
|
44
|
+
default: constants_1.DEFAULT_REGION,
|
|
45
|
+
message: "Please select a region " +
|
|
46
|
+
`(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
|
|
47
|
+
choices: allowedLocations.map((loc) => ({ value: loc })),
|
|
48
|
+
}));
|
|
37
49
|
(0, utils_1.logSuccess)(`Region set to ${location}.\n`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await apphosting.getBackend(projectId, location, backendId);
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
if (err.status === 404) {
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
throw new error_1.FirebaseError(`Failed to check if backend with id ${backendId} already exists in ${location}`, { original: err });
|
|
54
|
-
}
|
|
55
|
-
(0, utils_1.logWarning)(`Backend with id ${backendId} already exists in ${location}`);
|
|
56
|
-
}
|
|
57
|
-
const backend = await onboardBackend(projectId, location, backendId);
|
|
50
|
+
const backendId = await promptNewBackendId(projectId, location, {
|
|
51
|
+
name: "backendId",
|
|
52
|
+
type: "input",
|
|
53
|
+
default: "my-web-app",
|
|
54
|
+
message: "Create a name for your backend [1-30 characters]",
|
|
55
|
+
});
|
|
56
|
+
const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
|
|
57
|
+
const backend = await createBackend(projectId, location, backendId, cloudBuildConnRepo, serviceAccount);
|
|
58
58
|
const branch = await (0, prompt_1.promptOnce)({
|
|
59
59
|
name: "branch",
|
|
60
60
|
type: "input",
|
|
61
61
|
default: "main",
|
|
62
62
|
message: "Pick a branch for continuous deployment",
|
|
63
63
|
});
|
|
64
|
-
|
|
65
|
-
rolloutPolicy: {
|
|
66
|
-
codebaseBranch: branch,
|
|
67
|
-
stages: [
|
|
68
|
-
{
|
|
69
|
-
progression: "IMMEDIATE",
|
|
70
|
-
targetPercent: 100,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
|
|
76
|
-
await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
64
|
+
await setDefaultTrafficPolicy(projectId, location, backendId, branch);
|
|
77
65
|
const confirmRollout = await (0, prompt_1.promptOnce)({
|
|
78
66
|
type: "confirm",
|
|
79
67
|
name: "rollout",
|
|
@@ -85,68 +73,136 @@ async function doSetup(setup, projectId) {
|
|
|
85
73
|
(0, utils_1.logSuccess)(`Your site will be deployed at:\n\thttps://${backend.uri}`);
|
|
86
74
|
return;
|
|
87
75
|
}
|
|
88
|
-
|
|
76
|
+
await orchestrateRollout(projectId, location, backendId, {
|
|
89
77
|
source: {
|
|
90
78
|
codebase: {
|
|
91
79
|
branch,
|
|
92
80
|
},
|
|
93
81
|
},
|
|
94
82
|
});
|
|
95
|
-
if (build.state !== "READY") {
|
|
96
|
-
if (!build.buildLogsUri) {
|
|
97
|
-
throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
|
|
98
|
-
"This is an internal error and should be reported");
|
|
99
|
-
}
|
|
100
|
-
throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
|
|
101
|
-
}
|
|
102
83
|
(0, utils_1.logSuccess)(`Successfully created backend:\n\t${backend.name}`);
|
|
103
84
|
(0, utils_1.logSuccess)(`Your site is now deployed at:\n\thttps://${backend.uri}`);
|
|
104
|
-
(0, utils_1.logSuccess)(`View the rollout status by running:\n\tfirebase apphosting:backends:get ${backendId} --project ${projectId}`);
|
|
105
85
|
}
|
|
106
86
|
exports.doSetup = doSetup;
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
87
|
+
async function promptNewBackendId(projectId, location, prompt) {
|
|
88
|
+
while (true) {
|
|
89
|
+
const backendId = await (0, prompt_1.promptOnce)(prompt);
|
|
90
|
+
try {
|
|
91
|
+
await apphosting.getBackend(projectId, location, backendId);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.status === 404) {
|
|
95
|
+
return backendId;
|
|
96
|
+
}
|
|
97
|
+
throw new error_1.FirebaseError(`Failed to check if backend with id ${backendId} already exists in ${location}`, { original: err });
|
|
98
|
+
}
|
|
99
|
+
(0, utils_1.logWarning)(`Backend with id ${backendId} already exists in ${location}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function defaultComputeServiceAccountEmail(projectId) {
|
|
103
|
+
return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
|
|
116
104
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
105
|
+
async function createBackend(projectId, location, backendId, repository, serviceAccount) {
|
|
106
|
+
const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
|
|
107
|
+
const backendReqBody = {
|
|
119
108
|
servingLocality: "GLOBAL_ACCESS",
|
|
120
109
|
codebase: {
|
|
121
|
-
repository: `${
|
|
110
|
+
repository: `${repository.name}`,
|
|
122
111
|
rootDirectory: "/",
|
|
123
112
|
},
|
|
124
113
|
labels: deploymentTool.labels(),
|
|
114
|
+
computeServiceAccount: serviceAccount || defaultServiceAccount,
|
|
125
115
|
};
|
|
116
|
+
delete backendReqBody.computeServiceAccount;
|
|
117
|
+
async function createBackendAndPoll() {
|
|
118
|
+
const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
|
|
119
|
+
return await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return await createBackendAndPoll();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (err.status === 403 && err.message.includes(defaultServiceAccount)) {
|
|
126
|
+
await provisionDefaultComputeServiceAccount(projectId);
|
|
127
|
+
return await createBackendAndPoll();
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
exports.createBackend = createBackend;
|
|
133
|
+
async function provisionDefaultComputeServiceAccount(projectId) {
|
|
134
|
+
try {
|
|
135
|
+
await (0, iam_1.createServiceAccount)(projectId, DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME, "Firebase App Hosting compute service account", "Default service account used to run builds and deploys for Firebase App Hosting");
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err.status !== 409) {
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await (0, resourceManager_1.addServiceAccountToRoles)(projectId, defaultComputeServiceAccountEmail(projectId), [
|
|
143
|
+
"roles/firebaseapphosting.viewer",
|
|
144
|
+
"roles/artifactregistry.createOnPushWriter",
|
|
145
|
+
"roles/logging.logWriter",
|
|
146
|
+
"roles/storage.objectAdmin",
|
|
147
|
+
"roles/firebase.sdkAdminServiceAgent",
|
|
148
|
+
], true);
|
|
131
149
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
async function setDefaultTrafficPolicy(projectId, location, backendId, codebaseBranch) {
|
|
151
|
+
const traffic = {
|
|
152
|
+
rolloutPolicy: {
|
|
153
|
+
codebaseBranch: codebaseBranch,
|
|
154
|
+
stages: [
|
|
155
|
+
{
|
|
156
|
+
progression: "IMMEDIATE",
|
|
157
|
+
targetPercent: 100,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
|
|
163
|
+
await poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, operationResourceName: op.name }));
|
|
137
164
|
}
|
|
138
|
-
exports.
|
|
139
|
-
async function
|
|
165
|
+
exports.setDefaultTrafficPolicy = setDefaultTrafficPolicy;
|
|
166
|
+
async function orchestrateRollout(projectId, location, backendId, buildInput) {
|
|
140
167
|
(0, utils_1.logBullet)("Starting a new rollout... this may take a few minutes.");
|
|
141
168
|
const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
|
|
142
169
|
const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
|
|
143
|
-
const
|
|
170
|
+
const rolloutBody = {
|
|
144
171
|
build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`,
|
|
145
|
-
}
|
|
172
|
+
};
|
|
173
|
+
let tries = 0;
|
|
174
|
+
let done = false;
|
|
175
|
+
while (!done) {
|
|
176
|
+
tries++;
|
|
177
|
+
try {
|
|
178
|
+
const validateOnly = true;
|
|
179
|
+
await apphosting.createRollout(projectId, location, backendId, buildId, rolloutBody, validateOnly);
|
|
180
|
+
done = true;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err instanceof error_1.FirebaseError && err.status === 400) {
|
|
184
|
+
if (tries >= 5) {
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, rolloutBody);
|
|
146
195
|
const rolloutPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`, operationResourceName: rolloutOp.name }));
|
|
147
196
|
const buildPoll = poller.pollOperation(Object.assign(Object.assign({}, apphostingPollerOptions), { pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`, operationResourceName: buildOp.name }));
|
|
148
197
|
const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);
|
|
149
198
|
(0, utils_1.logSuccess)("Rollout completed.");
|
|
199
|
+
if (build.state !== "READY") {
|
|
200
|
+
if (!build.buildLogsUri) {
|
|
201
|
+
throw new error_1.FirebaseError("Failed to build your app, but failed to get build logs as well. " +
|
|
202
|
+
"This is an internal error and should be reported");
|
|
203
|
+
}
|
|
204
|
+
throw new error_1.FirebaseError(`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`, { children: [build.error] });
|
|
205
|
+
}
|
|
150
206
|
return { rollout, build };
|
|
151
207
|
}
|
|
152
|
-
exports.
|
|
208
|
+
exports.orchestrateRollout = orchestrateRollout;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
|
|
3
|
+
exports.fetchAllRepositories = exports.listAppHostingConnections = exports.getOrCreateRepository = exports.getOrCreateConnection = exports.createConnection = exports.linkGitHubRepository = exports.parseConnectionName = void 0;
|
|
4
4
|
const clc = require("colorette");
|
|
5
5
|
const gcb = require("../../../gcp/cloudbuild");
|
|
6
6
|
const rm = require("../../../gcp/resourceManager");
|
|
@@ -10,11 +10,13 @@ const api_1 = require("../../../api");
|
|
|
10
10
|
const error_1 = require("../../../error");
|
|
11
11
|
const prompt_1 = require("../../../prompt");
|
|
12
12
|
const getProjectNumber_1 = require("../../../getProjectNumber");
|
|
13
|
+
const fuzzy = require("fuzzy");
|
|
14
|
+
const inquirer = require("inquirer");
|
|
13
15
|
const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/;
|
|
14
16
|
const APPHOSTING_OAUTH_CONN_NAME = "apphosting-github-oauth";
|
|
15
17
|
const CONNECTION_NAME_REGEX = /^projects\/(?<projectId>[^\/]+)\/locations\/(?<location>[^\/]+)\/connections\/(?<id>[^\/]+)$/;
|
|
16
18
|
function parseConnectionName(name) {
|
|
17
|
-
const match =
|
|
19
|
+
const match = CONNECTION_NAME_REGEX.exec(name);
|
|
18
20
|
if (!match || typeof match.groups === undefined) {
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
@@ -47,19 +49,22 @@ function generateConnectionId() {
|
|
|
47
49
|
const randomHash = Math.random().toString(36).slice(6);
|
|
48
50
|
return `apphosting-github-conn-${randomHash}`;
|
|
49
51
|
}
|
|
52
|
+
const ADD_REPO_CHOICE = "@ADD_REPO";
|
|
53
|
+
const ADD_CONN_CHOICE = "@ADD_CONN";
|
|
54
|
+
const CONFIGURE_REMOTE_URI_CHOICES = [ADD_REPO_CHOICE, ADD_CONN_CHOICE];
|
|
50
55
|
async function linkGitHubRepository(projectId, location) {
|
|
51
|
-
var _a, _b, _c;
|
|
56
|
+
var _a, _b, _c, _d;
|
|
52
57
|
utils.logBullet(clc.bold(`${clc.yellow("===")} Set up a GitHub connection`));
|
|
58
|
+
let oauthConn = await getOrCreateConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
|
|
59
|
+
while (oauthConn.installationState.stage === "PENDING_USER_OAUTH") {
|
|
60
|
+
oauthConn = await promptConnectionAuth(oauthConn);
|
|
61
|
+
}
|
|
53
62
|
const existingConns = await listAppHostingConnections(projectId);
|
|
54
63
|
if (existingConns.length < 1) {
|
|
55
64
|
const grantSuccess = await promptSecretManagerAdminGrant(projectId);
|
|
56
65
|
if (!grantSuccess) {
|
|
57
66
|
throw new error_1.FirebaseError("Insufficient IAM permissions to create a new connection to GitHub");
|
|
58
67
|
}
|
|
59
|
-
let oauthConn = await getOrCreateConnection(projectId, location, APPHOSTING_OAUTH_CONN_NAME);
|
|
60
|
-
while (oauthConn.installationState.stage === "PENDING_USER_OAUTH") {
|
|
61
|
-
oauthConn = await promptConnectionAuth(oauthConn);
|
|
62
|
-
}
|
|
63
68
|
const connectionId = generateConnectionId();
|
|
64
69
|
const conn = await createConnection(projectId, location, connectionId, {
|
|
65
70
|
authorizerCredential: (_a = oauthConn.githubConfig) === null || _a === void 0 ? void 0 : _a.authorizerCredential,
|
|
@@ -71,20 +76,33 @@ async function linkGitHubRepository(projectId, location) {
|
|
|
71
76
|
existingConns.push(refreshedConn);
|
|
72
77
|
}
|
|
73
78
|
let { remoteUri, connection } = await promptRepositoryUri(projectId, existingConns);
|
|
74
|
-
while (remoteUri
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
while (CONFIGURE_REMOTE_URI_CHOICES.includes(remoteUri)) {
|
|
80
|
+
if (remoteUri === ADD_REPO_CHOICE) {
|
|
81
|
+
await utils.openInBrowser("https://github.com/apps/google-cloud-build/installations/new");
|
|
82
|
+
await (0, prompt_1.promptOnce)({
|
|
83
|
+
type: "input",
|
|
84
|
+
message: "Press ENTER once you have provided the GitHub app installation with access to your desired repository.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else if (remoteUri === ADD_CONN_CHOICE) {
|
|
88
|
+
const connectionId = generateConnectionId();
|
|
89
|
+
const conn = await createConnection(projectId, location, connectionId, {
|
|
90
|
+
authorizerCredential: (_b = oauthConn.githubConfig) === null || _b === void 0 ? void 0 : _b.authorizerCredential,
|
|
91
|
+
});
|
|
92
|
+
let refreshedConn = conn;
|
|
93
|
+
while (refreshedConn.installationState.stage !== "COMPLETE") {
|
|
94
|
+
refreshedConn = await promptAppInstall(conn);
|
|
95
|
+
}
|
|
96
|
+
existingConns.push(refreshedConn);
|
|
97
|
+
}
|
|
80
98
|
const selection = await promptRepositoryUri(projectId, existingConns);
|
|
81
99
|
remoteUri = selection.remoteUri;
|
|
82
100
|
connection = selection.connection;
|
|
83
101
|
}
|
|
84
102
|
const { id: connectionId } = parseConnectionName(connection.name);
|
|
85
103
|
await getOrCreateConnection(projectId, location, connectionId, {
|
|
86
|
-
authorizerCredential: (
|
|
87
|
-
appInstallationId: (
|
|
104
|
+
authorizerCredential: (_c = connection.githubConfig) === null || _c === void 0 ? void 0 : _c.authorizerCredential,
|
|
105
|
+
appInstallationId: (_d = connection.githubConfig) === null || _d === void 0 ? void 0 : _d.appInstallationId,
|
|
88
106
|
});
|
|
89
107
|
const repo = await getOrCreateRepository(projectId, location, connectionId, remoteUri);
|
|
90
108
|
utils.logSuccess(`Successfully linked GitHub repository at remote URI`);
|
|
@@ -93,28 +111,36 @@ async function linkGitHubRepository(projectId, location) {
|
|
|
93
111
|
}
|
|
94
112
|
exports.linkGitHubRepository = linkGitHubRepository;
|
|
95
113
|
async function promptRepositoryUri(projectId, connections) {
|
|
96
|
-
const remoteUriToConnection =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const { repos, remoteUriToConnection } = await fetchAllRepositories(projectId, connections);
|
|
115
|
+
const searchRepos = (repos) => async (_, input = "") => {
|
|
116
|
+
return [
|
|
117
|
+
new inquirer.Separator(),
|
|
118
|
+
{
|
|
119
|
+
name: "Missing a repo? Select this option to configure your installation's access settings",
|
|
120
|
+
value: ADD_REPO_CHOICE,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "Missing an account or org? Select this option to create a new connection",
|
|
124
|
+
value: ADD_CONN_CHOICE,
|
|
125
|
+
},
|
|
126
|
+
new inquirer.Separator(),
|
|
127
|
+
...fuzzy
|
|
128
|
+
.filter(input, repos, {
|
|
129
|
+
extract: (repo) => extractRepoSlugFromUri(repo.remoteUri) || "",
|
|
130
|
+
})
|
|
131
|
+
.map((result) => {
|
|
132
|
+
return {
|
|
133
|
+
name: extractRepoSlugFromUri(result.original.remoteUri) || "",
|
|
134
|
+
value: result.original.remoteUri,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
];
|
|
138
|
+
};
|
|
114
139
|
const remoteUri = await (0, prompt_1.promptOnce)({
|
|
115
|
-
type: "
|
|
140
|
+
type: "autocomplete",
|
|
141
|
+
name: "remoteUri",
|
|
116
142
|
message: "Which of the following repositories would you like to deploy?",
|
|
117
|
-
|
|
143
|
+
source: searchRepos(repos),
|
|
118
144
|
});
|
|
119
145
|
return { remoteUri, connection: remoteUriToConnection[remoteUri] };
|
|
120
146
|
}
|
|
@@ -217,3 +243,25 @@ async function listAppHostingConnections(projectId) {
|
|
|
217
243
|
!conn.disabled);
|
|
218
244
|
}
|
|
219
245
|
exports.listAppHostingConnections = listAppHostingConnections;
|
|
246
|
+
async function fetchAllRepositories(projectId, connections) {
|
|
247
|
+
const repos = [];
|
|
248
|
+
const remoteUriToConnection = {};
|
|
249
|
+
const getNextPage = async (conn, pageToken = "") => {
|
|
250
|
+
const { location, id } = parseConnectionName(conn.name);
|
|
251
|
+
const resp = await gcb.fetchLinkableRepositories(projectId, location, id, pageToken);
|
|
252
|
+
if (resp.repositories && resp.repositories.length > 0) {
|
|
253
|
+
for (const repo of resp.repositories) {
|
|
254
|
+
repos.push(repo);
|
|
255
|
+
remoteUriToConnection[repo.remoteUri] = conn;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (resp.nextPageToken) {
|
|
259
|
+
await getNextPage(conn, resp.nextPageToken);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
for (const conn of connections) {
|
|
263
|
+
await getNextPage(conn);
|
|
264
|
+
}
|
|
265
|
+
return { repos, remoteUriToConnection };
|
|
266
|
+
}
|
|
267
|
+
exports.fetchAllRepositories = fetchAllRepositories;
|
|
@@ -160,6 +160,11 @@ function writeChannelActionYMLFile(ymlPath, secretName, projectId, script) {
|
|
|
160
160
|
const workflowConfig = {
|
|
161
161
|
name: "Deploy to Firebase Hosting on PR",
|
|
162
162
|
on: "pull_request",
|
|
163
|
+
permissions: {
|
|
164
|
+
checks: "write",
|
|
165
|
+
contents: "read",
|
|
166
|
+
"pull-requests": "write",
|
|
167
|
+
},
|
|
163
168
|
jobs: {
|
|
164
169
|
["build_and_preview"]: {
|
|
165
170
|
if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}",
|
package/lib/metaprogramming.js
CHANGED
package/lib/prompt.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.confirm = exports.promptOnce = exports.prompt = void 0;
|
|
4
4
|
const inquirer = require("inquirer");
|
|
5
5
|
const error_1 = require("./error");
|
|
6
|
+
inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt"));
|
|
6
7
|
async function prompt(options, questions) {
|
|
7
8
|
const prompts = [];
|
|
8
9
|
for (const question of questions) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firebase-tools",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.3.1",
|
|
4
4
|
"description": "Command-Line Interface for Firebase",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -80,9 +80,11 @@
|
|
|
80
80
|
"filesize": "^6.1.0",
|
|
81
81
|
"form-data": "^4.0.0",
|
|
82
82
|
"fs-extra": "^10.1.0",
|
|
83
|
+
"fuzzy": "^0.1.3",
|
|
83
84
|
"glob": "^7.1.2",
|
|
84
85
|
"google-auth-library": "^7.11.0",
|
|
85
|
-
"inquirer": "^8.2.
|
|
86
|
+
"inquirer": "^8.2.6",
|
|
87
|
+
"inquirer-autocomplete-prompt": "^2.0.1",
|
|
86
88
|
"js-yaml": "^3.13.1",
|
|
87
89
|
"jsonwebtoken": "^9.0.0",
|
|
88
90
|
"leven": "^3.1.0",
|