Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ we should rename this section to "Unreleased" -->
- `api_key` configuration file field
- `apiKey` option in the JavaScript API
- Removed the `upload-proguard` subcommand's `--app-id`, `--version`, `--version-code`, `--android-manifest`, and `--platform` arguments ([#2876](https://github.com/getsentry/sentry-cli/pull/2876), [#2940](https://github.com/getsentry/sentry-cli/pull/2940), [#2948](https://github.com/getsentry/sentry-cli/pull/2948)). Users using these arguments should stop using them, as they are unnecessary. The information passed to these arguments is no longer visible in Sentry.
- In the JS API, the `SentryCli.execute` method's `live` parameter now only takes boolean values ([#2971](https://github.com/getsentry/sentry-cli/pull/2971)). Setting `live` to `true` now behaves like `'rejectOnError'` did previously, with a zero exit status resolving the returned promise with `"success (live mode)"` and a non-zero status rejecting the promise with an error message.
- In the JS API, the `option` parameter to `Releases.uploadSourceMaps` no longer takes a `live` property ([#2971](https://github.com/getsentry/sentry-cli/pull/2971)). We now always execute the command with `live` set to `true`.

### Improvements

Expand Down
14 changes: 12 additions & 2 deletions lib/__tests__/helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,27 @@ describe('SentryCli helper', () => {
expect(output.trim()).toBe('sentry-cli DEV');
});

test('execute with live=true resolves without output', async () => {
test('execute with live=true resolves on success', async () => {
// TODO (v3): This should resolve with a string, not undefined/void
const result = await helper.execute(['--version'], true);
expect(result).toBeUndefined();
expect(result).toBe('success (live mode)');
});

test('execute with live=true rejects on failure', async () => {
await expect(helper.execute(['fail'], true)).rejects.toThrow(
'Command fail failed with exit code 1'
);
});

// live=rejectOnError is not supported per the type declarations, but we should still aim
// to support it for backwards compatibility.
test('execute with live=rejectOnError resolves on success', async () => {
const result = await helper.execute(['--version'], 'rejectOnError');
expect(result).toBe('success (live mode)');
});

// live=rejectOnError is not supported per the type declarations, but we should still aim
// to support it for backwards compatibility.
test('execute with live=rejectOnError rejects on failure', async () => {
await expect(helper.execute(['fail'], 'rejectOnError')).rejects.toThrow(
'Command fail failed with exit code 1'
Expand Down
23 changes: 7 additions & 16 deletions lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,10 @@ function getPath() {
* expect(output.trim()).toBe('sentry-cli x.y.z');
*
* @param {string[]} args Command line arguments passed to `sentry-cli`.
* @param {boolean | 'rejectOnError'} live can be set to:
* - `true` to inherit stdio to display `sentry-cli` output directly.
* - `false` to not inherit stdio and return the output as a string.
* - `'rejectOnError'` to inherit stdio and reject the promise if the command
* @param {boolean} live can be set to:
* - `true` to inherit stdio and reject the promise if the command
* exits with a non-zero exit code.
* - `false` to not inherit stdio and return the output as a string.
* @param {boolean} silent Disable stdout for silents build (CI/Webpack Stats, ...)
* @param {string} [configFile] Relative or absolute path to the configuration file.
* @param {import('./index').SentryCliOptions} [config] More configuration to pass to the CLI
Expand Down Expand Up @@ -325,26 +324,18 @@ async function execute(args, live, silent, configFile, config = {}) {
}

return new Promise((resolve, reject) => {
if (live === true || live === 'rejectOnError') {
if (live) {
const output = silent ? 'ignore' : 'inherit';
const pid = childProcess.spawn(getPath(), args, {
env,
// stdin, stdout, stderr
stdio: ['ignore', output, output],
});
pid.on('exit', (exitCode) => {
if (live === 'rejectOnError') {
if (exitCode === 0) {
resolve('success (live mode)');
}
reject(new Error(`Command ${args.join(' ')} failed with exit code ${exitCode}`));
if (exitCode === 0) {
resolve('success (live mode)');
}
// According to the type definition, resolving with void is not allowed.
// However, for backwards compatibility, we resolve void here to
// avoid a behaviour-breaking change.
// TODO (v3): Clean this up and always resolve a string (or change the type definition)
// @ts-expect-error - see comment above
resolve();
reject(new Error(`Command ${args.join(' ')} failed with exit code ${exitCode}`));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing else causes reject after successful resolve

The exit handler unconditionally calls reject() after conditionally calling resolve() for zero exit codes. When exitCode === 0, both resolve('success (live mode)') and reject(new Error(...)) execute sequentially. While promises only settle once, this creates an unhandled rejection and indicates incorrect control flow logic. An else clause or return statement is needed to prevent rejection after successful resolution.

Fix in Cursor Fix in Web

});
} else {
childProcess.execFile(getPath(), args, { env }, (err, stdout) => {
Expand Down
7 changes: 3 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,10 @@ class SentryCli {
/**
* See {helper.execute} docs.
* @param {string[]} args Command line arguments passed to `sentry-cli`.
* @param {boolean | 'rejectOnError'} live can be set to:
* - `true` to inherit stdio to display `sentry-cli` output directly.
* - `false` to not inherit stdio and return the output as a string.
* - `'rejectOnError'` to inherit stdio and reject the promise if the command
* @param {boolean} live can be set to:
* - `true` to inherit stdio and reject the promise if the command
* exits with a non-zero exit code.
* - `false` to not inherit stdio and return the output as a string.
* @returns {Promise<string>} A promise that resolves to the standard output.
*/
execute(args, live) {
Expand Down
45 changes: 5 additions & 40 deletions lib/releases/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ describe('SentryCli releases', () => {
test('call sentry-cli releases propose-version', () => {
expect.assertions(1);
const cli = new SentryCli();
return cli.releases.proposeVersion().then(version => expect(version).toBeTruthy());
return cli.releases.proposeVersion().then((version) => expect(version).toBeTruthy());
});

describe('with mock', () => {
let cli;
let mockExecute;
beforeAll(() => {
mockExecute = jest.fn(async () => { });
mockExecute = jest.fn(async () => {});
jest.doMock('../../helper', () => ({
...jest.requireActual('../../helper'),
execute: mockExecute,
Expand Down Expand Up @@ -52,15 +52,7 @@ describe('SentryCli releases', () => {
test('without projects', async () => {
await cli.releases.uploadSourceMaps('my-version', { include: ['path'] });
expect(mockExecute).toHaveBeenCalledWith(
[
'sourcemaps',
'upload',
'--release',
'my-version',
'path',
'--ignore',
'node_modules',
],
['sourcemaps', 'upload', '--release', 'my-version', 'path', '--ignore', 'node_modules'],
true,
false,
undefined,
Expand Down Expand Up @@ -100,17 +92,9 @@ describe('SentryCli releases', () => {
await cli.releases.uploadSourceMaps('my-version', { include: paths });

expect(mockExecute).toHaveBeenCalledTimes(2);
paths.forEach(path =>
paths.forEach((path) =>
expect(mockExecute).toHaveBeenCalledWith(
[
'sourcemaps',
'upload',
'--release',
'my-version',
path,
'--ignore',
'node_modules',
],
['sourcemaps', 'upload', '--release', 'my-version', path, '--ignore', 'node_modules'],
true,
false,
undefined,
Expand Down Expand Up @@ -159,25 +143,6 @@ describe('SentryCli releases', () => {
{ silent: false }
);
});

test.each([true, false, 'rejectOnError'])('handles live mode %s', async (live) => {
await cli.releases.uploadSourceMaps('my-version', { include: ['path'], live });
expect(mockExecute).toHaveBeenCalledWith(
[
'sourcemaps',
'upload',
'--release',
'my-version',
'path',
'--ignore',
'node_modules',
],
live,
false,
undefined,
{ silent: false }
);
});
});
});
});
15 changes: 5 additions & 10 deletions lib/releases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,10 @@ class Releases {
* ext: ['js', 'map', 'jsbundle', 'bundle'], // override file extensions to scan for
* projects: ['node'], // provide a list of projects
* decompress: false // decompress gzip files before uploading
* live: true // whether to inherit stdio to display `sentry-cli` output directly.
* });
*
* @param {string} release Unique name of the release.
* @param {SentryCliUploadSourceMapsOptions & {live?: boolean | 'rejectOnError'}} options Options to configure the source map upload.
* @param {SentryCliUploadSourceMapsOptions} options Options to configure the source map upload.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@szokeasaurusrex Here the live option is entirely removed, but in types.ts the live option is just adapted. What is now true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly, this removal is intentional. For sourcemaps upload, we always use live = true, and there is no way to override this (this is actually what we had prior to introducing the 'rejectOnError' option; we only made live configurable here to allow us to use 'rejectOnError').

* @returns {Promise<string[]>} A promise that resolves when the upload has completed successfully.
* @memberof SentryReleases
*/
Expand Down Expand Up @@ -193,10 +192,7 @@ class Releases {

return uploadPaths.map((path) =>
// `execute()` is async and thus we're returning a promise here
this.execute(
helper.prepareCommand([...args, path], SOURCEMAPS_SCHEMA, newOptions),
options.live != null ? options.live : true
)
this.execute(helper.prepareCommand([...args, path], SOURCEMAPS_SCHEMA, newOptions), true)
);
});

Expand Down Expand Up @@ -252,11 +248,10 @@ class Releases {
/**
* See {helper.execute} docs.
* @param {string[]} args Command line arguments passed to `sentry-cli`.
* @param {boolean | 'rejectOnError'} live can be set to:
* - `true` to inherit stdio to display `sentry-cli` output directly.
* - `false` to not inherit stdio and return the output as a string.
* - `'rejectOnError'` to inherit stdio and reject the promise if the command
* @param {boolean} live can be set to:
* - `true` to inherit stdio and reject the promise if the command
* exits with a non-zero exit code.
* - `false` to not inherit stdio and return the output as a string.
* @returns {Promise<string>} A promise that resolves to the standard output.
*/
async execute(args, live) {
Expand Down
4 changes: 2 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,12 @@ export interface SentryCliReleases {

uploadSourceMaps(
release: string,
options: SentryCliUploadSourceMapsOptions & { live?: boolean | 'rejectOnError' }
options: SentryCliUploadSourceMapsOptions & { live?: boolean }
): Promise<string[]>;

listDeploys(release: string): Promise<string>;

newDeploy(release: string, options: SentryCliNewDeployOptions): Promise<string>;

execute(args: string[], live: boolean | 'rejectOnError'): Promise<string>;
execute(args: string[], live: boolean): Promise<string>;
}
Loading