209 lines
5.8 KiB
TypeScript
209 lines
5.8 KiB
TypeScript
|
|
/**
|
||
|
|
* Subprocess helper - utilities for running protocol clients in tests
|
||
|
|
*/
|
||
|
|
|
||
|
|
export interface ICommandResult {
|
||
|
|
success: boolean;
|
||
|
|
stdout: string;
|
||
|
|
stderr: string;
|
||
|
|
code: number;
|
||
|
|
signal?: Deno.Signal;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ICommandOptions {
|
||
|
|
cwd?: string;
|
||
|
|
env?: Record<string, string>;
|
||
|
|
timeout?: number;
|
||
|
|
stdin?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Execute a command and return the result
|
||
|
|
*/
|
||
|
|
export async function runCommand(
|
||
|
|
cmd: string[],
|
||
|
|
options: ICommandOptions = {}
|
||
|
|
): Promise<ICommandResult> {
|
||
|
|
const { cwd, env, timeout = 60000, stdin } = options;
|
||
|
|
|
||
|
|
const command = new Deno.Command(cmd[0], {
|
||
|
|
args: cmd.slice(1),
|
||
|
|
cwd,
|
||
|
|
env: { ...Deno.env.toObject(), ...env },
|
||
|
|
stdin: stdin ? 'piped' : 'null',
|
||
|
|
stdout: 'piped',
|
||
|
|
stderr: 'piped',
|
||
|
|
});
|
||
|
|
|
||
|
|
const child = command.spawn();
|
||
|
|
|
||
|
|
if (stdin && child.stdin) {
|
||
|
|
const writer = child.stdin.getWriter();
|
||
|
|
await writer.write(new TextEncoder().encode(stdin));
|
||
|
|
await writer.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
const timeoutId = setTimeout(() => {
|
||
|
|
try {
|
||
|
|
child.kill('SIGTERM');
|
||
|
|
} catch {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
}, timeout);
|
||
|
|
|
||
|
|
const output = await child.output();
|
||
|
|
clearTimeout(timeoutId);
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: output.success,
|
||
|
|
stdout: new TextDecoder().decode(output.stdout),
|
||
|
|
stderr: new TextDecoder().decode(output.stderr),
|
||
|
|
code: output.code,
|
||
|
|
signal: output.signal ?? undefined,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a command is available
|
||
|
|
*/
|
||
|
|
export async function commandExists(cmd: string): Promise<boolean> {
|
||
|
|
try {
|
||
|
|
const result = await runCommand(['which', cmd], { timeout: 5000 });
|
||
|
|
return result.success;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Protocol client wrappers
|
||
|
|
*/
|
||
|
|
export const clients = {
|
||
|
|
npm: {
|
||
|
|
check: () => commandExists('npm'),
|
||
|
|
publish: (dir: string, registry: string, token: string) =>
|
||
|
|
runCommand(['npm', 'publish', '--registry', registry], {
|
||
|
|
cwd: dir,
|
||
|
|
env: { NPM_TOKEN: token, npm_config__authToken: token },
|
||
|
|
}),
|
||
|
|
install: (pkg: string, registry: string, dir: string) =>
|
||
|
|
runCommand(['npm', 'install', pkg, '--registry', registry], { cwd: dir }),
|
||
|
|
unpublish: (pkg: string, registry: string, token: string) =>
|
||
|
|
runCommand(['npm', 'unpublish', pkg, '--registry', registry, '--force'], {
|
||
|
|
env: { NPM_TOKEN: token, npm_config__authToken: token },
|
||
|
|
}),
|
||
|
|
pack: (dir: string) => runCommand(['npm', 'pack'], { cwd: dir }),
|
||
|
|
},
|
||
|
|
|
||
|
|
docker: {
|
||
|
|
check: () => commandExists('docker'),
|
||
|
|
build: (dockerfile: string, tag: string, context: string) =>
|
||
|
|
runCommand(['docker', 'build', '-f', dockerfile, '-t', tag, context]),
|
||
|
|
push: (image: string) => runCommand(['docker', 'push', image]),
|
||
|
|
pull: (image: string) => runCommand(['docker', 'pull', image]),
|
||
|
|
rmi: (image: string, force = false) =>
|
||
|
|
runCommand(['docker', 'rmi', ...(force ? ['-f'] : []), image]),
|
||
|
|
login: (registry: string, username: string, password: string) =>
|
||
|
|
runCommand(['docker', 'login', registry, '-u', username, '--password-stdin'], {
|
||
|
|
stdin: password,
|
||
|
|
}),
|
||
|
|
tag: (source: string, target: string) => runCommand(['docker', 'tag', source, target]),
|
||
|
|
},
|
||
|
|
|
||
|
|
cargo: {
|
||
|
|
check: () => commandExists('cargo'),
|
||
|
|
package: (dir: string) => runCommand(['cargo', 'package', '--allow-dirty'], { cwd: dir }),
|
||
|
|
publish: (dir: string, registry: string, token: string) =>
|
||
|
|
runCommand(
|
||
|
|
['cargo', 'publish', '--registry', 'stack-test', '--token', token, '--allow-dirty'],
|
||
|
|
{ cwd: dir }
|
||
|
|
),
|
||
|
|
yank: (crate: string, version: string, token: string) =>
|
||
|
|
runCommand([
|
||
|
|
'cargo',
|
||
|
|
'yank',
|
||
|
|
crate,
|
||
|
|
'--version',
|
||
|
|
version,
|
||
|
|
'--registry',
|
||
|
|
'stack-test',
|
||
|
|
'--token',
|
||
|
|
token,
|
||
|
|
]),
|
||
|
|
},
|
||
|
|
|
||
|
|
pip: {
|
||
|
|
check: () => commandExists('pip'),
|
||
|
|
build: (dir: string) => runCommand(['python', '-m', 'build', dir]),
|
||
|
|
upload: (dist: string, repository: string, token: string) =>
|
||
|
|
runCommand([
|
||
|
|
'python',
|
||
|
|
'-m',
|
||
|
|
'twine',
|
||
|
|
'upload',
|
||
|
|
'--repository-url',
|
||
|
|
repository,
|
||
|
|
'-u',
|
||
|
|
'__token__',
|
||
|
|
'-p',
|
||
|
|
token,
|
||
|
|
`${dist}/*`,
|
||
|
|
]),
|
||
|
|
install: (pkg: string, indexUrl: string) =>
|
||
|
|
runCommand(['pip', 'install', pkg, '--index-url', indexUrl]),
|
||
|
|
},
|
||
|
|
|
||
|
|
composer: {
|
||
|
|
check: () => commandExists('composer'),
|
||
|
|
install: (pkg: string, repository: string, dir: string) =>
|
||
|
|
runCommand(
|
||
|
|
[
|
||
|
|
'composer',
|
||
|
|
'require',
|
||
|
|
pkg,
|
||
|
|
'--repository',
|
||
|
|
JSON.stringify({ type: 'composer', url: repository }),
|
||
|
|
],
|
||
|
|
{ cwd: dir }
|
||
|
|
),
|
||
|
|
},
|
||
|
|
|
||
|
|
gem: {
|
||
|
|
check: () => commandExists('gem'),
|
||
|
|
build: (gemspec: string, dir: string) => runCommand(['gem', 'build', gemspec], { cwd: dir }),
|
||
|
|
push: (gemFile: string, host: string, key: string) =>
|
||
|
|
runCommand(['gem', 'push', gemFile, '--host', host, '--key', key]),
|
||
|
|
install: (gemName: string, source: string) =>
|
||
|
|
runCommand(['gem', 'install', gemName, '--source', source]),
|
||
|
|
yank: (gemName: string, version: string, host: string, key: string) =>
|
||
|
|
runCommand(['gem', 'yank', gemName, '-v', version, '--host', host, '--key', key]),
|
||
|
|
},
|
||
|
|
|
||
|
|
maven: {
|
||
|
|
check: () => commandExists('mvn'),
|
||
|
|
deploy: (dir: string, repositoryUrl: string, username: string, password: string) =>
|
||
|
|
runCommand(
|
||
|
|
[
|
||
|
|
'mvn',
|
||
|
|
'deploy',
|
||
|
|
`-DaltDeploymentRepository=stack-test::default::${repositoryUrl}`,
|
||
|
|
`-Dusername=${username}`,
|
||
|
|
`-Dpassword=${password}`,
|
||
|
|
],
|
||
|
|
{ cwd: dir }
|
||
|
|
),
|
||
|
|
package: (dir: string) => runCommand(['mvn', 'package', '-DskipTests'], { cwd: dir }),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Skip test if command is not available
|
||
|
|
*/
|
||
|
|
export async function skipIfMissing(cmd: string): Promise<boolean> {
|
||
|
|
const exists = await commandExists(cmd);
|
||
|
|
if (!exists) {
|
||
|
|
console.warn(`[Skip] ${cmd} not available`);
|
||
|
|
}
|
||
|
|
return !exists;
|
||
|
|
}
|