Skip to content

Commit 3170abf

Browse files
committed
feat(cli): add reporter for migrations
See: WDX-190
1 parent e2c9984 commit 3170abf

File tree

12 files changed

+367
-37
lines changed

12 files changed

+367
-37
lines changed

packages/cli/src/commands/logs/list/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import path from 'node:path';
44
// Import the main components module first to ensure proper initialization
55
import '../index';
66
import { logsCommand } from '../command';
7-
import { getLogsPath } from '../../../utils/filesystem';
7+
import { resolveCommandPath } from '../../../utils/filesystem';
88

99
vi.mock('node:fs');
1010
vi.mock('node:fs/promises');
1111

1212
vi.spyOn(console, 'info');
1313
vi.spyOn(console, 'log');
1414

15-
const LOGS_FILE_DIR = getLogsPath('logs', '12345');
15+
const LOGS_FILE_DIR = resolveCommandPath('logs', '12345');
1616

1717
const preconditions = {
1818
hasLogFiles() {

packages/cli/src/commands/logs/list/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getLogsPath } from '../../../utils/filesystem';
1+
import { resolveCommandPath } from '../../../utils/filesystem';
22
import { getUI } from '../../../utils/ui';
33
import { logsCommand } from '../command';
44
import { FileTransport } from '../../../utils/logger-transport-file';
@@ -9,7 +9,7 @@ logsCommand.command('list')
99
.action(async () => {
1010
const { space, path } = logsCommand.opts();
1111
const ui = getUI();
12-
const logsPath = getLogsPath(directories.log, space, path);
12+
const logsPath = resolveCommandPath(directories.log, space, path);
1313
const logFiles = FileTransport.listLogFiles(logsPath);
1414

1515
if (logFiles.length === 0) {

packages/cli/src/commands/logs/prune/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import path from 'node:path';
44
// Import the main components module first to ensure proper initialization
55
import '../index';
66
import { logsCommand } from '../command';
7-
import { getLogsPath } from '../../../utils/filesystem';
7+
import { resolveCommandPath } from '../../../utils/filesystem';
88

99
vi.mock('node:fs');
1010
vi.mock('node:fs/promises');
1111

1212
vi.spyOn(console, 'info');
1313
vi.spyOn(console, 'log');
1414

15-
const LOGS_FILE_DIR = getLogsPath('logs', '12345');
15+
const LOGS_FILE_DIR = resolveCommandPath('logs', '12345');
1616

1717
const preconditions = {
1818
hasLogFiles() {

packages/cli/src/commands/logs/prune/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getLogsPath } from '../../../utils/filesystem';
1+
import { resolveCommandPath } from '../../../utils/filesystem';
22
import { getUI } from '../../../utils/ui';
33
import { logsCommand } from '../command';
44
import { FileTransport } from '../../../utils/logger-transport-file';
@@ -10,7 +10,7 @@ logsCommand.command('prune')
1010
.action(async ({ keep }: { keep: number }) => {
1111
const { space, path } = logsCommand.opts();
1212
const ui = getUI();
13-
const logsPath = getLogsPath(directories.log, space, path);
13+
const logsPath = resolveCommandPath(directories.log, space, path);
1414
const deletedFilesCount = FileTransport.pruneLogFiles(logsPath, keep);
1515

1616
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? '' : 's'}`);

packages/cli/src/commands/migrations/run/index.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,37 @@ describe('migrations run command', () => {
147147
}),
148148
);
149149
expect(fetchStory).toHaveBeenCalledWith('12345', '517473243');
150+
// Report
151+
const reportFile = Object.entries(vol.toJSON())
152+
.find(([filename]) => filename.includes('reports/12345/storyblok-migrations-run-'))?.[1];
153+
expect(JSON.parse(reportFile || '{}')).toEqual({
154+
status: 'SUCCESS',
155+
meta: {
156+
runId: expect.any(String),
157+
command: 'storyblok migrations run',
158+
cliVersion: expect.any(String),
159+
startedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
160+
endedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
161+
durationMs: expect.any(Number),
162+
logPath: expect.any(String),
163+
config: {
164+
space: '12345',
165+
},
166+
},
167+
summary: {
168+
migrationResults: {
169+
failed: 0,
170+
skipped: 0,
171+
succeeded: 1,
172+
total: 1,
173+
},
174+
updateResults: {
175+
failed: 0,
176+
succeeded: 1,
177+
total: 1,
178+
},
179+
},
180+
});
150181
// Logging
151182
const logFile = getLogFileContents();
152183
expect(logFile).toContain('Migration finished');
@@ -167,6 +198,26 @@ describe('migrations run command', () => {
167198
await migrationsCommand.parseAsync(['node', 'test', 'run', '--space', '12345']);
168199

169200
expect(updateStory).not.toHaveBeenCalled();
201+
// Report
202+
const reportFile = Object.entries(vol.toJSON())
203+
.find(([filename]) => filename.includes('reports/12345/storyblok-migrations-run-'))?.[1];
204+
expect(JSON.parse(reportFile || '{}')).toEqual({
205+
status: 'FAILURE',
206+
meta: expect.any(Object),
207+
summary: {
208+
migrationResults: {
209+
failed: 1,
210+
skipped: 0,
211+
succeeded: 0,
212+
total: 1,
213+
},
214+
updateResults: {
215+
failed: 0,
216+
succeeded: 0,
217+
total: 0,
218+
},
219+
},
220+
});
170221
// Logging
171222
const logFile = getLogFileContents();
172223
expect(logFile).toContain('Couldn\'t load migration function');

packages/cli/src/commands/migrations/run/index.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getProgram } from '../../../program';
22
import { getUI } from '../../../utils/ui';
33
import { getLogger } from '../../../utils/logger';
4+
import { getReporter } from '../../../utils/reporter';
45
import { colorPalette, commands } from '../../../constants';
56
import { CommandError, handleError, requireAuthentication } from '../../../utils';
67
import { session } from '../../../session';
@@ -24,6 +25,7 @@ migrationsCommand.command('run [componentName]')
2425
const program = getProgram();
2526
const ui = getUI();
2627
const logger = getLogger();
28+
const reporter = getReporter();
2729

2830
ui.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : 'Running migrations...');
2931
logger.info('Migration started');
@@ -150,22 +152,27 @@ migrationsCommand.command('run [componentName]')
150152
const updateSummary = updateStream.getSummary();
151153
ui.info(updateSummary);
152154

153-
const migrationResults = migrationStream.getResults();
154-
const updateResults = updateStream.getResults();
155+
const migrationStreamResults = migrationStream.getResults();
156+
const migrationResults = {
157+
total: migrationStreamResults.totalProcessed,
158+
succeeded: migrationStreamResults.successful.length,
159+
skipped: migrationStreamResults.skipped.length,
160+
failed: migrationStreamResults.failed.length,
161+
};
162+
const updateStreamResults = updateStream.getResults();
163+
const updateResults = {
164+
total: updateStreamResults.totalProcessed,
165+
succeeded: updateStreamResults.successful.length,
166+
failed: updateStreamResults.failed.length,
167+
};
155168

156169
logger.info('Migration finished', {
157-
migrationResults: {
158-
total: migrationResults.totalProcessed,
159-
succeeded: migrationResults.successful.length,
160-
skipped: migrationResults.skipped.length,
161-
failed: migrationResults.failed.length,
162-
},
163-
updateResults: {
164-
total: updateResults.totalProcessed,
165-
succeeded: updateResults.successful.length,
166-
failed: updateResults.failed.length,
167-
},
170+
migrationResults,
171+
updateResults,
168172
});
173+
reporter.addSummary('migrationResults', migrationResults);
174+
reporter.addSummary('updateResults', updateResults);
175+
reporter.finalize();
169176
}
170177
catch (error) {
171178
handleError(error as Error, verbose);

packages/cli/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,5 @@ export interface SpaceOptions {
8888

8989
export const directories = {
9090
log: 'logs',
91+
report: 'reports',
9192
} as const;

packages/cli/src/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import './commands/types';
1414
import './commands/datasources';
1515
import './commands/create';
1616
import './commands/logs';
17-
import pkg from '../package.json';
1817

1918
import { colorPalette } from './constants';
2019

@@ -27,10 +26,6 @@ konsola.br();
2726
konsola.br();
2827
konsola.title(` Storyblok CLI `, colorPalette.PRIMARY);
2928

30-
program.option('--verbose', 'Enable verbose output');
31-
program.version(pkg.version, '-v, --vers', 'Output the current version');
32-
program.helpOption('-h, --help', 'Display help for command');
33-
3429
program.on('command:*', () => {
3530
console.error(`Invalid command: ${program.args.join(' ')}`);
3631
konsola.br();

packages/cli/src/program.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { __dirname, handleError } from './utils';
66
import type { LogTransport } from './utils/logger';
77
import { getLogger } from './utils/logger';
88
import { getUI } from './utils/ui';
9+
import { getReporter } from './utils/reporter';
910
import { FileTransport } from './utils/logger-transport-file';
10-
import { getLogsPath } from './utils/filesystem';
11+
import { resolveCommandPath } from './utils/filesystem';
1112
import { directories } from './constants';
1213

1314
let packageJson: NormalizedPackageJson;
@@ -44,7 +45,9 @@ export function getProgram(): Command {
4445
programInstance
4546
.name(packageJson.name)
4647
.description(packageJson.description || '')
47-
.version(packageJson.version)
48+
.version(packageJson.version, '-v, --vers', 'Output the current version')
49+
.helpOption('-h, --help', 'Display help for command')
50+
.option('--verbose', 'Enable verbose output')
4851
.hook('preAction', (_, actionCmd) => {
4952
const options = actionCmd.optsWithGlobals();
5053
const commandPieces: string[] = [];
@@ -55,7 +58,7 @@ export function getProgram(): Command {
5558

5659
const runId = Date.now();
5760
const transports: LogTransport[] = [];
58-
const logsPath = getLogsPath(directories.log, options.space, options.path);
61+
const logsPath = resolveCommandPath(directories.log, options.space, options.path);
5962
const logFilename = `${commandPieces.join('-')}-${runId}.jsonl`;
6063
const filePath = path.join(logsPath, logFilename);
6164
transports.push(new FileTransport({
@@ -68,6 +71,16 @@ export function getProgram(): Command {
6871
});
6972

7073
getUI({ enabled: true });
74+
75+
const reportPath = resolveCommandPath(directories.report, options.space, options.path);
76+
const reportFilename = `${commandPieces.join('-')}-${runId}.jsonl`;
77+
const reportFilePath = path.join(reportPath, reportFilename);
78+
getReporter({ enabled: true, filePath: reportFilePath })
79+
.addMeta('command', command)
80+
.addMeta('cliVersion', packageJson.version)
81+
.addMeta('runId', String(runId))
82+
.addMeta('logPath', filePath)
83+
.addMeta('config', options);
7184
});
7285

7386
// Global error handling

packages/cli/src/utils/filesystem.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { appendFile, mkdir, readFile as readFileImpl, writeFile } from 'node:fs/
33
import { handleFileSystemError } from './error/filesystem-error';
44
import type { FileReaderResult } from '../types';
55
import filenamify from 'filenamify';
6-
import { appendFileSync, mkdirSync } from 'node:fs';
6+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
77

88
export interface FileOptions {
99
mode?: number;
@@ -39,6 +39,28 @@ export const saveToFile = async (filePath: string, data: string, options?: FileO
3939
}
4040
};
4141

42+
export const saveToFileSync = (filePath: string, data: string, options?: FileOptions) => {
43+
const resolvedPath = parse(filePath).dir;
44+
45+
// Only attempt to create a directory if there's a directory part
46+
if (resolvedPath) {
47+
try {
48+
mkdirSync(resolvedPath, { recursive: true });
49+
}
50+
catch (mkdirError) {
51+
handleFileSystemError('mkdir', mkdirError as Error);
52+
return; // Exit early if the directory creation fails
53+
}
54+
}
55+
56+
try {
57+
writeFileSync(filePath, data, options as any);
58+
}
59+
catch (writeError) {
60+
handleFileSystemError('write', writeError as Error);
61+
}
62+
};
63+
4264
export const appendToFile = async (filePath: string, data: string, options?: FileOptions) => {
4365
const resolvedPath = parse(filePath).dir;
4466

@@ -99,6 +121,23 @@ export const resolvePath = (path: string | undefined, folder: string) => {
99121
return resolve(resolve(process.cwd(), '.storyblok'), folder);
100122
};
101123

124+
/**
125+
* Resolves the absolute path for a specific command directory.
126+
*
127+
* If a `space` is provided, it is appended to the `commandPath` before resolution.
128+
*
129+
* @param commandPath - The relative path or name of the command category.
130+
* @param space - (Optional).
131+
* @param baseDir - (Optional) The base directory to resolve against.
132+
* @returns The fully resolved absolute path string.
133+
*/
134+
export function resolveCommandPath(commandPath: string, space?: string, baseDir?: string) {
135+
if (space) {
136+
return resolvePath(baseDir, join(commandPath, space));
137+
}
138+
return resolvePath(baseDir, commandPath);
139+
}
140+
102141
/**
103142
* Extracts the component name from a migration filename
104143
* @param filename - The migration filename (e.g., "simple_component.js")
@@ -138,10 +177,3 @@ export async function readJsonFile<T>(filePath: string): Promise<FileReaderResul
138177
export function importModule(filePath: string) {
139178
return import(`file://${filePath}`);
140179
}
141-
142-
export function getLogsPath(logFileDir: string, space?: string, baseDir?: string) {
143-
if (space) {
144-
return resolvePath(baseDir, join(logFileDir, space));
145-
}
146-
return resolvePath(baseDir, logFileDir);
147-
}

0 commit comments

Comments
 (0)