diff --git a/e2e/release/src/multiple-release-branches.test.ts b/e2e/release/src/multiple-release-branches.test.ts new file mode 100644 index 0000000000000..51c5d4ca744b6 --- /dev/null +++ b/e2e/release/src/multiple-release-branches.test.ts @@ -0,0 +1,441 @@ +import { NxJsonConfiguration } from '@nx/devkit'; +import { + cleanupProject, + getSelectedPackageManager, + newProject, + runCLI, + runCommand, + tmpProjPath, + uniq, + updateJson, +} from '@nx/e2e/utils'; + +expect.addSnapshotSerializer({ + serialize(str: string) { + return ( + str + // Remove all output unique to specific projects to ensure deterministic snapshots + .replaceAll(`/private/${tmpProjPath()}`, '') + .replaceAll(tmpProjPath(), '') + .replaceAll('/private/', '') + .replaceAll(/my-pkg-\d+/g, '{project-name}') + .replaceAll(' in /{project-name}', ' in {project-name}') + .replaceAll( + /integrity:\s*.*/g, + 'integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + ) + .replaceAll(/\b[0-9a-f]{40}\b/g, '{SHASUM}') + .replaceAll(/\d*B index\.js/g, 'XXB index.js') + .replaceAll(/\d*B project\.json/g, 'XXB project.json') + .replaceAll(/\d*B package\.json/g, 'XXXB package.json') + .replaceAll(/\d*B CHANGELOG\.md/g, 'XXXB CHANGELOG.md') + .replaceAll(/size:\s*\d*\s?B/g, 'size: XXXB') + .replaceAll(/\d*\.\d*\s?kB/g, 'XXX.XXX kb') + // Normalize the version title date + .replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)') + .replaceAll('package-lock.json', '{lock-file}') + .replaceAll('yarn.lock', '{lock-file}') + .replaceAll('pnpm-lock.yaml', '{lock-file}') + .replaceAll('npm install --package-lock-only', '{lock-file-command}') + .replaceAll( + 'yarn install --mode update-lockfile', + '{lock-file-command}' + ) + .replaceAll('pnpm install --lockfile-only', '{lock-file-command}') + .replaceAll(getSelectedPackageManager(), '{package-manager}') + // We trim each line to reduce the chances of snapshot flakiness + .split('\n') + .map((r) => r.trim()) + .join('\n') + ); + }, + test(val: string) { + return val != null && typeof val === 'string'; + }, +}); + +describe('nx release multiple release branches', () => { + let pkg1: string; + let pkg2: string; + let pkg3: string; + + beforeEach(() => { + newProject({ + unsetProjectNameAndRootFormat: false, + packages: ['@nx/js'], + }); + + pkg1 = uniq('my-pkg-1'); + runCLI(`generate @nx/workspace:npm-package ${pkg1}`); + + pkg2 = uniq('my-pkg-2'); + runCLI(`generate @nx/workspace:npm-package ${pkg2}`); + + pkg3 = uniq('my-pkg-3'); + runCLI(`generate @nx/workspace:npm-package ${pkg3}`); + }); + afterEach(() => cleanupProject()); + + it('git-tag version resolver should not detect tags in other branches', async () => { + updateJson('nx.json', (json) => { + json.release = { + version: { + git: { + commit: true, + tag: true, + }, + generatorOptions: { + currentVersionResolver: 'git-tag', + }, + }, + }; + + return json; + }); + + runCommand(`git checkout -b release/0.x`); + runCommand(`git add .`); + runCommand(`git commit -m "chore: initial commit"`); + const initialVersionResult = runCLI( + `release version 0.0.7 --first-release` + ); + + runCommand(`git checkout -b release/1.x`); + + // update my-pkg-1 with a feature commit + updateJson(`${pkg1}/package.json`, (json) => ({ + ...json, + license: 'MIT', + })); + runCommand(`git add ${pkg1}/package.json`); + runCommand(`git commit -m "feat(${pkg1}): new feature 1"`); + + const versionResult1x = runCLI(`release version minor`); + + // update my-pkg-2 with a fix commit + updateJson(`${pkg2}/package.json`, (json) => ({ + ...json, + license: 'MIT', + })); + runCommand(`git add ${pkg2}/package.json`); + runCommand(`git commit -m "fix(${pkg2}): new fix 1"`); + runCommand(`git checkout release/0.x`); + const versionResult0x = runCLI(`release version patch`); + + expect(initialVersionResult).toMatchInlineSnapshot(` + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Unable to resolve the current version from git tag using pattern "v{version}". Falling back to the version on disk of 0.0.0 + {project-name} 📄 Using the provided version specifier "0.0.7". + {project-name} ✍️ New version 0.0.7 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.0 already resolved from disk fallback. + {project-name} 📄 Using the provided version specifier "0.0.7". + {project-name} ✍️ New version 0.0.7 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.0 already resolved from disk fallback. + {project-name} 📄 Using the provided version specifier "0.0.7". + {project-name} ✍️ New version 0.0.7 written to {project-name}/package.json + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "0.0.7", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "0.0.7", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "0.0.7", + "scripts": { + + + NX Updating {package-manager} lock file + + + NX Committing changes with git + + + NX Tagging commit with git + + + `); + expect(versionResult1x).toMatchInlineSnapshot(` + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Resolved the current version as 0.0.7 from git tag "v0.0.7". + {project-name} 📄 Using the provided version specifier "minor". + {project-name} ✍️ New version 0.1.0 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.7 already resolved from git tag "v0.0.7". + {project-name} 📄 Using the provided version specifier "minor". + {project-name} ✍️ New version 0.1.0 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.7 already resolved from git tag "v0.0.7". + {project-name} 📄 Using the provided version specifier "minor". + {project-name} ✍️ New version 0.1.0 written to {project-name}/package.json + + + "name": "@proj/{project-name}", + - "version": "0.0.7", + + "version": "0.1.0", + "scripts": { + + } + + + + + "name": "@proj/{project-name}", + - "version": "0.0.7", + + "version": "0.1.0", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.7", + + "version": "0.1.0", + "scripts": { + + + NX Updating {package-manager} lock file + + + NX Committing changes with git + + + NX Tagging commit with git + + + `); + expect(versionResult0x).toMatchInlineSnapshot(` + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Resolved the current version as 0.0.7 from git tag "v0.0.7". + {project-name} 📄 Using the provided version specifier "patch". + {project-name} ✍️ New version 0.0.8 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.7 already resolved from git tag "v0.0.7". + {project-name} 📄 Using the provided version specifier "patch". + {project-name} ✍️ New version 0.0.8 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.7 already resolved from git tag "v0.0.7". + {project-name} 📄 Using the provided version specifier "patch". + {project-name} ✍️ New version 0.0.8 written to {project-name}/package.json + + + "name": "@proj/{project-name}", + - "version": "0.0.7", + + "version": "0.0.8", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.7", + + "version": "0.0.8", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.7", + + "version": "0.0.8", + "scripts": { + + + NX Updating {package-manager} lock file + + + NX Committing changes with git + + + NX Tagging commit with git + + + `); + }); + + it('git-tag version resolver should detect tags in other branches if none are reachable from the current commit', async () => { + updateJson('nx.json', (json) => { + json.release = { + version: { + git: { + commit: true, + tag: true, + }, + generatorOptions: { + currentVersionResolver: 'git-tag', + }, + }, + }; + + return json; + }); + + runCommand(`git checkout -b test-main`); + runCommand(`git add .`); + runCommand(`git commit -m "chore: initial commit"`); + + runCommand(`git checkout -b release/1.x`); + + // update my-pkg-1 with a feature commit + updateJson(`${pkg1}/package.json`, (json) => ({ + ...json, + license: 'MIT', + })); + runCommand(`git add ${pkg1}/package.json`); + runCommand(`git commit -m "feat(${pkg1}): new feature 1"`); + + const versionResult1x = runCLI(`release version minor --first-release`); + + runCommand(`git checkout test-main`); + // update my-pkg-2 with a fix commit + updateJson(`${pkg2}/package.json`, (json) => ({ + ...json, + license: 'MIT', + })); + runCommand(`git add ${pkg2}/package.json`); + runCommand(`git commit -m "fix(${pkg2}): new fix 1"`); + runCommand(`git checkout -b release/2.x`); + const versionResult2x = runCLI(`release version major`); + + expect(versionResult1x).toMatchInlineSnapshot(` + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Unable to resolve the current version from git tag using pattern "v{version}". Falling back to the version on disk of 0.0.0 + {project-name} 📄 Using the provided version specifier "minor". + {project-name} ✍️ New version 0.1.0 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.0 already resolved from disk fallback. + {project-name} 📄 Using the provided version specifier "minor". + {project-name} ✍️ New version 0.1.0 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.0.0 already resolved from disk fallback. + {project-name} 📄 Using the provided version specifier "minor". + {project-name} ✍️ New version 0.1.0 written to {project-name}/package.json + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "0.1.0", + "scripts": { + + } + + + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "0.1.0", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "0.1.0", + "scripts": { + + + NX Updating {package-manager} lock file + + + NX Committing changes with git + + + NX Tagging commit with git + + + `); + expect(versionResult2x).toMatchInlineSnapshot(` + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Resolved the current version as 0.1.0 from git tag "v0.1.0". + {project-name} 📄 Using the provided version specifier "major". + {project-name} ✍️ New version 1.0.0 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.1.0 already resolved from git tag "v0.1.0". + {project-name} 📄 Using the provided version specifier "major". + {project-name} ✍️ New version 1.0.0 written to {project-name}/package.json + + NX Running release version for project: {project-name} + + {project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json + {project-name} 📄 Using the current version 0.1.0 already resolved from git tag "v0.1.0". + {project-name} 📄 Using the provided version specifier "major". + {project-name} ✍️ New version 1.0.0 written to {project-name}/package.json + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "1.0.0", + "scripts": { + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "1.0.0", + "scripts": { + + } + + + + + "name": "@proj/{project-name}", + - "version": "0.0.0", + + "version": "1.0.0", + "scripts": { + + + NX Updating {package-manager} lock file + + + NX Committing changes with git + + + NX Tagging commit with git + + + `); + }); +}); diff --git a/packages/nx/src/command-line/release/utils/git.ts b/packages/nx/src/command-line/release/utils/git.ts index 40af41bacbdfb..208c3d1431bca 100644 --- a/packages/nx/src/command-line/release/utils/git.ts +++ b/packages/nx/src/command-line/release/utils/git.ts @@ -46,14 +46,31 @@ export async function getLatestGitTagForPattern( additionalInterpolationData = {} ): Promise<{ tag: string; extractedVersion: string } | null> { try { - const tags = await execCommand('git', ['tag', '--sort', '-v:refname']).then( - (r) => - r - .trim() - .split('\n') - .map((t) => t.trim()) - .filter(Boolean) + let tags: string[]; + tags = await execCommand('git', [ + 'tag', + '--sort', + '-v:refname', + '--merged', + ]).then((r) => + r + .trim() + .split('\n') + .map((t) => t.trim()) + .filter(Boolean) ); + if (!tags.length) { + // try again, but include all tags on the repo instead of just --merged ones + tags = await execCommand('git', ['tag', '--sort', '-v:refname']).then( + (r) => + r + .trim() + .split('\n') + .map((t) => t.trim()) + .filter(Boolean) + ); + } + if (!tags.length) { return null; }