From 2000ef93e5d0ee739bd2f4745ca3a53a56c5b460 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 22 Mar 2022 11:38:44 +0100 Subject: [PATCH] feat: add --dry-run mode --- readme.md | 11 +-- schema/swa-cli.config.schema.json | 9 +++ src/cli/commands/deploy.ts | 114 ++++++++++++++++++++++-------- src/cli/commands/start.ts | 6 +- src/cli/index.ts | 14 +++- src/config.ts | 1 + src/core/runtimes.ts | 24 ++++--- src/core/utils/cli-config.ts | 2 +- src/core/utils/logger.ts | 15 +++- src/swa.d.ts | 2 + 10 files changed, 145 insertions(+), 53 deletions(-) diff --git a/readme.md b/readme.md index 338cd8df..8eb3c00e 100644 --- a/readme.md +++ b/readme.md @@ -222,11 +222,12 @@ If you need to override the default values for the `swa start` subcommand, you c If you need to override the default values for the `swa deploy` subcommand, you can provide the following options: -| Option | Description | Default | Example | -| -------------------- | -------------------------------------------------------------- | ------- | ---------------------------- | -| `--output-location` | The folder where the front-end public files are location | `./` | `--output-location="./dist"` | -| `--api-location` | The folder containing the source code of the API application | `./api` | `--api-location="./api"` | -| `--deployment-token` | The secret toekn used to authenticate with the Static Web Apps | | `--deployment-token="123"` | +| Option | Description | Default | Example | +| -------------------- | -------------------------------------------------------------- | ------- | ------------------------------- | +| `--output-location` | The folder where the front-end public files are location | `./` | `--output-location="./dist"` | +| `--api-location` | The folder containing the source code of the API application | `./api` | `--api-location="./api"` | +| `--deployment-token` | The secret toekn used to authenticate with the Static Web Apps | | `--deployment-token="123"` | +| `--dry-run` | Simulate a deploy process without actually running it | `false` | `--dry-run` or `--dry-run=true` | ## The CLI `swa-cli.config.json` configuration file diff --git a/schema/swa-cli.config.schema.json b/schema/swa-cli.config.schema.json index e193d80e..d2f787f6 100644 --- a/schema/swa-cli.config.schema.json +++ b/schema/swa-cli.config.schema.json @@ -18,6 +18,10 @@ "description": "The secret toekn used to authenticate with the Static Web Apps", "type": "string" }, + "dryRun": { + "description": "Simulate a deploy process without actually running it", + "type": "boolean" + }, "apiPort": { "description": "The API server port passed to func start", "type": "number" @@ -35,6 +39,11 @@ "type": "string" }, "build": { + "description": "Build the front-end app and API before starting the emulator", + "type": "boolean" + }, + "open": { + "description": "Automatically open the CLI dev server in the default browser", "type": "boolean" }, "customUrlScheme": { diff --git a/src/cli/commands/deploy.ts b/src/cli/commands/deploy.ts index 47b3d848..58894a0b 100644 --- a/src/cli/commands/deploy.ts +++ b/src/cli/commands/deploy.ts @@ -1,13 +1,21 @@ import { spawn } from "child_process"; import path from "path"; import ora from "ora"; -import { logger } from "../../core"; +import { logger, readWorkflowFile } from "../../core"; import { cleanUp, getDeployClientPath } from "../../core/deploy-client"; import chalk from "chalk"; -const pkg = require(path.join(__dirname, "..", "..", "..", "package.json")); +const packageInfo = require(path.join(__dirname, "..", "..", "..", "package.json")); export async function deploy(deployContext: string, options: SWACLIConfig) { + const { SWA_CLI_DEPLOYMENT_TOKEN, SWA_CLI_DEBUG } = process.env; + const isVerboseEnabled = SWA_CLI_DEBUG === "silly"; + + if (options.dryRun) { + logger.warn("", "swa"); + logger.warn("WARNING: Running in dry run mode!", "swa"); + } + if (!options.outputLocation) { logger.error("--output-location option is missing", true); return; @@ -21,16 +29,68 @@ export async function deploy(deployContext: string, options: SWACLIConfig) { let deploymentToken = ""; if (options.deploymentToken) { deploymentToken = options.deploymentToken; - logger.log("Deployment token provide via flag"); - } else if (process.env.SWA_CLI_DEPLOYMENT_TOKEN) { - deploymentToken = process.env.SWA_CLI_DEPLOYMENT_TOKEN; - logger.log("Deployment token found in Environment Variables:"); - logger.log({ SWA_CLI_DEPLOYMENT_TOKEN: "" }); + logger.log("Deployment token provide via flag", "swa"); + } else if (SWA_CLI_DEPLOYMENT_TOKEN) { + deploymentToken = SWA_CLI_DEPLOYMENT_TOKEN; + logger.log("Deployment token found in Environment Variables:", "swa"); + logger.log({ SWA_CLI_DEPLOYMENT_TOKEN }, "swa"); } else { logger.error("--deployment-token option is missing", true); return; } - logger.log(``); + logger.log(``, "swa"); + + let userWorkflowConfig: Partial | undefined = { + appLocation: options.appLocation, + outputLocation: options.outputLocation, + apiLocation: options.apiLocation, + }; + + // mix CLI args with the project's build workflow configuration (if any) + // use any specific workflow config that the user might provide undef ".github/workflows/" + // Note: CLI args will take precedence over workflow config + 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.`, + "swa" + ); + } + + const cliEnv = { + SWA_CLI_DEBUG: options.verbose, + SWA_CLI_ROUTES_LOCATION: options.swaConfigLocation, + SWA_WORKFLOW_FILES: userWorkflowConfig?.files?.join(","), + SWA_CLI_VERSION: packageInfo.version, + SWA_CLI_DEPLOY_DRY_RUN: `${options.dryRun}`, + }; + + const deployClientEnv = { + DEPLOYMENT_ACTION: options.dryRun ? "close" : "upload", + DEPLOYMENT_PROVIDER: `swa-cli-${packageInfo.version}`, + REPOSITORY_BASE: deployContext, + SKIP_APP_BUILD: "true", + SKIP_API_BUILD: "true", + DEPLOYMENT_TOKEN: deploymentToken, + APP_LOCATION: options.outputLocation, + API_LOCATION: options.apiLocation, + VERBOSE: isVerboseEnabled ? "true" : "false", + }; + + process.env = { ...process.env, ...cliEnv }; + + logger.silly( + { + env: { ...cliEnv, ...deployClientEnv }, + }, + "swa" + ); // TODO: add support for .env file // TODO: add support for Azure CLI @@ -38,35 +98,32 @@ export async function deploy(deployContext: string, options: SWACLIConfig) { // TODO: check that platform.apiRuntime in staticwebapp.config.json is provided. // This is required by the StaticSiteClient! - const spinner = ora({ text: `Preparing deployment...`, prefixText: chalk.dim.gray(`[swa]`) }).start(); + let spinner: ora.Ora = {} as ora.Ora; try { const { binary, version } = await getDeployClientPath(); - spinner.text = `Deploying using ${binary}@${version}`; if (binary) { - const env = { - ...process.env, - DEPLOYMENT_ACTION: "upload", - DEPLOYMENT_PROVIDER: `swa-cli-${pkg.version}`, - REPOSITORY_BASE: deployContext, - SKIP_APP_BUILD: "true", - SKIP_API_BUILD: "true", - DEPLOYMENT_TOKEN: deploymentToken, - APP_LOCATION: options.outputLocation, - API_LOCATION: options.apiLocation, - VERBOSE: options.verbose === "silly" ? "true" : "false", - }; + spinner = ora({ text: `Preparing deployment...`, prefixText: chalk.dim.gray(`[swa]`) }).start(); + spinner.text = `Deploying using ${binary}@${version}`; - let projectUrl = ""; - const child = spawn(binary, [], { env }); + const child = spawn(binary, [], { + env: { + ...process.env, + ...deployClientEnv, + }, + }); + let projectUrl = ""; child.stdout!.on("data", (data) => { data .toString() .trim() .split("\n") .forEach((line: string) => { - if (line.includes("Visit your site at:")) { + 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; } @@ -75,8 +132,8 @@ export async function deploy(deployContext: string, options: SWACLIConfig) { spinner.fail(line); } - if (options.verbose === "silly") { - spinner.succeed(line.trim()); + if (isVerboseEnabled || options.dryRun) { + spinner.info(line.trim()); } else { spinner.text = line.trim(); } @@ -96,7 +153,8 @@ export async function deploy(deployContext: string, options: SWACLIConfig) { }); } } catch (error) { - spinner.stop(); logger.error((error as any).message, true); + } finally { + cleanUp(); } } diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index ded93870..4e9484dc 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -86,9 +86,9 @@ export async function start(startContext: string, options: SWACLIConfig) { userWorkflowConfig, }); } catch (err) { - logger.warn(``, "swa"); - logger.warn(`Error reading workflow configuration:`, "swa"); - logger.warn((err as any).message, "swa"); + 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.`, "swa" diff --git a/src/cli/index.ts b/src/cli/index.ts index 90922dec..d0127437 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,8 +7,16 @@ import { start } from "./commands/start"; import updateNotifier from "update-notifier"; import { getFileOptions, swaCliConfigFilename } from "../core/utils/cli-config"; import { login } from "./commands/login"; +import { deploy } from "./commands/deploy"; +import chalk from "chalk"; const pkg = require("../../package.json"); +const printWelcomeMessage = () => { + console.log(chalk.dim.gray(`[swa]`)); + console.log(chalk.dim.gray(`[swa]`), `Azure Static Web App CLI v${pkg.version}`); + console.log(chalk.dim.gray(`[swa]`)); +}; + const processConfigurationFile = async (cli: SWACLIConfig & GithubActionWorkflow & program.Command, context: string, options: SWACLIConfig) => { const verbose = cli.opts().verbose; @@ -46,7 +54,7 @@ export async function run(argv?: string[]) { updateNotifier({ pkg }).notify(); // don't use logger here: SWA_CLI_DEBUG is not set yet - console.log(`Azure Static Web App CLI v${pkg.version}`); + printWelcomeMessage(); const cli: SWACLIConfig & program.Command = program .name("swa") @@ -131,7 +139,7 @@ export async function run(argv?: string[]) { parseDevserverTimeout, DEFAULT_CONFIG.devserverTimeout ) - .option("--open", "Automatically open the CLI dev server in the default", DEFAULT_CONFIG.open) + .option("--open", "Automatically open the CLI dev server in the default browser", DEFAULT_CONFIG.open) .option("--func-args ", "Pass additional arguments to the func start command") .action(async (context = DEFAULT_CONFIG.outputLocation as string, parsedOptions: SWACLIConfig) => { let { options, fileOptions } = await processConfigurationFile(cli, context, parsedOptions); @@ -169,6 +177,7 @@ Examples: .option("--output-location ", "The folder where the front-end public files are location", DEFAULT_CONFIG.outputLocation) .option("--api-location ", "The folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation) .option("--deployment-token ", "The secret toekn used to authenticate with the Static Web Apps") + .option("--dry-run", "Simulate a deploy process without actually running it", DEFAULT_CONFIG.dryRun) .action(async (context = DEFAULT_CONFIG.outputLocation as string, parsedOptions: SWACLIConfig) => { let { options, fileOptions } = await processConfigurationFile(cli, context, parsedOptions); await deploy(fileOptions.context ?? context, options); @@ -185,6 +194,7 @@ Examples: SWA_CLI_DEPLOYMENT_TOKEN=123 swa deploy --output-location ./app/dist/ --api-location ./api/ Deploy using swa-cli.config.json file + swa deploy --dry-run swa deploy myconfig swa deploy ` diff --git a/src/config.ts b/src/config.ts index f88502a2..6012da76 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,4 +27,5 @@ export const DEFAULT_CONFIG: SWACLIConfig = { resourceGroup: undefined, tenantId: undefined, appName: undefined, + dryRun: false, }; diff --git a/src/core/runtimes.ts b/src/core/runtimes.ts index d0ef3abe..cd8a7153 100644 --- a/src/core/runtimes.ts +++ b/src/core/runtimes.ts @@ -9,21 +9,23 @@ export enum RuntimeType { } export function detectRuntime(appLocation: string | undefined) { + let runtime = RuntimeType.unknown; if (!appLocation || fs.existsSync(appLocation) === false) { logger.info(`The provided app location "${appLocation}" was not found. Can't detect runtime!`); - return RuntimeType.unknown; - } - - const files = fs.readdirSync(appLocation); + runtime = RuntimeType.unknown; + } else { + const files = fs.readdirSync(appLocation); - if (files.some((file) => [".csproj", ".sln"].includes(path.extname(file)))) { - return RuntimeType.dotnet; - } + if (files.some((file) => [".csproj", ".sln"].includes(path.extname(file)))) { + runtime = RuntimeType.dotnet; + } - if (files.includes("package.json")) { - return RuntimeType.node; + if (files.includes("package.json")) { + runtime = RuntimeType.node; + } } - logger.silly(`Detected runtime: ${RuntimeType}`); - return RuntimeType.unknown; + logger.silly(`Detected runtime:`, "swa"); + logger.silly({ runtime }, "swa"); + return runtime; } diff --git a/src/core/utils/cli-config.ts b/src/core/utils/cli-config.ts index 79f00461..77038e78 100644 --- a/src/core/utils/cli-config.ts +++ b/src/core/utils/cli-config.ts @@ -55,5 +55,5 @@ function printConfigMsg(name: string, file: string) { logger.log(`Using configuration "${name}" from file:`, "swa"); logger.log(`\t${file}`, "swa"); logger.log("", "swa"); - logger.log(`Options passed in via CLI will be overridden by options in file.`, "swa"); + logger.warn(`Options passed in via CLI will be overridden by options in file.`, "swa"); } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index f7eb66c5..0ec39763 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -3,6 +3,8 @@ import type http from "http"; import { DEFAULT_CONFIG } from "../../config"; import { SWA_CLI_APP_PROTOCOL } from "../constants"; +const SENSITIVE_KEYS = ["DEPLOYMENT_TOKEN", "SWA_CLI_DEPLOYMENT_TOKEN"]; + export const logger = { _print(prefix: string | null, data: string) { if (prefix) { @@ -39,9 +41,10 @@ export const logger = { * @param data Either a string or an object to be printed. * @param prefix (optional) A prefix to prepend to the printed message. */ - log(data: string | object, prefix: string | null = "swa") { + log(data: string | object, prefix: string | null = null) { this.silly(data, prefix, "log", chalk.reset); }, + /** * Print information data. * @param data Either a string or an object to be printed. @@ -50,6 +53,7 @@ export const logger = { warn(data: string | object, prefix: string | null = null) { this.silly(data, prefix, "log", chalk.yellow); }, + /** * Print error data and optionally exit the CLI instance. * @param data Either a string or an object to be printed. @@ -61,7 +65,7 @@ export const logger = { return; } - console.error(chalk.red(data)); + console.error(chalk.red(`[swa]`), chalk.red(data)); if (exit) { process.exit(-1); } @@ -84,7 +88,12 @@ export const logger = { if (typeof data === "object") { this._traverseObjectProperties(data, (key: string, value: string | null, indent: string) => { if (value !== null) { - value = typeof value === "undefined" ? chalk.yellow("") : value; + if (SENSITIVE_KEYS.includes(key)) { + value = chalk.yellow(""); + } else if (typeof value === "undefined") { + value = chalk.yellow(""); + } + this._print(prefix, color(`${indent}- ${key}: ${chalk.yellow(value)}`)); } else { this._print(prefix, color(`${indent}- ${key}:`)); diff --git a/src/swa.d.ts b/src/swa.d.ts index 3b1b78c4..eb7d7066 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -14,6 +14,7 @@ declare global { SWA_CLI_APP_SSL_CERT: string; SWA_CLI_STARTUP_COMMAND: string; SWA_CLI_DEPLOYMENT_TOKEN: string; + SWA_CLI_DEPLOY_DRY_RUN: string; } } } @@ -85,6 +86,7 @@ declare type SWACLIDeployOptions = { appOutputLocation?: string; apiOutputLocation?: string; deploymentToken?: string; + dryRun?: boolean; }; declare type SWACLIConfig = SWACLIStartOptions & SWACLIDeployOptions & GithubActionWorkflow;