Address review feedback and regenerate API reports

- Handle quoted arguments in createScriptOptionsParser using a proper
  shell-like splitter instead of naive whitespace splitting
- Pass CommandContext directly in bump tests instead of using a helper
- Regenerate cli-report.md

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-28 11:08:57 +01:00
parent ff4a45ac01
commit eb73feac15
4 changed files with 248 additions and 96 deletions
@@ -15,6 +15,40 @@
*/
import { parseArgs, type ParseArgsConfig } from 'node:util';
// Splits a shell-like argument string, respecting single and double quotes
function splitShellArgs(str: string): string[] {
const args: string[] = [];
let current = '';
let quote: string | undefined;
for (let i = 0; i < str.length; i++) {
const ch = str[i];
if (quote) {
if (ch === quote) {
quote = undefined;
} else {
current += ch;
}
} else if (ch === '"' || ch === "'") {
quote = ch;
} else if (/\s/.test(ch)) {
if (current) {
args.push(current);
current = '';
}
} else {
current += ch;
}
}
if (current) {
args.push(current);
}
return args;
}
export function createScriptOptionsParser(
commandPath: string[],
options: ParseArgsConfig['options'],
@@ -27,7 +61,7 @@ export function createScriptOptionsParser(
}
const argsStr = scriptStr.slice(expectedScript.length).trim();
const args = argsStr ? argsStr.split(/\s+/) : [];
const args = argsStr ? splitShellArgs(argsStr) : [];
const { values } = parseArgs({ args, strict: false, options });
return values;
@@ -15,6 +15,40 @@
*/
import { parseArgs, type ParseArgsConfig } from 'node:util';
// Splits a shell-like argument string, respecting single and double quotes
function splitShellArgs(str: string): string[] {
const args: string[] = [];
let current = '';
let quote: string | undefined;
for (let i = 0; i < str.length; i++) {
const ch = str[i];
if (quote) {
if (ch === quote) {
quote = undefined;
} else {
current += ch;
}
} else if (ch === '"' || ch === "'") {
quote = ch;
} else if (/\s/.test(ch)) {
if (current) {
args.push(current);
current = '';
}
} else {
current += ch;
}
}
if (current) {
args.push(current);
}
return args;
}
export function createScriptOptionsParser(
commandPath: string[],
options: ParseArgsConfig['options'],
@@ -27,7 +61,7 @@ export function createScriptOptionsParser(
}
const argsStr = scriptStr.slice(expectedScript.length).trim();
const args = argsStr ? argsStr.split(/\s+/) : [];
const args = argsStr ? splitShellArgs(argsStr) : [];
const { values } = parseArgs({ args, strict: false, options });
return values;
@@ -17,7 +17,6 @@ import fs from 'fs-extra';
import * as runObj from '@backstage/cli-common';
import { overrideTargetPaths } from '@backstage/cli-common/testUtils';
import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump';
import type { CommandContext } from '../../../../wiring/types';
import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils';
import { YarnInfoInspectData } from '../../lib/versioning/packages';
import { setupServer } from 'msw/node';
@@ -126,27 +125,7 @@ const expectLogsToMatch = (
expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort());
};
function callBump(flags: Record<string, string | boolean | null>) {
const args: string[] = [];
for (const [key, value] of Object.entries(flags)) {
if (value === null || value === undefined) {
continue;
}
const flag = `--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`;
if (typeof value === 'boolean') {
if (value) {
args.push(flag);
}
} else {
args.push(flag, value);
}
}
const context: CommandContext = {
args,
info: { usage: 'backstage-cli versions:bump', description: 'test' },
};
return bump(context);
}
const info = { usage: 'backstage-cli versions:bump', description: '' };
describe('bump', () => {
const mockDir = createMockDirectory();
@@ -212,7 +191,7 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
'Using default pattern glob @backstage/*',
@@ -305,7 +284,7 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main', skipInstall: true });
await bump({ args: ['--release', 'main', '--skip-install'], info });
});
expectLogsToMatch(logs, [
'Using default pattern glob @backstage/*',
@@ -407,7 +386,7 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
'Using default pattern glob @backstage/*',
@@ -511,7 +490,7 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
'Using default pattern glob @backstage/*',
@@ -606,9 +585,9 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await expect(callBump({ release: '999.0.1' })).rejects.toThrow(
'No release found for 999.0.1 version',
);
await expect(
bump({ args: ['--release', '999.0.1'], info }),
).rejects.toThrow('No release found for 999.0.1 version');
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
@@ -712,7 +691,7 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'next' });
await bump({ args: ['--release', 'next'], info });
});
expectLogsToMatch(logs, [
'Using default pattern glob @backstage/*',
@@ -791,9 +770,14 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({
pattern: '@{backstage,backstage-extra}/*',
release: 'main',
await bump({
args: [
'--pattern',
'@{backstage,backstage-extra}/*',
'--release',
'main',
],
info,
});
});
expectLogsToMatch(logs, [
@@ -900,7 +884,7 @@ describe('bump', () => {
),
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
'Using default pattern glob @backstage/*',
@@ -1139,7 +1123,7 @@ describe('environment variables', () => {
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
@@ -1213,7 +1197,7 @@ describe('environment variables', () => {
} as any);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
@@ -1298,7 +1282,7 @@ describe('environment variables', () => {
);
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
await callBump({ release: 'main' });
await bump({ args: ['--release', 'main'], info });
});
expectLogsToMatch(logs, [
@@ -1347,7 +1331,7 @@ describe('environment variables', () => {
},
});
await expect(callBump({ release: 'main' })).rejects.toThrow();
await expect(bump({ args: ['--release', 'main'], info })).rejects.toThrow();
});
it('should handle network errors when using custom base URL', async () => {
@@ -1375,6 +1359,6 @@ describe('environment variables', () => {
),
);
await expect(callBump({ release: 'main' })).rejects.toThrow();
await expect(bump({ args: ['--release', 'main'], info })).rejects.toThrow();
});
});