Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(release): update dist-tags when publishing a package version that already exists #20316

Merged
merged 12 commits into from
Nov 24, 2023
Merged
2 changes: 1 addition & 1 deletion e2e/release/src/private-js-packages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('nx release - private JS packages', () => {

> nx run {private-project-name}:nx-release-publish

Skipping package "@proj/{private-project-name}" from project "{private-project-name}", because it has \`"private": true\` in {private-project-name}/package.json
Skipped package "@proj/{private-project-name}" from project "{private-project-name}", because it has \`"private": true\` in {private-project-name}/package.json

> nx run {public-project-name}:nx-release-publish

Expand Down
77 changes: 77 additions & 0 deletions e2e/release/src/release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,83 @@ describe('nx release', () => {



> NX Successfully ran target nx-release-publish for 3 projects



`);

// All packages should be skipped when the same publish is performed again
const publishOutput3Repeat = runCLI(publishToNext);
expect(publishOutput3Repeat).toMatchInlineSnapshot(`

> NX Running target nx-release-publish for 3 projects:

- {project-name}
- {project-name}
- {project-name}

With additional flags:
--registry=${customRegistryUrl}
--tag=next



> nx run {project-name}:nx-release-publish

Skipped package "@proj/{project-name}" from project "{project-name}" because v1000.0.0-next.0 already exists in ${customRegistryUrl} with tag "next"

> nx run {project-name}:nx-release-publish

Skipped package "@proj/{project-name}" from project "{project-name}" because v1000.0.0-next.0 already exists in ${customRegistryUrl} with tag "next"

> nx run {project-name}:nx-release-publish

Skipped package "@proj/{project-name}" from project "{project-name}" because v1000.0.0-next.0 already exists in ${customRegistryUrl} with tag "next"



> NX Successfully ran target nx-release-publish for 3 projects



`);

// All packages should have dist-tags updated when they were already published to a different dist-tag
const publishOutput3NewDistTags = runCLI(
`release publish --registry=${customRegistryUrl} --tag=next2`
);
expect(publishOutput3NewDistTags).toMatchInlineSnapshot(`

> NX Running target nx-release-publish for 3 projects:

- {project-name}
- {project-name}
- {project-name}

With additional flags:
--registry=${customRegistryUrl}
--tag=next2



> nx run {project-name}:nx-release-publish

Added the dist-tag next2 to v1000.0.0-next.0 for registry ${customRegistryUrl}.


> nx run {project-name}:nx-release-publish

Added the dist-tag next2 to v1000.0.0-next.0 for registry ${customRegistryUrl}.


> nx run {project-name}:nx-release-publish

Added the dist-tag next2 to v1000.0.0-next.0 for registry ${customRegistryUrl}.




> NX Successfully ran target nx-release-publish for 3 projects


Expand Down
123 changes: 97 additions & 26 deletions packages/js/src/executors/release-publish/release-publish.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,21 @@ export default async function runExecutor(

if (projectPackageJson.private === true) {
console.warn(
`Skipping ${packageTxt}, because it has \`"private": true\` in ${packageJsonPath}`
`Skipped ${packageTxt}, because it has \`"private": true\` in ${packageJsonPath}`
);
return {
success: true,
};
}

const npmPublishCommandSegments = [`npm publish --json`];
const npmViewCommandSegments = [
`npm view ${packageName} versions dist-tags --json`,
];

if (options.registry) {
npmPublishCommandSegments.push(`--registry=${options.registry}`);
npmViewCommandSegments.push(`--registry=${options.registry}`);
}

if (options.tag) {
Expand All @@ -72,6 +76,98 @@ export default async function runExecutor(
const registry =
options.registry ?? execSync(`npm config get registry`).toString().trim();
const tag = options.tag ?? execSync(`npm config get tag`).toString().trim();
const currentVersion = projectPackageJson.version;

try {
const result = execSync(npmViewCommandSegments.join(' '), {
env: processEnv(true),
cwd: packageRoot,
stdio: ['ignore', 'pipe', 'pipe'],
});

const resultJson = JSON.parse(result.toString());
const distTags = resultJson['dist-tags'] || {};
if (distTags[tag] === currentVersion) {
console.warn(
`Skipped ${packageTxt} because v${currentVersion} already exists in ${registry} with tag "${tag}"`
);
return {
success: true,
};
}

if (resultJson.versions.includes(currentVersion)) {
try {
if (!options.dryRun) {
execSync(
`npm dist-tag add ${packageName}@${currentVersion} ${tag} --registry=${registry}`,
{
env: processEnv(true),
cwd: packageRoot,
stdio: 'ignore',
}
);
console.log(
`Added the dist-tag ${tag} to v${currentVersion} for registry ${registry}.\n`
);
} else {
console.log(
`Would add the dist-tag ${tag} to v${currentVersion} for registry ${registry}, but ${chalk.keyword(
'orange'
)('[dry-run]')} was set.\n`
);
}
return {
success: true,
};
} catch (err) {
try {
const stdoutData = JSON.parse(err.stdout?.toString() || '{}');

console.error('npm dist-tag add error:');
if (stdoutData.error.summary) {
console.error(stdoutData.error.summary);
}
if (stdoutData.error.detail) {
console.error(stdoutData.error.detail);
}

if (context.isVerbose) {
console.error('npm dist-tag add stdout:');
console.error(JSON.stringify(stdoutData, null, 2));
}
return {
success: false,
};
} catch (err) {
console.error(
'Something unexpected went wrong when processing the npm dist-tag add output\n',
err
);
return {
success: false,
};
}
}
}
} catch (err) {
const stdoutData = JSON.parse(err.stdout?.toString() || '{}');
// If the error is that the package doesn't exist, then we can ignore it because we will be publishing it for the first time in the next step
if (
!(
stdoutData.error?.code?.includes('E404') &&
stdoutData.error?.summary?.includes('no such package available')
)
) {
console.error(
`Something unexpected went wrong when checking for existing dist-tags.\n`,
err
);
return {
success: false,
};
}
}

try {
const output = execSync(npmPublishCommandSegments.join(' '), {
Expand Down Expand Up @@ -102,30 +198,7 @@ export default async function runExecutor(
};
} catch (err) {
try {
const currentVersion = projectPackageJson.version;

const stdoutData = JSON.parse(err.stdout?.toString() || '{}');
if (
// handle npm conflict error
stdoutData.error?.code === 'EPUBLISHCONFLICT' ||
// handle npm conflict error when the package has a scope
(stdoutData.error?.code === 'E403' &&
stdoutData.error?.summary?.includes(
'You cannot publish over the previously published versions'
)) ||
// handle verdaccio conflict error
(stdoutData.error?.code === 'E409' &&
stdoutData.error?.summary?.includes(
'this package is already present'
))
) {
console.warn(
`Skipping ${packageTxt}, as v${currentVersion} has already been published to ${registry} with tag "${tag}"`
);
return {
success: true,
};
}

console.error('npm publish error:');
if (stdoutData.error.summary) {
Expand All @@ -143,8 +216,6 @@ export default async function runExecutor(
success: false,
};
} catch (err) {
// npm v9 onwards seems to guarantee stdout will be well formed JSON when --json is used, so maybe we need to
// specify that as minimum supported version? (comes with node 18 and 20 by default)
console.error(
'Something unexpected went wrong when processing the npm publish output\n',
err
Expand Down