Skip to content

Commit a0500cc

Browse files
committed
ci: Add a new option for cleaner CI/build output
1 parent e6e758b commit a0500cc

File tree

5 files changed

+342
-20
lines changed

5 files changed

+342
-20
lines changed

js/__tests__/helper.test.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,179 @@ describe('SentryCli helper', () => {
171171
]);
172172
});
173173
});
174+
175+
describe('silentLogs functionality', () => {
176+
let consoleInfoSpy;
177+
let consoleErrorSpy;
178+
179+
beforeEach(() => {
180+
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
181+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
182+
});
183+
184+
afterEach(() => {
185+
consoleInfoSpy.mockRestore();
186+
consoleErrorSpy.mockRestore();
187+
});
188+
189+
test('determineSuccessMessage returns correct message for releases new', () => {
190+
const args = ['releases', 'new', 'v1.0.0'];
191+
const message = helper.determineSuccessMessage(args);
192+
expect(message).toBe('✓ Release v1.0.0 created');
193+
});
194+
195+
test('determineSuccessMessage returns correct message for sourcemaps upload', () => {
196+
const args = ['sourcemaps', 'upload'];
197+
const message = helper.determineSuccessMessage(args);
198+
expect(message).toBe('✓ Source maps uploaded');
199+
});
200+
201+
test('determineSuccessMessage returns null for version check', () => {
202+
const args = ['--version'];
203+
const message = helper.determineSuccessMessage(args);
204+
expect(message).toBe(null);
205+
});
206+
207+
test('determineSuccessMessage handles empty args', () => {
208+
const message = helper.determineSuccessMessage([]);
209+
expect(message).toBe(null);
210+
});
211+
212+
test('determineSuccessMessage handles null args', () => {
213+
const message = helper.determineSuccessMessage(null);
214+
expect(message).toBe(null);
215+
});
216+
217+
test('execute with silent=true takes precedence over silentLogs=true', async () => {
218+
const result = await helper.execute(['--version'], false, true, true);
219+
220+
expect(result).toBe('');
221+
expect(consoleInfoSpy).not.toHaveBeenCalled();
222+
});
223+
});
224+
225+
describe('command type coverage', () => {
226+
test('determineSuccessMessage handles supported high-impact commands', () => {
227+
const testCases = [
228+
{ args: ['releases', 'new', 'v1.0.0'], expected: '✓ Release v1.0.0 created' },
229+
{ args: ['releases', 'finalize', 'v1.0.0'], expected: '✓ Release v1.0.0 finalized' },
230+
{
231+
args: ['releases', 'files', 'v1.0.0', 'upload-sourcemaps'],
232+
expected: '✓ Source maps uploaded to release v1.0.0',
233+
},
234+
{ args: ['sourcemaps', 'upload'], expected: '✓ Source maps uploaded' },
235+
{ args: ['sourcemaps', 'inject'], expected: '✓ Source maps injected' },
236+
{ args: ['debug-files', 'upload'], expected: '✓ Debug files uploaded' },
237+
{ args: ['upload-proguard'], expected: '✓ ProGuard mappings uploaded' },
238+
{ args: ['upload-dif'], expected: '✓ Debug information files uploaded' },
239+
{ args: ['upload-dsym'], expected: '✓ dSYM files uploaded' },
240+
{ args: ['deploys', 'new'], expected: '✓ Deploy created' },
241+
{ args: ['mobile-app', 'upload'], expected: '✓ Mobile app uploaded' },
242+
{ args: ['send-event'], expected: '✓ Event sent' },
243+
{ args: ['send-envelope'], expected: '✓ Envelope sent' },
244+
{ args: ['send-metric'], expected: '✓ Metric sent' },
245+
];
246+
247+
testCases.forEach(({ args, expected }) => {
248+
const message = helper.determineSuccessMessage(args);
249+
expect(message).toBe(expected);
250+
});
251+
});
252+
253+
test('determineSuccessMessage returns null for info/list operations and utility commands', () => {
254+
const testCases = [
255+
['--help'],
256+
['--version'],
257+
['info'],
258+
['login'],
259+
['organizations', 'list'],
260+
['projects', 'list'],
261+
['issues', 'list'],
262+
['events', 'list'],
263+
['files', 'list'],
264+
['deploys', 'list'],
265+
['monitors', 'list'],
266+
['releases', 'list'],
267+
['releases', 'delete', 'v1.0.0'],
268+
['unknown-command'],
269+
];
270+
271+
testCases.forEach((args) => {
272+
const message = helper.determineSuccessMessage(args);
273+
expect(message).toBe(null);
274+
});
275+
});
276+
});
277+
278+
describe('Promise resolution scenarios', () => {
279+
let consoleInfoSpy;
280+
281+
beforeEach(() => {
282+
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
283+
});
284+
285+
afterEach(() => {
286+
consoleInfoSpy.mockRestore();
287+
});
288+
289+
test('execute with live=true, silentLogs=true uses stdio piping and resolves with undefined', async () => {
290+
const result = await helper.execute(['--version'], true, false, true);
291+
292+
expect(result).toBeUndefined();
293+
expect(consoleInfoSpy).not.toHaveBeenCalled(); // --version doesn't have success message
294+
});
295+
296+
test('execute with live=true, silentLogs=true shows success message for supported commands', async () => {
297+
const result = await helper.execute(['sourcemaps', 'upload'], true, false, true);
298+
299+
expect(result).toBeUndefined();
300+
expect(consoleInfoSpy).toHaveBeenCalledWith('✓ Source maps uploaded');
301+
});
302+
303+
test('execute with live=false, silentLogs=true uses callback mode and resolves with empty string', async () => {
304+
const result = await helper.execute(['--version'], false, false, true);
305+
306+
expect(result).toBe('');
307+
expect(consoleInfoSpy).not.toHaveBeenCalled();
308+
});
309+
310+
test('execute with live=false, silentLogs=true shows success message and returns empty string', async () => {
311+
const result = await helper.execute(['sourcemaps', 'upload'], false, false, true);
312+
313+
expect(result).toBe('');
314+
expect(consoleInfoSpy).toHaveBeenCalledWith('✓ Source maps uploaded');
315+
});
316+
317+
test('execute with normal mode (live=false, silent=false, silentLogs=false) returns actual stdout', async () => {
318+
const result = await helper.execute(['--version'], false, false, false);
319+
320+
expect(result.trim()).toBe('sentry-cli DEV');
321+
expect(consoleInfoSpy).not.toHaveBeenCalled();
322+
});
323+
324+
test('execute with silent=true suppresses output and messages', async () => {
325+
// Test with live=true - uses stdio piping, resolves with undefined
326+
const result1 = await helper.execute(['--version'], true, true, false);
327+
expect(result1).toBeUndefined();
328+
329+
// Test with live=false - uses callback mode, resolves with empty string
330+
const result2 = await helper.execute(['--version'], false, true, false);
331+
expect(result2).toBe('');
332+
333+
// Test with silentLogs=true (should be ignored when silent=true)
334+
const result3 = await helper.execute(['sourcemaps', 'upload'], false, true, true);
335+
expect(result3).toBe('');
336+
337+
// No success messages should be shown when silent=true
338+
expect(consoleInfoSpy).not.toHaveBeenCalled();
339+
});
340+
341+
test('execute with live=true and normal mode uses stdio inherit and resolves with undefined', async () => {
342+
const result = await helper.execute(['--version'], true, false, false);
343+
344+
// Should resolve with undefined (stdio inherit mode)
345+
expect(result).toBeUndefined();
346+
expect(consoleInfoSpy).not.toHaveBeenCalled();
347+
});
348+
});
174349
});

js/helper.js

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,17 @@ function getPath() {
282282
* @param {string[]} args Command line arguments passed to `sentry-cli`.
283283
* @param {boolean} live We inherit stdio to display `sentry-cli` output directly.
284284
* @param {boolean} silent Disable stdout for silents build (CI/Webpack Stats, ...)
285+
* @param {boolean} silentLogs Show only errors and success messages, hide verbose logs
285286
* @param {string} [configFile] Relative or absolute path to the configuration file.
286287
* @param {Object} [config] More configuration to pass to the CLI
287288
* @returns {Promise.<string>} A promise that resolves to the standard output.
288289
*/
289-
async function execute(args, live, silent, configFile, config = {}) {
290+
async function execute(args, live, silent, silentLogs, configFile, config = {}) {
291+
// Ensure silent takes precedence over silentLogs
292+
if (silent) {
293+
silentLogs = false;
294+
}
295+
290296
const env = { ...process.env };
291297
if (configFile) {
292298
env.SENTRY_PROPERTIES = configFile;
@@ -323,21 +329,58 @@ async function execute(args, live, silent, configFile, config = {}) {
323329
}
324330
return new Promise((resolve, reject) => {
325331
if (live === true) {
326-
const output = silent ? 'ignore' : 'inherit';
332+
let stdoutMode, stderrMode;
333+
334+
if (silent) {
335+
// Complete silence
336+
stdoutMode = 'ignore';
337+
stderrMode = 'ignore';
338+
} else if (silentLogs) {
339+
// Capture stdout to filter it, but show stderr for errors
340+
stdoutMode = 'pipe';
341+
stderrMode = 'inherit';
342+
} else {
343+
// Show everything
344+
stdoutMode = 'inherit';
345+
stderrMode = 'inherit';
346+
}
347+
327348
const pid = childProcess.spawn(getPath(), args, {
328349
env,
329350
// stdin, stdout, stderr
330-
stdio: ['ignore', output, output],
331-
});
332-
pid.on('exit', () => {
333-
resolve();
351+
stdio: ['ignore', stdoutMode, stderrMode],
334352
});
353+
354+
if (silentLogs) {
355+
pid.on('exit', (code) => {
356+
if (code === 0) {
357+
const successMessage = determineSuccessMessage(args);
358+
if (successMessage) {
359+
console.info(successMessage);
360+
}
361+
}
362+
// Note: errors are already shown via stderr inherit
363+
resolve();
364+
});
365+
} else {
366+
pid.on('exit', () => {
367+
resolve();
368+
});
369+
}
335370
} else {
336371
childProcess.execFile(getPath(), args, { env }, (err, stdout) => {
337372
if (err) {
338373
reject(err);
339374
} else {
340-
resolve(stdout);
375+
if (silentLogs) {
376+
const successMessage = determineSuccessMessage(args);
377+
if (successMessage) {
378+
console.info(successMessage);
379+
}
380+
resolve('');
381+
} else {
382+
resolve(silent ? '' : stdout);
383+
}
341384
}
342385
});
343386
}
@@ -348,6 +391,84 @@ function getProjectFlagsFromOptions({ projects = [] } = {}) {
348391
return projects.reduce((flags, project) => flags.concat('-p', project), []);
349392
}
350393

394+
/**
395+
* Determines an appropriate success message based on the command.
396+
*
397+
* @param {string[]} args Command line arguments passed to sentry-cli
398+
* @returns {string|null} Success message to display or null if no message needed
399+
*/
400+
function determineSuccessMessage(args) {
401+
if (!args || args.length === 0) {
402+
return null;
403+
}
404+
405+
const command = args[0];
406+
407+
// Only show success messages for high-impact operations customers care about
408+
switch (command) {
409+
case 'releases':
410+
if (args[1] === 'new' && args[2]) {
411+
return `✓ Release ${args[2]} created`;
412+
} else if (args[1] === 'finalize' && args[2]) {
413+
return `✓ Release ${args[2]} finalized`;
414+
} else if (args[1] === 'files' && args[3] === 'upload-sourcemaps' && args[2]) {
415+
return `✓ Source maps uploaded to release ${args[2]}`;
416+
}
417+
break;
418+
419+
case 'sourcemaps':
420+
if (args[1] === 'upload') {
421+
return `✓ Source maps uploaded`;
422+
} else if (args[1] === 'inject') {
423+
return `✓ Source maps injected`;
424+
}
425+
break;
426+
427+
case 'debug-files':
428+
if (args[1] === 'upload') {
429+
return `✓ Debug files uploaded`;
430+
}
431+
break;
432+
433+
case 'upload-proguard':
434+
return `✓ ProGuard mappings uploaded`;
435+
436+
case 'upload-dif':
437+
return `✓ Debug information files uploaded`;
438+
439+
case 'upload-dsym':
440+
return `✓ dSYM files uploaded`;
441+
442+
case 'deploys':
443+
if (args[1] === 'new') {
444+
return `✓ Deploy created`;
445+
}
446+
break;
447+
448+
case 'mobile-app':
449+
if (args[1] === 'upload') {
450+
return `✓ Mobile app uploaded`;
451+
}
452+
break;
453+
454+
case 'send-event':
455+
return `✓ Event sent`;
456+
457+
case 'send-envelope':
458+
return `✓ Envelope sent`;
459+
460+
case 'send-metric':
461+
return `✓ Metric sent`;
462+
463+
// Don't show success messages for info/list operations - they show their own output
464+
// Don't show for --version, --help - the output is the success
465+
default:
466+
return null;
467+
}
468+
469+
return null;
470+
}
471+
351472
module.exports = {
352473
execute,
353474
getPath,
@@ -358,4 +479,5 @@ module.exports = {
358479
getDistributionForThisPlatform,
359480
throwUnsupportedPlatformError,
360481
getFallbackBinaryPath,
482+
determineSuccessMessage,
361483
};

js/index.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ class SentryCli {
3434
if (typeof configFile === 'string') {
3535
this.configFile = configFile;
3636
}
37-
this.options = options || { silent: false };
37+
this.options = {
38+
silent: false,
39+
silentLogs: false,
40+
...options,
41+
};
3842
this.releases = new Releases({ ...this.options, configFile });
3943
}
4044

@@ -61,7 +65,14 @@ class SentryCli {
6165
* @returns {Promise.<string>} A promise that resolves to the standard output.
6266
*/
6367
execute(args, live) {
64-
return helper.execute(args, live, this.options.silent, this.configFile, this.options);
68+
return helper.execute(
69+
args,
70+
live,
71+
this.options.silent,
72+
this.options.silentLogs,
73+
this.configFile,
74+
this.options
75+
);
6576
}
6677
}
6778

0 commit comments

Comments
 (0)