cli: update to use new run utils from cli-common

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-11-29 15:53:03 +01:00
parent 4e8c7261e9
commit 7fbac5cfac
17 changed files with 178 additions and 287 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Updated to use new utilities from `@backstage/cli-common`.
-121
View File
@@ -1,121 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
SpawnOptions,
spawn,
ChildProcess,
execFile as execFileCb,
} from 'child_process';
import { ExitCodeError } from './errors';
import { promisify } from 'util';
import { assertError, ForwardedError } from '@backstage/errors';
export const execFile = promisify(execFileCb);
type LogFunc = (data: Buffer) => void;
type SpawnOptionsPartialEnv = Omit<SpawnOptions, 'env'> & {
env?: Partial<NodeJS.ProcessEnv>;
// Pipe stdout to this log function
stdoutLogFunc?: LogFunc;
// Pipe stderr to this log function
stderrLogFunc?: LogFunc;
};
// Runs a child command, returning a promise that is only resolved if the child exits with code 0.
export async function run(
name: string,
args: string[] = [],
options: SpawnOptionsPartialEnv = {},
) {
const { stdoutLogFunc, stderrLogFunc } = options;
const env: NodeJS.ProcessEnv = {
...process.env,
FORCE_COLOR: 'true',
...(options.env ?? {}),
};
const stdio = [
'inherit',
stdoutLogFunc ? 'pipe' : 'inherit',
stderrLogFunc ? 'pipe' : 'inherit',
] as ('inherit' | 'pipe')[];
const child = spawn(name, args, {
stdio,
shell: true,
...options,
env,
});
if (stdoutLogFunc && child.stdout) {
child.stdout.on('data', stdoutLogFunc);
}
if (stderrLogFunc && child.stderr) {
child.stderr.on('data', stderrLogFunc);
}
await waitForExit(child, name);
}
export async function runPlain(cmd: string, ...args: string[]) {
try {
const { stdout } = await execFile(cmd, args, { shell: true });
return stdout.trim();
} catch (error) {
assertError(error);
if ('stderr' in error) {
process.stderr.write(error.stderr as Buffer);
}
if (typeof error.code === 'number') {
throw new ExitCodeError(error.code, [cmd, ...args].join(' '));
}
throw new ForwardedError('Unknown execution error', error);
}
}
export async function runCheck(cmd: string, ...args: string[]) {
try {
await execFile(cmd, args, { shell: true });
return true;
} catch (error) {
return false;
}
}
export async function waitForExit(
child: ChildProcess & { exitCode: number | null },
name?: string,
): Promise<void> {
if (typeof child.exitCode === 'number') {
if (child.exitCode) {
throw new ExitCodeError(child.exitCode, name);
}
return;
}
await new Promise<void>((resolve, reject) => {
child.once('error', error => reject(error));
child.once('exit', code => {
if (code) {
reject(new ExitCodeError(code, name));
} else {
resolve();
}
});
});
}
@@ -14,16 +14,17 @@
* limitations under the License.
*/
import * as runObj from '../run';
import * as runObj from '@backstage/cli-common';
import * as yarn from './yarn';
import { fetchPackageInfo, mapDependencies } from './packages';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { NotFoundError } from '@backstage/errors';
jest.mock('../run', () => {
jest.mock('@backstage/cli-common', () => {
const actual = jest.requireActual('@backstage/cli-common');
return {
run: jest.fn(),
execFile: jest.fn(),
...actual,
runOutput: jest.fn(),
};
});
@@ -39,42 +40,40 @@ describe('fetchPackageInfo', () => {
});
it('should forward info for yarn classic', async () => {
jest.spyOn(runObj, 'execFile').mockResolvedValue({
stdout: `{"type":"inspect","data":{"the":"data"}}`,
stderr: '',
});
jest
.spyOn(runObj, 'runOutput')
.mockResolvedValue(`{"type":"inspect","data":{"the":"data"}}`);
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.execFile).toHaveBeenCalledWith(
expect(runObj.runOutput).toHaveBeenCalledWith([
'yarn',
['info', '--json', 'my-package'],
{ shell: true },
);
'info',
'--json',
'my-package',
]);
});
it('should forward info for yarn berry', async () => {
jest
.spyOn(runObj, 'execFile')
.mockResolvedValue({ stdout: `{"the":"data"}`, stderr: '' });
jest.spyOn(runObj, 'runOutput').mockResolvedValue(`{"the":"data"}`);
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
await expect(fetchPackageInfo('my-package')).resolves.toEqual({
the: 'data',
});
expect(runObj.execFile).toHaveBeenCalledWith(
expect(runObj.runOutput).toHaveBeenCalledWith([
'yarn',
['npm', 'info', '--json', 'my-package'],
{ shell: true },
);
'npm',
'info',
'--json',
'my-package',
]);
});
it('should throw if no info with yarn classic', async () => {
jest
.spyOn(runObj, 'execFile')
.mockResolvedValue({ stdout: '', stderr: '' });
jest.spyOn(runObj, 'runOutput').mockResolvedValue('');
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('classic');
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
@@ -83,9 +82,10 @@ describe('fetchPackageInfo', () => {
});
it('should throw if no info with yarn berry', async () => {
jest
.spyOn(runObj, 'execFile')
.mockRejectedValue({ stdout: 'bla bla bla Response Code: 404 bla bla' });
const error = new Error('Command failed');
(error as Error & { stdout?: string }).stdout =
'bla bla bla Response Code: 404 bla bla';
jest.spyOn(runObj, 'runOutput').mockRejectedValue(error);
jest.spyOn(yarn, 'detectYarnVersion').mockResolvedValue('berry');
await expect(fetchPackageInfo('my-package')).rejects.toThrow(
+8 -7
View File
@@ -17,7 +17,7 @@
import { minimatch } from 'minimatch';
import { getPackages } from '@manypkg/get-packages';
import { detectYarnVersion } from './yarn';
import { execFile } from '../run';
import { runOutput } from '@backstage/cli-common';
import { NotFoundError } from '@backstage/errors';
const DEP_TYPES = [
@@ -54,11 +54,7 @@ export async function fetchPackageInfo(
const cmd = yarnVersion === 'classic' ? ['info'] : ['npm', 'info'];
try {
const { stdout: output } = await execFile(
'yarn',
[...cmd, '--json', name],
{ shell: true },
);
const output = await runOutput(['yarn', ...cmd, '--json', name]);
if (!output) {
throw new NotFoundError(
@@ -81,7 +77,12 @@ export async function fetchPackageInfo(
throw error;
}
if (error?.stdout.includes('Response Code: 404')) {
if (
error instanceof Error &&
'stdout' in error &&
typeof error.stdout === 'string' &&
error.stdout.includes('Response Code: 404')
) {
throw new NotFoundError(
`No package information found for package ${name}`,
);
+2 -9
View File
@@ -15,10 +15,7 @@
*/
import { assertError, ForwardedError } from '@backstage/errors';
import { execFile as execFileCb } from 'child_process';
import { promisify } from 'util';
const execFile = promisify(execFileCb);
import { runOutput } from '@backstage/cli-common';
const versions = new Map<string, Promise<'classic' | 'berry'>>();
@@ -30,16 +27,12 @@ export function detectYarnVersion(dir?: string): Promise<'classic' | 'berry'> {
const promise = Promise.resolve().then(async () => {
try {
const { stdout } = await execFile('yarn', ['--version'], {
shell: true,
const stdout = await runOutput(['yarn', '--version'], {
cwd,
});
return stdout.trim().startsWith('1.') ? 'classic' : 'berry';
} catch (error) {
assertError(error);
if ('stderr' in error) {
process.stderr.write(error.stderr as Buffer);
}
throw new ForwardedError('Failed to determine yarn version', error);
}
});
@@ -29,7 +29,7 @@ import { paths as cliPaths } from '../../../../lib/paths';
import fs from 'fs-extra';
import { optimization as optimizationConfig } from './optimization';
import pickBy from 'lodash/pickBy';
import { runPlain } from '../../../../lib/run';
import { runOutput } from '@backstage/cli-common';
import { transforms } from './transforms';
import { version } from '../../../../lib/version';
import yn from 'yn';
@@ -78,14 +78,14 @@ async function readBuildInfo() {
let commit: string | undefined;
try {
commit = await runPlain('git', 'rev-parse', 'HEAD');
commit = await runOutput(['git', 'rev-parse', 'HEAD']);
} catch (error) {
// ignore, see below
}
let gitVersion: string | undefined;
try {
gitVersion = await runPlain('git', 'describe', '--always');
gitVersion = await runOutput(['git', 'describe', '--always']);
} catch (error) {
// ignore, see below
}
@@ -25,7 +25,7 @@ import { tmpdir } from 'os';
import tar, { CreateOptions, FileOptions } from 'tar';
import partition from 'lodash/partition';
import { paths } from '../../../../lib/paths';
import { run } from '../../../../lib/run';
import { run } from '@backstage/cli-common';
import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
@@ -228,11 +228,11 @@ export async function createDistWorkspace(
await runParallelWorkers({
items: customBuild,
worker: async ({ name, dir, args }) => {
await run('yarn', ['run', 'build', ...(args || [])], {
await run(['yarn', 'run', 'build', ...(args || [])], {
cwd: dir,
stdoutLogFunc: prefixLogFunc(`${name}: `, 'stdout'),
stderrLogFunc: prefixLogFunc(`${name}: `, 'stderr'),
});
}).waitForExit();
},
});
}
@@ -321,9 +321,9 @@ async function moveToDistWorkspace(
console.log(`Repacking ${target.name} into dist workspace`);
const archivePath = resolvePath(workspaceDir, archive);
await run('yarn', ['pack', '--filename', archivePath], {
await run(['yarn', 'pack', '--filename', archivePath], {
cwd: target.dir,
});
}).waitForExit();
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
@@ -16,14 +16,14 @@
import { version as cliVersion } from '../../../../package.json';
import os from 'os';
import { runPlain } from '../../../lib/run';
import { runOutput } from '@backstage/cli-common';
import { paths } from '../../../lib/paths';
import { Lockfile } from '../../../lib/versioning';
import fs from 'fs-extra';
export default async () => {
await new Promise(async () => {
const yarnVersion = await runPlain('yarn --version');
const yarnVersion = await runOutput(['yarn', '--version']);
const isLocal = fs.existsSync(paths.resolveOwn('./src'));
const backstageFile = paths.resolveTargetRoot('backstage.json');
@@ -14,14 +14,11 @@
* limitations under the License.
*/
import { execFile as execFileCb } from 'child_process';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { promisify } from 'util';
import { PackageGraph } from '@backstage/cli-node';
import { paths } from '../../../../lib/paths';
const execFile = promisify(execFileCb);
import { run } from '@backstage/cli-common';
export async function command(): Promise<void> {
const packages = await PackageGraph.listTargetPackages();
@@ -44,12 +41,9 @@ export async function command(): Promise<void> {
await fs.remove(resolvePath(pkg.dir, 'dist-types'));
await fs.remove(resolvePath(pkg.dir, 'coverage'));
} else if (cleanScript) {
const result = await execFile('yarn', ['run', 'clean'], {
await run(['yarn', 'run', 'clean'], {
cwd: pkg.dir,
shell: true,
});
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
}).waitForExit();
}
}
}),
@@ -17,7 +17,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { PackageGraph } from '@backstage/cli-node';
import { runPlain } from '../../../lib/run';
import { runOutput } from '@backstage/cli-common';
const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`;
@@ -84,6 +84,6 @@ export async function command() {
}
if (hasPrettier) {
await runPlain('prettier', '--write', ...configPaths);
await runOutput(['prettier', '--write', ...configPaths]);
}
}
@@ -15,7 +15,7 @@
*/
import fs from 'fs-extra';
import { Command } from 'commander';
import * as runObj from '../../../../lib/run';
import * as runObj from '@backstage/cli-common';
import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump';
import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils';
import { YarnInfoInspectData } from '../../../../lib/versioning/packages';
@@ -60,21 +60,22 @@ jest.mock('ora', () => ({
}));
let mockDir: MockDirectory;
jest.mock('@backstage/cli-common', () => ({
...jest.requireActual('@backstage/cli-common'),
findPaths: () => ({
resolveTargetRoot(filename: string) {
return mockDir.resolve(filename);
},
get targetDir() {
return mockDir.path;
},
}),
}));
jest.mock('../../../../lib/run', () => {
jest.mock('@backstage/cli-common', () => {
const actual = jest.requireActual('@backstage/cli-common');
return {
run: jest.fn(),
...actual,
findPaths: () => ({
resolveTargetRoot(filename: string) {
return mockDir.resolve(filename);
},
get targetDir() {
return mockDir.path;
},
}),
run: jest.fn().mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
}),
};
});
@@ -184,7 +185,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -222,8 +226,7 @@ describe('bump', () => {
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -277,7 +280,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -318,8 +324,7 @@ describe('bump', () => {
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme');
expect(runObj.run).not.toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -373,7 +378,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -421,8 +429,7 @@ describe('bump', () => {
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -477,7 +484,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -526,14 +536,14 @@ describe('bump', () => {
expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core');
expect(runObj.run).toHaveBeenCalledTimes(2);
expect(runObj.run).toHaveBeenCalledWith('yarn', [
expect(runObj.run).toHaveBeenCalledWith([
'yarn',
'plugin',
'import',
'https://versions.backstage.io/v1/releases/0.0.1/yarn-plugin',
]);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -587,7 +597,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/releases/999.0.1/manifest.json',
@@ -656,7 +669,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -763,7 +779,10 @@ describe('bump', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -813,8 +832,7 @@ describe('bump', () => {
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -873,7 +891,10 @@ describe('bump', () => {
});
mockFetchPackageInfo.mockRejectedValue(new NotFoundError('Nope'));
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://versions.backstage.io/v1/tags/main/manifest.json',
@@ -1094,7 +1115,10 @@ describe('environment variables', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://custom.example.com/v1/tags/main/manifest.json',
@@ -1131,8 +1155,7 @@ describe('environment variables', () => {
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -1186,7 +1209,10 @@ describe('environment variables', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await bump({ pattern: null, release: 'main' } as unknown as Command);
@@ -1215,8 +1241,7 @@ describe('environment variables', () => {
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
@@ -1253,7 +1278,10 @@ describe('environment variables', () => {
},
});
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
jest.spyOn(runObj, 'run').mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
} as any);
worker.use(
rest.get(
'https://custom.example.com/v1/tags/main/manifest.json',
@@ -1291,14 +1319,14 @@ describe('environment variables', () => {
]);
expect(runObj.run).toHaveBeenCalledTimes(2);
expect(runObj.run).toHaveBeenCalledWith('yarn', [
expect(runObj.run).toHaveBeenCalledWith([
'yarn',
'plugin',
'import',
'https://custom.example.com/v1/releases/1.5.0/yarn-plugin',
]);
expect(runObj.run).toHaveBeenCalledWith(
'yarn',
['install'],
['yarn', 'install'],
expect.any(Object),
);
});
@@ -41,7 +41,7 @@ import {
} from '@backstage/release-manifests';
import { migrateMovedPackages } from './migrate';
import { runYarnInstall } from '../../lib/utils';
import { run } from '../../../../lib/run';
import { run } from '@backstage/cli-common';
const DEP_TYPES = [
'dependencies',
@@ -135,7 +135,7 @@ export default async (opts: OptionValues) => {
? `${env.BACKSTAGE_VERSIONS_BASE_URL}/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`
: `https://versions.backstage.io/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`;
await run('yarn', ['plugin', 'import', yarnPluginUrl]);
await run(['yarn', 'plugin', 'import', yarnPluginUrl]).waitForExit();
console.log();
}
@@ -17,7 +17,7 @@ import {
MockDirectory,
createMockDirectory,
} from '@backstage/backend-test-utils';
import * as run from '../../../../lib/run';
import * as runObj from '@backstage/cli-common';
import migrate from './migrate';
import { withLogCollector } from '@backstage/test-utils';
import fs from 'fs-extra';
@@ -33,21 +33,22 @@ jest.mock('chalk', () => ({
}));
let mockDir: MockDirectory;
jest.mock('@backstage/cli-common', () => ({
...jest.requireActual('@backstage/cli-common'),
findPaths: () => ({
resolveTargetRoot(filename: string) {
return mockDir.resolve(filename);
},
get targetDir() {
return mockDir.path;
},
}),
}));
jest.mock('../../../../lib/run', () => {
jest.mock('@backstage/cli-common', () => {
const actual = jest.requireActual('@backstage/cli-common');
return {
run: jest.fn(),
...actual,
findPaths: () => ({
resolveTargetRoot(filename: string) {
return mockDir.resolve(filename);
},
get targetDir() {
return mockDir.path;
},
}),
run: jest.fn().mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
}),
};
});
@@ -58,8 +59,15 @@ function expectLogsToMatch(receivedLogs: String[], expected: String[]): void {
describe('versions:migrate', () => {
mockDir = createMockDirectory();
beforeEach(() => {
(runObj.run as jest.Mock).mockReturnValue({
exitCode: null,
waitForExit: jest.fn().mockResolvedValue(undefined),
});
});
afterEach(() => {
jest.resetAllMocks();
(runObj.run as jest.Mock).mockClear();
});
it('should bump to the moved version when the package is moved', async () => {
@@ -116,8 +124,6 @@ describe('versions:migrate', () => {
},
});
jest.spyOn(run, 'run').mockResolvedValue(undefined);
const { warn, log: logs } = await withLogCollector(async () => {
await migrate({});
});
@@ -136,10 +142,9 @@ describe('versions:migrate', () => {
'Could not find package.json for @backstage/theme@^1.0.0 in b (dependencies)',
]);
expect(run.run).toHaveBeenCalledTimes(1);
expect(run.run).toHaveBeenCalledWith(
'yarn',
['install'],
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
['yarn', 'install'],
expect.any(Object),
);
@@ -227,16 +232,13 @@ describe('versions:migrate', () => {
},
});
jest.spyOn(run, 'run').mockResolvedValue(undefined);
await withLogCollector(async () => {
await migrate({});
});
expect(run.run).toHaveBeenCalledTimes(1);
expect(run.run).toHaveBeenCalledWith(
'yarn',
['install'],
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
['yarn', 'install'],
expect.any(Object),
);
@@ -259,7 +261,7 @@ describe('versions:migrate', () => {
);
});
it('should replaces the occurrences of changed packages, and is careful', async () => {
it('should replace occurrences of changed packages, and is careful', async () => {
mockDir.setContent({
'package.json': JSON.stringify({
workspaces: {
@@ -314,16 +316,13 @@ describe('versions:migrate', () => {
},
});
jest.spyOn(run, 'run').mockResolvedValue(undefined);
await withLogCollector(async () => {
await migrate({});
});
expect(run.run).toHaveBeenCalledTimes(1);
expect(run.run).toHaveBeenCalledWith(
'yarn',
['install'],
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith(
['yarn', 'install'],
expect.any(Object),
);
@@ -16,7 +16,7 @@
import ora from 'ora';
import chalk from 'chalk';
import { run } from '../../../lib/run';
import { run } from '@backstage/cli-common';
export async function runYarnInstall() {
const spinner = ora({
@@ -27,7 +27,7 @@ export async function runYarnInstall() {
const installOutput = new Array<Buffer>();
try {
await run('yarn', ['install'], {
await run(['yarn', 'install'], {
env: {
FORCE_COLOR: 'true',
// We filter out all of the npm_* environment variables that are added when
@@ -41,7 +41,7 @@ export async function runYarnInstall() {
},
stdoutLogFunc: data => installOutput.push(data),
stderrLogFunc: data => installOutput.push(data),
});
}).waitForExit();
spinner.succeed();
} catch (error) {
spinner.fail();
+3 -11
View File
@@ -16,11 +16,8 @@
import chalk from 'chalk';
import ora from 'ora';
import { promisify } from 'util';
import { exec as execCb } from 'child_process';
import { assertError } from '@backstage/errors';
const exec = promisify(execCb);
import { run } from '@backstage/cli-common';
const TASK_NAME_MAX_LENGTH = 14;
@@ -71,16 +68,11 @@ export class Task {
) {
try {
await Task.forItem('executing', command, async () => {
await exec(command, { cwd: options?.cwd });
const parts = command.trim().split(/\s+/);
await run(parts, { cwd: options?.cwd }).waitForExit();
});
} catch (error) {
assertError(error);
if (error.stderr) {
process.stderr.write(error.stderr as Buffer);
}
if (error.stdout) {
process.stdout.write(error.stdout as Buffer);
}
if (options?.optional) {
Task.error(`Warning: Failed to execute command ${chalk.cyan(command)}`);
} else {
@@ -16,7 +16,7 @@
import { Command, OptionValues } from 'commander';
import { paths } from '../../../../lib/paths';
import { runCheck } from '../../../../lib/run';
import { runCheck } from '@backstage/cli-common';
function includesAnyOf(hayStack: string[], ...needles: string[]) {
for (const needle of needles) {
@@ -55,8 +55,8 @@ export default async (_opts: OptionValues, cmd: Command) => {
!includesAnyOf(args, '--watch', '--watchAll')
) {
const isGitRepo = () =>
runCheck('git', 'rev-parse', '--is-inside-work-tree');
const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root');
runCheck(['git', 'rev-parse', '--is-inside-work-tree']);
const isMercurialRepo = () => runCheck(['hg', '--cwd', '.', 'root']);
if ((await isGitRepo()) || (await isMercurialRepo())) {
args.push('--watch');
@@ -22,7 +22,7 @@ import { relative as relativePath } from 'path';
import { Command, OptionValues } from 'commander';
import { Lockfile, PackageGraph } from '@backstage/cli-node';
import { paths } from '../../../../lib/paths';
import { runCheck, runPlain } from '../../../../lib/run';
import { runCheck, runOutput } from '@backstage/cli-common';
import { isChildPath } from '@backstage/cli-common';
import { SuccessCache } from '../../../../lib/cache/SuccessCache';
@@ -63,14 +63,14 @@ async function readPackageTreeHashes(graph: PackageGraph) {
...pkg,
path: relativePath(paths.targetRoot, pkg.dir),
}));
const output = await runPlain(
const output = await runOutput([
'git',
'ls-tree',
'--format="%(objectname)=%(path)"',
'--format=%(objectname)=%(path)',
'HEAD',
'--',
...pkgs.map(pkg => pkg.path),
);
]);
const map = new Map(
output
@@ -175,8 +175,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
!hasFlags('--coverage', '--watch', '--watchAll')
) {
const isGitRepo = () =>
runCheck('git', 'rev-parse', '--is-inside-work-tree');
const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root');
runCheck(['git', 'rev-parse', '--is-inside-work-tree']);
const isMercurialRepo = () => runCheck(['hg', '--cwd', '.', 'root']);
if ((await isGitRepo()) || (await isMercurialRepo())) {
isSingleWatchMode = true;