Skip to content

Commit

Permalink
fix(publish): fix version conflict recognition for github packages (#738
Browse files Browse the repository at this point in the history
)

The version conflict recognition logic was not working for github
because it returns a different `code` for the same error compared to
what npmjs.org returns.

I've added two utility functions to check for the error code and
message to make the code more readable in the error handler of the
publish operation.

This way there is a robust detection supporting both npmjs.com and
npm.pkg.github.com registries, not just npmjs.org.

Signed-off-by: Peter Somogyvari <peter.metz@unarin.com>
  • Loading branch information
petermetz committed Oct 5, 2023
1 parent d8aeb27 commit 210eefa
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 7 deletions.
115 changes: 113 additions & 2 deletions packages/publish/src/__tests__/publish-from-git.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,11 @@ describe('publish from-git', () => {
process.exitCode = undefined;
});

it('should not throw and assume package is already published when npm publish throws EPUBLISHCONFLICT from a previous half publish process', async () => {
it('should not throw and assume package is already published on npmjs.com registry when npm publish throws EPUBLISHCONFLICT from a previous half publish process', async () => {
(npmPublish as Mock).mockImplementation(() => {
return Promise.reject({ code: 'EPUBLISHCONFLICT' });
const ex = new Error('Mocked npm publish fail') as Error & { code: string };
ex.code = 'EPUBLISHCONFLICT';
return Promise.reject(ex);
});

const cwd = await initFixture('normal');
Expand All @@ -270,4 +272,113 @@ describe('publish from-git', () => {
await command.execute();
expect(loggerSpy).toHaveBeenCalledWith('All packages have already been published.');
});

it('should not throw and assume package is already published on npmjs.com registry when npm publish throws "You cannot publish over the previously published versions" from a previous half publish process', async () => {
(npmPublish as Mock).mockImplementation(() => {
const ex = new Error('You cannot publish over the previously published versions') as Error & { code: string };
ex.code = 'E403';
return Promise.reject(ex);
});

const cwd = await initFixture('normal');

await gitTag(cwd, 'v1.0.0');
const command = new PublishCommand(createArgv(cwd, 'from-git', '--no-sort'));

await expect(command).resolves.toBeUndefined();

// called from chained describeRef()
expect(throwIfUncommitted).toHaveBeenCalled();

// since all packages rejected with publish conflict, we should have the "All published" message when recalling execute()
command.initialize();
const loggerSpy = vi.spyOn(command.logger, 'success');
await command.execute();
expect(loggerSpy).toHaveBeenCalledWith('All packages have already been published.');
});

it('should not throw and assume package is already published on GitHub npm Registry when npm publish throws E409 from a previous half publish process', async () => {
(npmPublish as Mock).mockImplementation(() => {
const ex = new Error('Mocked npm publish fail') as Error & { code: string };
ex.code = 'E409';
return Promise.reject(ex);
});

const cwd = await initFixture('normal');

await gitTag(cwd, 'v1.0.0');
const command = new PublishCommand(createArgv(cwd, 'from-git', '--no-sort'));

await expect(command).resolves.toBeUndefined();

// called from chained describeRef()
expect(throwIfUncommitted).toHaveBeenCalled();

// since all packages rejected with publish conflict, we should have the "All published" message when recalling execute()
command.initialize();
const loggerSpy = vi.spyOn(command.logger, 'success');
await command.execute();
expect(loggerSpy).toHaveBeenCalledWith('All packages have already been published.');
});

it('should not throw and assume package is already published on GitHub npm Registry when npm publish throws "409 Conflict - PUT https://npm.pkg.github.com" from a previous half publish process', async () => {
(npmPublish as Mock).mockImplementation(() => {
const ex = new Error('409 Conflict - PUT https://npm.pkg.github.com/');
return Promise.reject(ex);
});

const cwd = await initFixture('normal');

await gitTag(cwd, 'v1.0.0');
const command = new PublishCommand(createArgv(cwd, 'from-git', '--no-sort'));

await expect(command).resolves.toBeUndefined();

// called from chained describeRef()
expect(throwIfUncommitted).toHaveBeenCalled();

// since all packages rejected with publish conflict, we should have the "All published" message when recalling execute()
command.initialize();
const loggerSpy = vi.spyOn(command.logger, 'success');
await command.execute();
expect(loggerSpy).toHaveBeenCalledWith('All packages have already been published.');
});

it('should not throw and assume package is already published on GitHub npm Registry when npm publish throws "Cannot publish over existing version" from a previous half publish process', async () => {
(npmPublish as Mock).mockImplementation(() => {
const ex = new Error('some unrelated error message because we want detection based on ex.body.error') as Error & { body: { error: string } };
ex.body = { error: 'Cannot publish over existing version' };
return Promise.reject(ex);
});

const cwd = await initFixture('normal');

await gitTag(cwd, 'v1.0.0');
const command = new PublishCommand(createArgv(cwd, 'from-git', '--no-sort'));

await expect(command).resolves.toBeUndefined();

// called from chained describeRef()
expect(throwIfUncommitted).toHaveBeenCalled();

// since all packages rejected with publish conflict, we should have the "All published" message when recalling execute()
command.initialize();
const loggerSpy = vi.spyOn(command.logger, 'success');
await command.execute();
expect(loggerSpy).toHaveBeenCalledWith('All packages have already been published.');
});

it('should re-throw when npm publish throws not related to npmjs/github version conflict', async () => {
(npmPublish as Mock).mockImplementation(() => {
const ex = new Error('The milk was spilt.');
return Promise.reject(ex);
});

const cwd = await initFixture('normal');

await gitTag(cwd, 'v1.0.0');
const command = new PublishCommand(createArgv(cwd, 'from-git', '--no-sort'));

await expect(command).rejects.toBeTruthy();
});
});
40 changes: 40 additions & 0 deletions packages/publish/src/lib/is-npm-js-publish-version-conflict.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* The purpose of this function is to determine whether the error object passed
* in represents a version conflict when publishing a package to the npm registry.
*
* An example of a version conflict error is:
* ```json
* {
* "name": "HttpErrorGeneral",
* "headers": {
* },
* "statusCode": 403,
* "code": "E403",
* "method": "PUT",
* "uri": "https://registry.npmjs.org/@hyperledger%2fcactus-cmd-api-server",
* "body": {
* "success": false,
* "error": "You cannot publish over the previously published versions: 2.0.0-alpha.2."
* },
* "pkgid": "@hyperledger/cactus-cmd-api-server@2.0.0-alpha.2",
* "message": "403 Forbidden - PUT https://registry.npmjs.org/@hyperledger%2fcactus-cmd-api-server - You cannot publish over the previously published versions: 2.0.0-alpha.2."
* }
* ```
*
* @param ex The exception object to check for a version conflict error.
* @returns {boolean} true if the error represents a version conflict, false otherwise
*/
export function isNpmJsPublishVersionConflict(ex: unknown): boolean {
if (!ex || typeof ex !== 'object' || !(ex instanceof Error)) {
return false;
} else if ('code' in ex && ex.code === 'EPUBLISHCONFLICT') {
return true;
} else if (
'code' in ex &&
ex.code === 'E403' &&
ex.message?.includes('You cannot publish over the previously published versions')
) {
return true;
}
return false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* The purpose of this function is to determine whether the exception object
* passed in represents a version conflict when publishing a package to the
* GitHub npm registry.
*
* An example of a version conflict error is pasted below:
*
* ```json
* {
* "name": "HttpErrorGeneral",
* "headers": {
* },
* "statusCode": 409,
* "code": "E409",
* "method": "PUT",
* "uri": "https://npm.pkg.github.com/hyperledger/@hyperledger%2fcacti-cactus-cmd-api-server",
* "body": {
* "error": "Cannot publish over existing version"
* },
* "pkgid": "@hyperledger/cacti-cactus-cmd-api-server@2.0.0-alpha.2",
* "message": "409 Conflict - PUT https://npm.pkg.github.com/hyperledger/@hyperledger%2fcacti-cactus-cmd-api-server - Cannot publish over existing version"
* }
* ```
* @param ex The exception object to check for a version conflict error.
* @returns {boolean} true if the error represents a version conflict, false otherwise.
*/
export function isNpmPkgGitHubPublishVersionConflict(ex: unknown): boolean {
if (!ex || typeof ex !== 'object' || !(ex instanceof Error)) {
return false;
} else if ('code' in ex && ex.code === 'E409') {
return true;
} else if (
'body' in ex &&
typeof ex.body === 'object' &&
(ex.body as Record<string, unknown>).error === 'Cannot publish over existing version'
) {
return true;
} else {
return ex.message.startsWith('409 Conflict - PUT https://npm.pkg.github.com');
}
}
11 changes: 6 additions & 5 deletions packages/publish/src/publish-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { removeTempLicenses } from './lib/remove-temp-licenses.js';
import { createTempLicenses } from './lib/create-temp-licenses.js';
import { getPackagesWithoutLicense } from './lib/get-packages-without-license.js';
import { Tarball } from './models/index.js';
import { isNpmJsPublishVersionConflict } from './lib/is-npm-js-publish-version-conflict.js';
import { isNpmPkgGitHubPublishVersionConflict } from './lib/is-npm-pkg-github-publish-version-conflict.js';

export function factory(argv: PublishCommandOption) {
return new PublishCommand(argv);
Expand Down Expand Up @@ -931,13 +933,12 @@ export class PublishCommand extends Command<PublishCommandOption> {
return pkg;
})
.catch((err) => {
if (
err.code === 'EPUBLISHCONFLICT' ||
(err.code === 'E403' && err.body?.error?.includes('You cannot publish over the previously published versions'))
) {
const isNpmJsComConflict = isNpmJsPublishVersionConflict(err);
const isNpmPkgGitHubComConflict = isNpmPkgGitHubPublishVersionConflict(err);

if (isNpmJsComConflict || isNpmPkgGitHubComConflict) {
tracker.warn('publish', `Package is already published: ${pkg.name}@${pkg.version}`);
tracker.completeWork(1);

return pkg;
}

Expand Down

0 comments on commit 210eefa

Please sign in to comment.