Skip to content

Commit

Permalink
feat: add --dry-run mode
Browse files Browse the repository at this point in the history
  • Loading branch information
manekinekko committed Apr 4, 2022
1 parent d94fe81 commit 2000ef9
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 53 deletions.
11 changes: 6 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions schema/swa-cli.config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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": {
Expand Down
114 changes: 86 additions & 28 deletions src/cli/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,52 +29,101 @@ 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: "<hidden>" });
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<GithubActionWorkflow> | 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
// TODO: add support for Service Principal
// 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;
}

Expand All @@ -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();
}
Expand All @@ -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();
}
}
6 changes: 3 additions & 3 deletions src/cli/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 12 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 <funcArgs>", "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);
Expand Down Expand Up @@ -169,6 +177,7 @@ Examples:
.option("--output-location <outputLocation>", "The folder where the front-end public files are location", DEFAULT_CONFIG.outputLocation)
.option("--api-location <apiLocation>", "The folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation)
.option("--deployment-token <secret>", "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);
Expand All @@ -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
`
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export const DEFAULT_CONFIG: SWACLIConfig = {
resourceGroup: undefined,
tenantId: undefined,
appName: undefined,
dryRun: false,
};
24 changes: 13 additions & 11 deletions src/core/runtimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/core/utils/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
15 changes: 12 additions & 3 deletions src/core/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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);
}
Expand All @@ -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("<undefined>") : value;
if (SENSITIVE_KEYS.includes(key)) {
value = chalk.yellow("<hidden>");
} else if (typeof value === "undefined") {
value = chalk.yellow("<undefined>");
}

this._print(prefix, color(`${indent}- ${key}: ${chalk.yellow(value)}`));
} else {
this._print(prefix, color(`${indent}- ${key}:`));
Expand Down
2 changes: 2 additions & 0 deletions src/swa.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -85,6 +86,7 @@ declare type SWACLIDeployOptions = {
appOutputLocation?: string;
apiOutputLocation?: string;
deploymentToken?: string;
dryRun?: boolean;
};

declare type SWACLIConfig = SWACLIStartOptions & SWACLIDeployOptions & GithubActionWorkflow;
Expand Down

0 comments on commit 2000ef9

Please sign in to comment.