-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathdeploy.ts
More file actions
154 lines (151 loc) · 6.09 KB
/
deploy.ts
File metadata and controls
154 lines (151 loc) · 6.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { requireDatabaseInstance } from "../requireDatabaseInstance";
import { requirePermissions } from "../requirePermissions";
import { checkServiceAccountIam } from "../deploy/functions/checkIam";
import { checkValidTargetFilters } from "../checkValidTargetFilters";
import { Command } from "../command";
import { deploy } from "../deploy";
import { requireConfig } from "../requireConfig";
import { filterTargets } from "../filterTargets";
import { requireHostingSite } from "../requireHostingSite";
import { errNoDefaultSite } from "../getDefaultHostingSite";
import { FirebaseError } from "../error";
import { bold } from "colorette";
import { pickHostingSiteName } from "../hosting/interactive";
import { logBullet } from "../utils";
import { createSite } from "../hosting/api";
import { Options } from "../options";
// in order of least time-consuming to most time-consuming
export const VALID_DEPLOY_TARGETS = [
"database",
"storage",
"firestore",
"functions",
"hosting",
"remoteconfig",
"extensions",
"dataconnect",
"apphosting",
];
export const TARGET_PERMISSIONS: Record<(typeof VALID_DEPLOY_TARGETS)[number], string[]> = {
database: ["firebasedatabase.instances.update"],
hosting: ["firebasehosting.sites.update"],
functions: [
"cloudfunctions.functions.list",
"cloudfunctions.functions.create",
"cloudfunctions.functions.get",
"cloudfunctions.functions.update",
"cloudfunctions.functions.delete",
"cloudfunctions.operations.get",
],
firestore: [
"datastore.indexes.list",
"datastore.indexes.create",
"datastore.indexes.update",
"datastore.indexes.delete",
],
storage: [
"firebaserules.releases.create",
"firebaserules.rulesets.create",
"firebaserules.releases.update",
],
remoteconfig: ["cloudconfig.configs.get", "cloudconfig.configs.update"],
dataconnect: [
"cloudsql.databases.create",
"cloudsql.databases.update",
"cloudsql.instances.connect",
"cloudsql.instances.create", // TODO: Support users who don't have cSQL writer permissions and want to use existing instances
"cloudsql.instances.get",
"cloudsql.instances.list",
"cloudsql.instances.update",
"cloudsql.users.create",
"firebasedataconnect.connectors.create",
"firebasedataconnect.connectors.delete",
"firebasedataconnect.connectors.list",
"firebasedataconnect.connectors.update",
"firebasedataconnect.operations.get",
"firebasedataconnect.services.create",
"firebasedataconnect.services.delete",
"firebasedataconnect.services.update",
"firebasedataconnect.services.list",
"firebasedataconnect.schemas.create",
"firebasedataconnect.schemas.delete",
"firebasedataconnect.schemas.list",
"firebasedataconnect.schemas.update",
],
};
export const command = new Command("deploy")
.description("deploy code and assets to your Firebase project")
.withForce(
"delete Cloud Functions missing from the current working directory and bypass interactive prompts",
)
.option("-p, --public <path>", "override the Hosting public directory specified in firebase.json")
.option("-m, --message <message>", "an optional message describing this deploy")
.option(
"--only <targets>",
'only deploy to specified, comma-separated targets (e.g. "hosting,storage"). For functions, ' +
'can specify filters with colons to scope function deploys to only those functions (e.g. "--only functions:func1,functions:func2"). ' +
"When filtering based on export groups (the exported module object keys), use dots to specify group names " +
'(e.g. "--only functions:group1.subgroup1,functions:group2"). ' +
"When filtering based on codebases, use colons to specify codebase names " +
'(e.g. "--only functions:codebase1:func1,functions:codebase2:group1.subgroup1"). ' +
"For data connect, can specify filters with colons to deploy only a service, connector, or schema" +
'(e.g. "--only dataconnect:serviceId,dataconnect:serviceId:connectorId,dataconnect:serviceId:schema")',
)
.option("--except <targets>", 'deploy to all targets except specified (e.g. "database")')
.option(
"--dry-run",
"perform a dry run of your deployment. Validates your changes and builds your code without deploying any changes to your project. " +
"In order to provide better validation, this may still enable APIs on the target project",
)
.before(requireConfig)
.before((options: Options) => {
options.filteredTargets = filterTargets(options, VALID_DEPLOY_TARGETS);
const permissions = options.filteredTargets.reduce((perms: string[], target: string) => {
return perms.concat(TARGET_PERMISSIONS[target]);
}, []);
return requirePermissions(options, permissions);
})
.before((options: Options) => {
if (options.filteredTargets.includes("functions")) {
return checkServiceAccountIam(options.project!);
}
})
.before(async (options: Options) => {
// only fetch the default instance for hosting or database deploys
if (options.filteredTargets.includes("database")) {
await requireDatabaseInstance(options);
}
if (options.filteredTargets.includes("hosting")) {
let shouldCreateSite = false;
try {
await requireHostingSite(options);
} catch (err: unknown) {
const isPermissionError =
err instanceof FirebaseError &&
err.original instanceof FirebaseError &&
err.original.status === 403;
if (isPermissionError) {
throw err;
} else if (err === errNoDefaultSite) {
shouldCreateSite = true;
}
}
if (!shouldCreateSite) {
return;
}
if (options.nonInteractive) {
throw new FirebaseError(
`Unable to deploy to Hosting as there is no Hosting site. Use ${bold(
"firebase hosting:sites:create",
)} to create a site.`,
);
}
logBullet("No Hosting site detected.");
const siteId = await pickHostingSiteName("", options);
await createSite(options.project!, siteId);
}
})
.before(checkValidTargetFilters)
.action((options) => {
return deploy(options.filteredTargets, options);
});