[Dev tool] Add check-node-versions command (#13828)

* [Dev tool] Add checkNodesVers command

* address feedback

* rename keep-docker-image to plural

* rename a couple more flags to plural

* fix
This commit is contained in:
Deyaaeldeen Almahallawi 2021-02-19 18:32:41 -05:00 коммит произвёл GitHub
Родитель 90eab8eee4
Коммит 4a6bb5c649
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 309 добавлений и 1 удалений

Просмотреть файл

@ -20,6 +20,7 @@ It provides a place to centralize scripts, resources, and processes for developm
- `dev` (link samples to local sources for access to IntelliSense during development)
- `prep` (prepare samples for local source-linked execution)
- `run` (execute a sample or all samples within a directory)
- `check-node-versions` (execute samples with different node versions, typically in preparation for release)
The `dev-tool about` command will print some information about how to use the command. All commands additionally accept the `--help` argument, which will print information about the usage of that specific command. For example, to show help information for the `resolve` command above, issue the command `dev-tool package resolve --help`.

Просмотреть файл

@ -0,0 +1,306 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import fs from "fs-extra";
import path from "path";
import pr from "child_process";
import os from "os";
import { createPrinter } from "../../util/printer";
import { leafCommand, makeCommandInfo } from "../../framework/command";
import { S_IRWXO } from "constants";
import { resolveProject } from "../../util/resolveProject";
const log = createPrinter("check-node-versions-samples");
async function spawnCMD(cmd: string, args: string[], errorMessage?: string): Promise<void> {
const spawnedProcess = pr.spawn(cmd, args);
await new Promise((resolve, reject) => {
spawnedProcess.on("exit", resolve);
spawnedProcess.on("error", (err: Error) => {
log.info(errorMessage);
reject(err);
});
});
}
async function deleteDockerContainers(deleteContainerNames?: string[]): Promise<void> {
if (deleteContainerNames) {
log.info(`Cleanup: deleting ${deleteContainerNames.join(", ")} docker containers`);
await spawnCMD(
"docker",
["rm", ...deleteContainerNames, "-f"],
`Attempted to delete ${deleteContainerNames.join(
", "
)} docker containers but encountered an error doing so`
);
}
}
async function deleteDockerImages(dockerImageNames?: string[]) {
if (dockerImageNames) {
log.info(`Cleanup: deleting ${dockerImageNames.join(", ")} docker images`);
await spawnCMD(
"docker",
["rmi", ...dockerImageNames, "-f"],
`Attempted to delete the ${dockerImageNames.join(
", "
)} docker images but encountered an error doing so`
);
}
}
async function deleteDockerContext(dockerContextDirectory?: string) {
if (dockerContextDirectory) {
log.info(`Cleanup: deleting the ${dockerContextDirectory} docker context directory`);
await spawnCMD("rm", ["-rf", dockerContextDirectory], undefined);
}
}
async function cleanup(
dockerContextDirectory?: string,
dockerContainerNames?: string[],
dockerImageNames?: string[]
) {
await deleteDockerContext(dockerContextDirectory);
await deleteDockerContainers(dockerContainerNames);
await deleteDockerImages(dockerImageNames);
}
function buildRunSamplesScript(
containerWorkspacePath: string,
artifactName: string,
envFileName: string,
logFilePath?: string
) {
function compileCMD(cmd: string, printToScreen?: boolean) {
return printToScreen ? cmd : `${cmd} >> ${logFilePath} 2>&1`;
}
const printToScreen = logFilePath === undefined;
const artifactPath = `${containerWorkspacePath}/${artifactName}`;
const envFilePath = `${containerWorkspacePath}/${envFileName}`;
const javascriptSamplesPath = `${containerWorkspacePath}/samples/javascript`;
const typescriptCompiledSamplesPath = `${containerWorkspacePath}/samples/typescript/dist`;
const scriptContent = `#!/bin/sh
function install_dependencies_helper() {
local samples_path=\$1;
cd \${samples_path};
${compileCMD(`npm install ${artifactPath}`, printToScreen)}
${compileCMD(`npm install`, printToScreen)}
}
function install_packages() {
echo "Using node \$(node -v) to install dependencies";
install_dependencies_helper ${containerWorkspacePath}/samples/javascript
install_dependencies_helper ${containerWorkspacePath}/samples/typescript;
cp ${envFilePath} ${containerWorkspacePath}/samples/javascript/;
}
function run_samples() {
samples_path=\$1;
echo "Using node \$(node -v) to run samples in \${samples_path}";
cd "\${samples_path}";
for SAMPLE in *.js; do
node \${SAMPLE};
done
}
function build_typescript() {
echo "Using node \$(node -v) to build the typescript samples";
cd ${containerWorkspacePath}/samples/typescript
${compileCMD(`npm run build`, printToScreen)}
cp ${envFilePath} ${containerWorkspacePath}/samples/typescript/dist/
}
function main() {
install_packages;
run_samples "${javascriptSamplesPath}";
build_typescript && run_samples "${typescriptCompiledSamplesPath}";
}
main`;
return scriptContent;
}
function createDockerContextDirectory(
dockerContextDirectory: string,
containerWorkspacePath: string,
samples_path: string,
envPath: string,
artifactPath?: string,
logFilePath?: string
): void {
if (artifactPath === undefined) {
throw new Error("artifact_path is a required argument but it was not passed");
} else if (!fs.existsSync(artifactPath)) {
throw new Error(`artifact path passed does not exist: ${artifactPath}`);
}
const artifactName = path.basename(artifactPath);
const envFileName = path.basename(envPath);
fs.copySync(samples_path, path.join(dockerContextDirectory, "samples"));
fs.copyFileSync(artifactPath, path.join(dockerContextDirectory, artifactName));
fs.copyFileSync(envPath, path.join(dockerContextDirectory, envFileName));
fs.writeFileSync(
path.join(dockerContextDirectory, "run_samples.sh"),
buildRunSamplesScript(containerWorkspacePath, artifactName, envFileName, logFilePath),
{ mode: S_IRWXO }
);
}
async function runDockerContainer(
dockerContextDirectory: string,
dockerImageName: string,
dockerContainerName: string,
containerWorkspace: string,
stdoutListener: (chunk: string | Buffer) => void,
stderrListener: (chunk: string | Buffer) => void
): Promise<void> {
const args = [
"run",
"--name",
dockerContainerName,
"--workdir",
containerWorkspace,
"-v",
`${dockerContextDirectory}:${containerWorkspace}`,
dockerImageName,
"./run_samples.sh"
];
const dockerContainerRunProcess = pr.spawn("docker", args, {
cwd: dockerContextDirectory
});
log.info(`Started running the docker container ${dockerContainerName}`);
dockerContainerRunProcess.stdout.on("data", stdoutListener);
dockerContainerRunProcess.stderr.on("data", stderrListener);
const exitCode = await new Promise((resolve, reject) => {
dockerContainerRunProcess.on("exit", resolve);
dockerContainerRunProcess.on("error", reject);
});
if (exitCode === 0) {
log.info(`Docker container ${dockerContainerName} finished running`);
} else {
log.error(`Docker container ${dockerContainerName} encountered an error`);
}
}
export const commandInfo = makeCommandInfo(
"check-node-versions",
"execute samples with different node versions, typically in preparation for release",
{
"artifact-path": {
kind: "string",
description: "Path to the downloaded artifact built by the release pipeline"
},
directory: {
kind: "string",
description: "Base dir, default is process.cwd()",
default: process.cwd()
},
"node-versions": {
kind: "string",
description: "A comma separated list of node versions to use",
default: "8,10,12"
},
"context-directory-path": {
kind: "string",
description: "Absolute path to a directory used for mounting inside docker containers",
default: ""
},
"keep-docker-context": {
kind: "boolean",
description: "Boolean to indicate whether to keep the current docker context directory",
default: false
},
"log-in-file": {
kind: "boolean",
description:
"Boolean to indicate whether to save the the stdout and sterr for npm commands to the log.txt log file",
default: true
},
"use-existing-docker-containers": {
kind: "boolean",
description: "Boolean to indicate whether to use existing docker containers if any",
default: false
},
"keep-docker-containers": {
kind: "boolean",
description: "Boolean to indicate whether to keep docker containers",
default: false
},
"keep-docker-images": {
kind: "boolean",
description: "Boolean to indicate whether to keep the downloaded docker images",
default: false
}
}
);
export default leafCommand(commandInfo, async (options) => {
const nodeVersions = options["node-versions"]?.split(",");
const dockerContextDirectory: string =
options["context-directory-path"] === ""
? await fs.mkdtemp(path.join(os.tmpdir(), "context"))
: options["context-directory-path"];
const pkg = await resolveProject(options.directory);
const samplesPath = path.join(pkg.path, "samples");
const envFilePath = path.join(pkg.path, ".env");
const keepDockerContextDirectory = options["keep-docker-context"];
const dockerImageNames = nodeVersions.map((version: string) => `node:${version}-alpine`);
const dockerContainerNames = nodeVersions.map((version: string) => `${version}-container`);
const containerWorkspace = "/workspace";
const containerLogFilePath = options["log-in-file"] ? `${containerWorkspace}/log.txt` : undefined;
const useExistingDockerContainer = options["use-existing-docker-containers"];
const keepDockerContainers = options["keep-docker-containers"];
const keepDockerImages = options["keep-docker-images"];
const stdoutListener = (chunk: Buffer | string) => log.info(chunk.toString());
const stderrListener = (chunk: Buffer | string) => log.error(chunk.toString());
async function cleanupBefore(): Promise<void> {
const dockerContextDirectoryChildren = await fs.readdir(dockerContextDirectory);
await cleanup(
// If the directory is empty, we will not delete it.
dockerContextDirectoryChildren.length === 0 ? undefined : dockerContextDirectory,
useExistingDockerContainer ? undefined : dockerContainerNames,
// Do not delete the image
undefined
);
}
async function cleanupAfter(): Promise<void> {
await cleanup(
keepDockerContextDirectory ? undefined : dockerContextDirectory,
keepDockerContainers ? undefined : dockerContainerNames,
keepDockerImages ? undefined : dockerImageNames
);
}
function createDockerContextDirectoryThunk(): void {
createDockerContextDirectory(
dockerContextDirectory,
containerWorkspace,
samplesPath,
envFilePath,
options["artifact-path"],
containerLogFilePath
);
}
async function runContainers(): Promise<void> {
const containerRuns = dockerImageNames.map((imageName, containerIndex) => () =>
runDockerContainer(
dockerContextDirectory,
imageName,
dockerContainerNames[containerIndex],
containerWorkspace,
stdoutListener,
stderrListener
)
);
for (const run of containerRuns) {
await run();
}
}
await cleanupBefore();
createDockerContextDirectoryThunk();
await runContainers();
await cleanupAfter();
return true;
});

Просмотреть файл

@ -9,5 +9,6 @@ export default subCommand(commandInfo, {
dev: () => import("./dev"),
prep: () => import("./prep"),
run: () => import("./run"),
"ts-to-js": () => import("./tsToJs")
"ts-to-js": () => import("./tsToJs"),
"check-node-versions": () => import("./checkNodeVersions")
});