Skip to content

Commit

Permalink
feat(release): update dist-tags when publishing a package version tha…
Browse files Browse the repository at this point in the history
…t already exists (#20316)

Co-authored-by: James Henry <james@henry.sc>
  • Loading branch information
fahslaj and JamesHenry committed Nov 24, 2023
1 parent 74d438c commit 378d9b3
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 27 deletions.
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

0 comments on commit 378d9b3

Please sign in to comment.