diff --git a/packages/nx/src/command-line/release/utils/remote-release-clients/github.spec.ts b/packages/nx/src/command-line/release/utils/remote-release-clients/github.spec.ts index d7631586a5e..4ebc1b6f185 100644 --- a/packages/nx/src/command-line/release/utils/remote-release-clients/github.spec.ts +++ b/packages/nx/src/command-line/release/utils/remote-release-clients/github.spec.ts @@ -1,4 +1,5 @@ import type { Mock } from 'vitest'; +import { output } from '../../../../utils/output'; import { GithubRemoteReleaseClient } from './github'; vi.mock('axios', () => { @@ -12,10 +13,16 @@ vi.mock('node:child_process', async () => ({ execSync: require('node:child_process').execSync, })); +vi.mock('../../../../utils/prompt-helpers', () => ({ + selectPrompt: vi.fn(), +})); + import { execFileSync } from 'node:child_process'; +import { selectPrompt } from '../../../../utils/prompt-helpers'; const axiosGetMock = (await import('axios')).default.get as Mock; const execFileSyncMock = execFileSync as Mock; +const selectPromptMock = selectPrompt as Mock; describe('GithubRemoteReleaseClient', () => { const client = new GithubRemoteReleaseClient( @@ -168,4 +175,93 @@ describe('GithubRemoteReleaseClient', () => { ).resolves.toBeUndefined(); expect(authors.get('Test User')?.username).toBeUndefined(); }); + + describe('handleError', () => { + const repoData = { + hostname: 'github.com', + slug: 'nrwl/nx', + apiBaseUrl: 'https://api.github.com', + }; + + async function printedErrorBody( + client: GithubRemoteReleaseClient + ): Promise { + const errorSpy = vi.spyOn(output, 'error').mockImplementation(() => {}); + selectPromptMock.mockResolvedValue('No'); + const originalExitCode = process.exitCode; + try { + await (client as any).handleError( + { response: { data: { message: 'Bad credentials' } } }, + { url: 'https://github.com/nrwl/nx/releases/new', requestData: {} } + ); + } finally { + process.exitCode = originalExitCode; + } + expect(errorSpy).toHaveBeenCalledTimes(1); + const printed = errorSpy.mock.calls[0][0].bodyLines.join('\n'); + errorSpy.mockRestore(); + return printed; + } + + it('should redact the token in the API error output', async () => { + const token = 'ghp_secret'; + const clientWithToken = new GithubRemoteReleaseClient(repoData, false, { + token, + headerName: 'Authorization', + }); + + const printed = await printedErrorBody(clientWithToken); + + expect(printed).not.toContain(token); + expect(printed).toContain( + 'Token Header: Authorization: Bearer ' + ); + }); + + it('should report when no token was configured', async () => { + const clientWithoutToken = new GithubRemoteReleaseClient( + repoData, + false, + null + ); + + const printed = await printedErrorBody(clientWithoutToken); + + expect(printed).toContain('Token Header: none'); + }); + + it('should redact the token in the unknown-error dump', async () => { + const token = 'ghp_secret'; + const clientWithToken = new GithubRemoteReleaseClient(repoData, false, { + token, + headerName: 'Authorization', + }); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + selectPromptMock.mockResolvedValue('No'); + const originalExitCode = process.exitCode; + + try { + await (clientWithToken as any).handleError( + { + message: 'Network Error', + config: { headers: { Authorization: `Bearer ${token}` } }, + request: { _header: `Authorization: Bearer ${token}` }, + }, + { url: 'https://github.com/nrwl/nx/releases/new', requestData: {} } + ); + } finally { + process.exitCode = originalExitCode; + } + + const logged = logSpy.mock.calls.map((args) => args.join(' ')).join('\n'); + expect(logged).not.toContain(token); + expect(logged).toContain(''); + expect(logged).toContain('Network Error'); + logSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + }); }); diff --git a/packages/nx/src/command-line/release/utils/remote-release-clients/github.ts b/packages/nx/src/command-line/release/utils/remote-release-clients/github.ts index e73b4e95d48..1e30c025b22 100644 --- a/packages/nx/src/command-line/release/utils/remote-release-clients/github.ts +++ b/packages/nx/src/command-line/release/utils/remote-release-clients/github.ts @@ -341,12 +341,12 @@ export class GithubRemoteReleaseClient extends RemoteReleaseClient()?.slug}`, - `Token Header Data: ${this.tokenHeader}`, + `Token Header: ${this.getRedactedTokenHeader()}`, `Body: ${JSON.stringify(result.requestData)}`, ], }); } else { - console.log(error); + console.log(this.inspectWithRedactedToken(error)); console.error( `An unknown error occurred while trying to create a release on GitHub, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitHub token from the error message!)` ); diff --git a/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.spec.ts b/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.spec.ts new file mode 100644 index 00000000000..b3a16fd3da3 --- /dev/null +++ b/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.spec.ts @@ -0,0 +1,105 @@ +import type { Mock } from 'vitest'; +import { output } from '../../../../utils/output'; +import { GitLabRemoteReleaseClient } from './gitlab'; + +vi.mock('../../../../utils/prompt-helpers', () => ({ + selectPrompt: vi.fn(), +})); + +import { selectPrompt } from '../../../../utils/prompt-helpers'; + +const selectPromptMock = selectPrompt as Mock; + +describe('GitLabRemoteReleaseClient', () => { + afterEach(() => { + vi.resetAllMocks(); + }); + + describe('handleError', () => { + const repoData = { + hostname: 'gitlab.com', + slug: 'nrwl/nx', + apiBaseUrl: 'https://gitlab.com/api/v4', + projectId: 'nrwl%2Fnx', + }; + + async function printedErrorBody( + client: GitLabRemoteReleaseClient + ): Promise { + const errorSpy = vi.spyOn(output, 'error').mockImplementation(() => {}); + selectPromptMock.mockResolvedValue('No'); + const originalExitCode = process.exitCode; + try { + await (client as any).handleError( + { response: { data: { message: '401 Unauthorized' } } }, + { url: 'https://gitlab.com/nrwl/nx/-/releases/new', requestData: {} } + ); + } finally { + process.exitCode = originalExitCode; + } + expect(errorSpy).toHaveBeenCalledTimes(1); + const printed = errorSpy.mock.calls[0][0].bodyLines.join('\n'); + errorSpy.mockRestore(); + return printed; + } + + it('should redact the token in the API error output', async () => { + const token = 'glpat-secret'; + const clientWithToken = new GitLabRemoteReleaseClient(repoData, false, { + token, + headerName: 'PRIVATE-TOKEN', + }); + + const printed = await printedErrorBody(clientWithToken); + + expect(printed).not.toContain(token); + expect(printed).toContain('Token Header: PRIVATE-TOKEN: '); + }); + + it('should report when no token was configured', async () => { + const clientWithoutToken = new GitLabRemoteReleaseClient( + repoData, + false, + null + ); + + const printed = await printedErrorBody(clientWithoutToken); + + expect(printed).toContain('Token Header: none'); + }); + + it('should redact the token in the unknown-error dump', async () => { + const token = 'glpat-secret'; + const clientWithToken = new GitLabRemoteReleaseClient(repoData, false, { + token, + headerName: 'PRIVATE-TOKEN', + }); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + selectPromptMock.mockResolvedValue('No'); + const originalExitCode = process.exitCode; + + try { + await (clientWithToken as any).handleError( + { + message: 'Network Error', + config: { headers: { 'PRIVATE-TOKEN': token } }, + request: { _header: `PRIVATE-TOKEN: ${token}` }, + }, + { url: 'https://gitlab.com/nrwl/nx/-/releases/new', requestData: {} } + ); + } finally { + process.exitCode = originalExitCode; + } + + const logged = logSpy.mock.calls.map((args) => args.join(' ')).join('\n'); + expect(logged).not.toContain(token); + expect(logged).toContain(''); + expect(logged).toContain('Network Error'); + logSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + }); +}); diff --git a/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.ts b/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.ts index 21198a6b7e5..7816d0db4e8 100644 --- a/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.ts +++ b/packages/nx/src/command-line/release/utils/remote-release-clients/gitlab.ts @@ -234,12 +234,12 @@ export class GitLabRemoteReleaseClient extends RemoteReleaseClient()?.slug}`, - `Token Header Data: ${this.tokenHeader}`, + `Token Header: ${this.getRedactedTokenHeader()}`, `Body: ${JSON.stringify(result.requestData)}`, ], }); } else { - console.log(error); + console.log(this.inspectWithRedactedToken(error)); console.error( `An unknown error occurred while trying to create a release on GitLab, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitLab token from the error message!)` ); diff --git a/packages/nx/src/command-line/release/utils/remote-release-clients/remote-release-client.ts b/packages/nx/src/command-line/release/utils/remote-release-clients/remote-release-client.ts index b15efc64d94..011cc83b9f4 100644 --- a/packages/nx/src/command-line/release/utils/remote-release-clients/remote-release-client.ts +++ b/packages/nx/src/command-line/release/utils/remote-release-clients/remote-release-client.ts @@ -1,3 +1,4 @@ +import { inspect } from 'node:util'; import type { AxiosRequestConfig } from 'axios'; import axios from 'axios'; import type { PostGitTask } from '../../changelog'; @@ -67,6 +68,23 @@ export abstract class RemoteReleaseClient< return this.remoteRepoData as T | null; } + protected inspectWithRedactedToken(error: unknown): string { + const inspected = inspect(error); + return this.tokenData + ? inspected.split(this.tokenData.token).join('') + : inspected; + } + + protected getRedactedTokenHeader(): string { + if (!this.tokenData) { + return 'none'; + } + const { headerName } = this.tokenData; + return headerName === 'Authorization' + ? `${headerName}: Bearer ` + : `${headerName}: `; + } + /** * Create a post git task that will be executed by nx release changelog after performing any relevant * git operations, if the user has opted into remote release creation.