forked from Azure/static-web-apps-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.ts
More file actions
360 lines (317 loc) · 13.6 KB
/
deploy.ts
File metadata and controls
360 lines (317 loc) · 13.6 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import chalk from "chalk";
import { spawn } from "node:child_process";
import fs from "node:fs";
import ora, { Ora } from "ora";
import path from "node:path";
import { findSWAConfigFile } from "../../../core/utils/user-config.js";
import { getCurrentSwaCliConfigFromFile, updateSwaCliConfigFile } from "../../../core/utils/cli-config.js";
import { logger, logGitHubIssueMessageAndExit } from "../../../core/utils/logger.js";
import { isUserOrConfigOption } from "../../../core/utils/options.js";
import { readWorkflowFile } from "../../../core/utils/workflow-config.js";
import { chooseOrCreateProjectDetails, getStaticSiteDeployment } from "../../../core/account.js";
import { DEFAULT_RUNTIME_LANGUAGE } from "../../../core/constants.js";
import { cleanUp, getDeployClientPath } from "../../../core/deploy-client.js";
import { swaCLIEnv } from "../../../core/env.js";
import { getDefaultVersion } from "../../../core/functions-versions.js";
import { login } from "../login/login.js";
import { loadPackageJson } from "../../../core/utils/json.js";
const packageInfo = loadPackageJson();
export async function deploy(options: SWACLIConfig) {
const { SWA_CLI_DEPLOYMENT_TOKEN, SWA_CLI_DEBUG } = swaCLIEnv();
const isVerboseEnabled = SWA_CLI_DEBUG === "silly";
let {
appLocation,
apiLocation,
dataApiLocation,
outputLocation,
dryRun,
deploymentToken,
printToken,
appName,
swaConfigLocation,
verbose,
apiLanguage,
apiVersion,
} = options;
if (dryRun) {
logger.warn("***********************************************************************");
logger.warn("* WARNING: Running in dry run mode. This project will not be deployed *");
logger.warn("***********************************************************************");
logger.warn("");
}
// make sure appLocation is set
appLocation = path.resolve(appLocation || process.cwd());
// make sure dataApiLocation is set
if (dataApiLocation) {
dataApiLocation = path.resolve(dataApiLocation);
if (!fs.existsSync(dataApiLocation)) {
logger.error(`The provided Data API folder ${dataApiLocation} does not exist. Abort.`, true);
return;
} else {
logger.log(`Deploying Data API from folder:`);
logger.log(` ${chalk.green(dataApiLocation)}`);
logger.log(``);
}
}
logger.silly(`Resolving outputLocation=${outputLocation} full path...`);
let resolvedOutputLocation = path.resolve(appLocation, outputLocation as string);
// if folder exists, deploy from a specific build folder (outputLocation), relative to appLocation
if (!fs.existsSync(resolvedOutputLocation)) {
if (!fs.existsSync(outputLocation as string)) {
logger.error(`The folder "${resolvedOutputLocation}" is not found. Exit.`, true);
return;
}
// otherwise, build folder (outputLocation) is using the absolute location
resolvedOutputLocation = path.resolve(outputLocation as string);
}
logger.log(`Deploying front-end files from folder:`);
logger.log(` ${chalk.green(resolvedOutputLocation)}`);
logger.log(``);
// if --api-location is provided, use it as the api folder
let resolvedApiLocation = undefined;
if (apiLocation) {
resolvedApiLocation = path.resolve(apiLocation!);
if (!fs.existsSync(resolvedApiLocation)) {
logger.error(`The provided API folder ${resolvedApiLocation} does not exist. Abort.`, true);
return;
} else {
logger.log(`Deploying API from folder:`);
logger.log(` ${chalk.green(resolvedApiLocation)}`);
logger.log(``);
}
} else {
// otherwise, check if the default api folder exists and print a warning
const apiFolder = await findApiFolderInPath(appLocation);
if (apiFolder) {
logger.warn(
`An API folder was found at ".${
path.sep + path.basename(apiFolder)
}" but the --api-location option was not provided. The API will not be deployed.\n`,
);
}
}
if (!isUserOrConfigOption("apiLanguage")) {
logger.log(`Consider providing api-language and version using --api-language and --api-version flags,
otherwise default values apiLanguage: ${apiLanguage} and apiVersion: ${apiVersion} will apply`);
} else if (!isUserOrConfigOption("apiVersion")) {
if (!apiLanguage) {
apiLanguage = DEFAULT_RUNTIME_LANGUAGE;
}
apiVersion = getDefaultVersion(apiLanguage);
logger.log(`Api language "${apiLanguage}" is provided but api version is not provided.
Assuming default version "${apiVersion}"`);
}
// resolve the deployment token
if (deploymentToken) {
deploymentToken = deploymentToken;
logger.silly("Deployment token provided via flag");
logger.silly({ [chalk.green(`--deployment-token`)]: deploymentToken });
} else if (SWA_CLI_DEPLOYMENT_TOKEN) {
deploymentToken = SWA_CLI_DEPLOYMENT_TOKEN;
logger.silly("Deployment token found in Environment Variables:");
logger.silly({ [chalk.green(`SWA_CLI_DEPLOYMENT_TOKEN`)]: SWA_CLI_DEPLOYMENT_TOKEN });
} else if (dryRun === false) {
logger.silly(`No deployment token found. Trying interactive login...`);
try {
const { credentialChain, subscriptionId } = await login({
...options,
});
logger.silly(`Login successful`);
if (appName) {
logger.log(`\nChecking project "${appName}" settings...`);
} else {
logger.log(`\nChecking project settings...`);
}
const { resourceGroup, staticSiteName } = (await chooseOrCreateProjectDetails(options, credentialChain, subscriptionId, printToken)) as {
resourceGroup: string;
staticSiteName: string;
};
logger.silly(`Project settings:`);
logger.silly({
resourceGroup,
staticSiteName,
subscriptionId,
});
const deploymentTokenResponse = await getStaticSiteDeployment(
credentialChain,
subscriptionId,
resourceGroup as string,
staticSiteName as string,
);
deploymentToken = deploymentTokenResponse?.properties?.apiKey;
if (!deploymentToken) {
logger.error("Cannot find a deployment token. Aborting.", true);
} else {
logger.log(chalk.green(`✔ Successfully setup project!`));
// store project settings in swa-cli.config.json (if available)
if (dryRun === false) {
const currentSwaCliConfig = getCurrentSwaCliConfigFromFile();
if (currentSwaCliConfig?.config) {
logger.silly(`Saving project settings to swa-cli.config.json...`);
const newConfig = { ...currentSwaCliConfig?.config };
newConfig.appName = staticSiteName;
newConfig.resourceGroup = resourceGroup;
updateSwaCliConfigFile(newConfig);
} else {
logger.silly(`No swa-cli.config.json file found. Skipping saving project settings.`);
}
}
logger.silly("\nDeployment token provided via remote configuration");
logger.silly({ [chalk.green(`deploymentToken`)]: deploymentToken });
}
} catch (error: any) {
logger.error(error.message);
return;
}
}
logger.log(`\nDeploying to environment: ${chalk.green(options.env)}\n`);
if (printToken) {
logger.log(`Deployment token:`);
logger.log(chalk.green(deploymentToken));
process.exit(0);
}
// TODO: do that in options
// mix CLI args with the project's build workflow configuration (if any)
// use any specific workflow config that the user might provide under ".github/workflows/"
// Note: CLI args will take precedence over workflow config
let userWorkflowConfig: Partial<GithubActionWorkflow> | undefined = {
appLocation,
outputLocation: resolvedOutputLocation,
apiLocation: resolvedApiLocation,
dataApiLocation,
};
try {
userWorkflowConfig = readWorkflowFile({
userWorkflowConfig,
});
} catch (err) {
logger.warn(``);
logger.warn(`Error reading workflow configuration:`);
logger.warn((err as any).message);
logger.warn(
`See https://docs.microsoft.com/azure/static-web-apps/build-configuration?tabs=github-actions#build-configuration for more information.`,
);
}
swaConfigLocation = swaConfigLocation || userWorkflowConfig?.appLocation;
const swaConfigFilePath = (await findSWAConfigFile(swaConfigLocation!))?.filepath;
const resolvedSwaConfigLocation = swaConfigFilePath ? path.dirname(swaConfigFilePath) : undefined;
const cliEnv: SWACLIEnv = {
SWA_CLI_DEBUG: verbose as DebugFilterLevel,
SWA_RUNTIME_WORKFLOW_LOCATION: userWorkflowConfig?.files?.[0],
SWA_RUNTIME_CONFIG_LOCATION: resolvedSwaConfigLocation,
SWA_RUNTIME_CONFIG: swaConfigFilePath,
SWA_CLI_VERSION: packageInfo.version,
SWA_CLI_DEPLOY_DRY_RUN: `${dryRun}`,
SWA_CLI_DEPLOY_BINARY: undefined,
};
const deployClientEnv: StaticSiteClientEnv = {
DEPLOYMENT_ACTION: options.dryRun ? "close" : "upload",
DEPLOYMENT_PROVIDER: "SwaCli",
REPOSITORY_BASE: userWorkflowConfig?.appLocation,
SKIP_APP_BUILD: "true",
SKIP_API_BUILD: "true",
DEPLOYMENT_TOKEN: deploymentToken,
// /!\ Static site client doesn't use OUTPUT_LOCATION at all if SKIP_APP_BUILD is set,
// so you need to provide the output path as the app location
APP_LOCATION: userWorkflowConfig?.outputLocation,
// OUTPUT_LOCATION: outputLocation,
API_LOCATION: userWorkflowConfig?.apiLocation,
DATA_API_LOCATION: userWorkflowConfig?.dataApiLocation,
// If config file is not in output location, we need to tell where to find it
CONFIG_FILE_LOCATION: resolvedSwaConfigLocation,
VERBOSE: isVerboseEnabled ? "true" : "false",
FUNCTION_LANGUAGE: apiLanguage,
FUNCTION_LANGUAGE_VERSION: apiVersion,
};
// set the DEPLOYMENT_ENVIRONMENT env variable only when the user has provided
// a deployment environment which is not "production".
if (options.env?.toLowerCase() !== "production" && options.env?.toLowerCase() !== "prod") {
deployClientEnv.DEPLOYMENT_ENVIRONMENT = options.env;
}
logger.log(`Deploying project to Azure Static Web Apps...`);
let spinner: Ora = {} as Ora;
try {
const { binary, buildId } = await getDeployClientPath();
if (binary) {
spinner = ora();
(cliEnv as any).SWA_CLI_DEPLOY_BINARY = `${binary}@${buildId}`;
spinner.text = `Deploying using ${cliEnv.SWA_CLI_DEPLOY_BINARY}`;
logger.silly(`Deploying using ${cliEnv.SWA_CLI_DEPLOY_BINARY}`);
logger.silly(`Deploying using the following options:`);
logger.silly({ env: { ...cliEnv, ...deployClientEnv } });
spinner.start(`Preparing deployment. Please wait...`);
const child = spawn(binary, [], {
env: {
...swaCLIEnv(cliEnv, deployClientEnv),
},
});
let projectUrl = "";
child.stdout!.on("data", (data) => {
data
.toString()
.trim()
.split("\n")
.forEach((line: string) => {
if (line.includes("Exiting")) {
spinner.text = line;
spinner.stop();
} else if (line.includes("Visit your site at:")) {
projectUrl = line.match("http.*")?.pop()?.trim() as string;
line = "";
}
// catch errors printed to stdout
else if (line.includes("[31m")) {
if (line.includes("Cannot deploy to the function app because Function language info isn't provided.")) {
line = chalk.red(
`Cannot deploy to the function app because Function language info isn't provided, use flags "--api-language" and "--api-version" or add a "platform.apiRuntime" property to your staticwebapp.config.json file, or create one in ${options.outputLocation!}. Please consult the documentation for more information about staticwebapp.config.json: https://learn.microsoft.com/azure/static-web-apps/build-configuration?tabs=github-actions#skip-building-the-api`,
);
}
spinner.fail(chalk.red(line));
} else {
if (isVerboseEnabled || dryRun) {
spinner.info(line.trim());
} else {
spinner.text = line.trim();
}
}
});
});
child.stderr!.on("data", (data) => {
const stderrOutput = data.toString().trim();
if (stderrOutput) {
logger.error(stderrOutput);
}
});
child.on("error", (error) => {
logger.error(error.toString());
});
child.on("close", (code) => {
cleanUp();
if (code === 0) {
spinner.succeed(chalk.green(`Project deployed to ${chalk.underline(projectUrl)} 🚀`));
logger.log(``);
} else {
spinner.fail(chalk.red(`Deployment failed with exit code ${code}`));
logger.error(`The deployment binary exited with code ${code}.`);
logger.error(`If you are running in a minimal container image, ensure native dependencies are installed.`);
logger.error(`Run ${chalk.cyan(`ldd ${binary}`)} to check for missing shared libraries.`);
process.exit(1);
}
});
}
} catch (error) {
logger.error("");
logger.error("Deployment Failed :(");
logger.error(`Deployment Failure Reason: ${(error as any).message}`);
logger.error(
`For further information, please visit the Azure Static Web Apps documentation at https://docs.microsoft.com/azure/static-web-apps/`,
);
logGitHubIssueMessageAndExit();
} finally {
cleanUp();
}
}
async function findApiFolderInPath(appPath: string): Promise<string | undefined> {
const entries = await fs.promises.readdir(appPath, { withFileTypes: true });
return entries.find((entry) => entry.name.toLowerCase() === "api" && entry.isDirectory())?.name;
}