diff --git a/README.md b/README.md index 4a413c4..70a1b91 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,13 @@ Alternatively some options may be set via CLI flags. ### `deps` Options -| Option | Type | CLI Flag | Description | -| ------ | ---- | -------- | ----------- | -| bump | `override \| satisfy \| inherit` | `--deps.bump` | Define deps version updating rule. Allowed: override, satisfy, inherit. **`override` by default.** | -| release | `patch \| minor \| major \| inherit` | `--deps.release` | Define release type for dependent package if any of its deps changes. Supported values: patch, minor, major, inherit. **`patch` by default** | -| prefix | `'^' \| '~' \| ''` | `--deps.prefix` | Optional prefix to be attached to the next version if `bump` is set to `override`. **`''` by default**. | +| Option | Type | CLI Flag | Description | +|-----------------------|--------------------------------------|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| bump | `override \| satisfy \| inherit` | `--deps.bump` | Define deps version updating rule. Allowed: override, satisfy, inherit. **`override` by default.** | +| release | `patch \| minor \| major \| inherit` | `--deps.release` | Define release type for dependent package if any of its deps changes. Supported values: patch, minor, major, inherit. **`patch` by default** | +| prefix | `'^' \| '~' \| ''` | `--deps.prefix` | Optional prefix to be attached to the next version if `bump` is set to `override`. **`''` by default**. | +| pullTagsForPrerelease | `boolean` | `--deps.pullTagsForPrerelease` | Optional flag to use release tags for evaluating prerelease version bumping. Normally, this option will lead to dumping dependencies to a version past what was just released and tagged by semantic release. Only set this option to true if you previously had a workflow that compensated for the previous bug behavior. **`'false'` by default**. | + ### Examples diff --git a/bin/cli.js b/bin/cli.js index fa0f225..3f6df88 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -20,6 +20,7 @@ const cli = meow( --deps.bump Define deps version updating rule. Allowed: override, satisfy, inherit. --deps.release Define release type for dependent package if any of its deps changes. Supported values: patch, minor, major, inherit. --deps.prefix Optional prefix to be attached to the next dep version if '--deps.bump' set to 'override'. Supported values: '^' | '~' | '' (empty string as default). + --deps.pullTagsForPrerelease Optional flag to control using release tags for evaluating prerelease version bumping. This is almost always the correct option since semantic-release will be creating tags for every dependency and it would lead to us bumping to a non-existent version. Set to false if you've already compensated for this in your workflow previously (true as default) --ignore-packages Packages list to be ignored on bumping process --ignore-private Exclude private packages. Enabled by default, pass 'no-ignore-private' to disable. --tag-format Format to use for creating tag names. Should include "name" and "version" vars. Default: "\${name}@\${version}" generates "package-name@1.0.0" @@ -59,6 +60,9 @@ const cli = meow( "deps.prefix": { type: "string", }, + "deps.pullTagsForPrerelease": { + type: "boolean", + }, ignorePrivate: { type: "boolean", }, diff --git a/lib/getConfigMultiSemrel.js b/lib/getConfigMultiSemrel.js index c042bcb..2a9881f 100644 --- a/lib/getConfigMultiSemrel.js +++ b/lib/getConfigMultiSemrel.js @@ -3,6 +3,28 @@ import { cosmiconfig } from "cosmiconfig"; import { pickBy, isNil, castArray, uniq } from "lodash-es"; import { createRequire } from "node:module"; +/** + * @typedef {Object} DepsConfig + * @property {'override' | 'satisfy' | 'inherit'} bump + * @property {'patch' | 'minor' | 'major' | 'inherit'} release + * @property {'^' | '~' | ''} prefix + * @property {boolean} pullTagsForPrerelease + */ + +/** + * @typedef {Object} MultiReleaseConfig + * @property {boolean} sequentialInit + * @property {boolean} sequentialPrepare + * @property {boolean} firstParent + * @property {boolean} debug + * @property {boolean} ignorePrivate + * @property {Array} ignorePackages + * @property {string} tagFormat + * @property {boolean} dryRun + * @property {DepsConfig} deps + * @property {boolean} silent + */ + const CONFIG_NAME = "multi-release"; const CONFIG_FILES = [ "package.json", @@ -36,7 +58,7 @@ const mergeConfig = (a = {}, b = {}) => { * * @param {string} cwd The directory to search. * @param {Object} cliOptions cli supplied options. - * @returns {Object} The found configuration option + * @returns {MultiReleaseConfig} The found configuration option * * @internal */ @@ -72,6 +94,7 @@ export default async function getConfig(cwd, cliOptions) { bump: "override", release: "patch", prefix: "", + pullTagsForPrerelease: false, }, silent: false, }, diff --git a/lib/multiSemanticRelease.js b/lib/multiSemanticRelease.js index 690da56..86c5c3e 100644 --- a/lib/multiSemanticRelease.js +++ b/lib/multiSemanticRelease.js @@ -35,6 +35,10 @@ import { createRequire } from "module"; * @param {Package[]} localDeps Array of local dependencies this package relies on. * @param {context|void} context The semantic-release context for this package's release (filled in once semantic-release runs). * @param {undefined|Result|false} result The result of semantic-release (object with lastRelease, nextRelease, commits, releases), false if this package was skipped (no changes or similar), or undefined if the package's release hasn't completed yet. + * @param {Object} options Aggregate of semantic-release options for the package + * @param {boolean} pullTagsForPrerelease if set to true, the package will use tags to determine if its dependencies need to change (legacy functionality) + * @param {Object} _lastRelease The last release object for the package before its current release (set during anaylze-commit) + * @param {Object} _nextRelease The next release object (the release the package is releasing for this cycle) (set during generateNotes) */ /** @@ -83,7 +87,15 @@ async function multiSemanticRelease( // Vars. const globalOptions = await getConfig(cwd); - const multiContext = { globalOptions, inputOptions, cwd, env, stdout, stderr }; + const multiContext = { + globalOptions, + inputOptions, + cwd, + env, + stdout, + stderr, + pullTagsForPrerelease: flags.deps.pullTagsForPrerelease, + }; const { queue, packages: _packages } = await topo({ cwd, workspacesExtra: Array.isArray(flags.ignorePackages) ? flags.ignorePackages.map((p) => `!${p}`) : [], @@ -141,7 +153,7 @@ async function multiSemanticRelease( * * @internal */ -async function getPackage(path, { globalOptions, inputOptions, env, cwd, stdout, stderr }) { +async function getPackage(path, { globalOptions, inputOptions, env, cwd, stdout, stderr, pullTagsForPrerelease }) { // Make path absolute. path = cleanPath(path, cwd); const dir = dirname(path); @@ -173,7 +185,17 @@ async function getPackage(path, { globalOptions, inputOptions, env, cwd, stdout, const { options, plugins } = await getConfigSemantic({ cwd: dir, env, stdout, stderr }, finalOptions); // Return package object. - return { path, dir, name, manifest, deps, options, plugins, fakeLogger: fakeLogger }; + return { + path, + dir, + name, + manifest, + deps, + options, + plugins, + fakeLogger: fakeLogger, + pullTagsForPrerelease: !!pullTagsForPrerelease, + }; } /** @@ -239,7 +261,9 @@ async function releasePackage(pkg, createInlinePlugin, multiContext, flags) { function normalizeFlags(_flags) { return { - deps: {}, + deps: { + pullTagsForPrerelease: !!_flags.deps?.pullTagsForPrerelease, + }, ..._flags, }; } diff --git a/lib/updateDeps.js b/lib/updateDeps.js index 3628651..8782271 100644 --- a/lib/updateDeps.js +++ b/lib/updateDeps.js @@ -44,6 +44,13 @@ const getVersionFromTag = (pkg, tag) => { /** * Resolve next package version on prereleases. * + * Will resolve highest next version of either: + * + * 1. The last release for the package during this multi-release cycle + * 2. (if the package has pullTagsForPrerelease true): + * a. the highest increment of the tags array provided + * b. the highest increment of the gitTags for the prerelease + * * @param {Package} pkg Package object. * @param {Array} tags Override list of tags from specific pkg and branch. * @returns {string|undefined} Next pkg version. @@ -51,7 +58,12 @@ const getVersionFromTag = (pkg, tag) => { */ const getNextPreVersion = (pkg, tags) => { const tagFilters = [pkg._preRelease]; - const lastVersion = pkg._lastRelease && pkg._lastRelease.version; + // Note: this is only set is a current multi-semantic-release released + const lastVersionForCurrentRelease = pkg._lastRelease && pkg._lastRelease.version; + + if (!pkg.pullTagsForPrerelease && tags) { + throw new Error("Supplied tags for NextPreVersion but the package does not use tags for next prerelease"); + } // Extract tags: // 1. Set filter to extract only package tags @@ -59,9 +71,9 @@ const getNextPreVersion = (pkg, tags) => { // 3. Resolve the versions from the tags // TODO: replace {cwd: '.'} with multiContext.cwd if (pkg.name) tagFilters.push(pkg.name); - if (!tags) { + if (!tags && pkg.pullTagsForPrerelease) { try { - tags = getTags(pkg._branch, { cwd: process.cwd() }, tagFilters); + tags = getTags(pkg._branch, { cwd: pkg.dir }, tagFilters); } catch (e) { tags = []; console.warn(e); @@ -69,15 +81,15 @@ const getNextPreVersion = (pkg, tags) => { } } - const lastPreRelTag = getPreReleaseTag(lastVersion); + const lastPreRelTag = getPreReleaseTag(lastVersionForCurrentRelease); const isNewPreRelTag = lastPreRelTag && lastPreRelTag !== pkg._preRelease; const versionToSet = - isNewPreRelTag || !lastVersion + isNewPreRelTag || !lastVersionForCurrentRelease ? `1.0.0-${pkg._preRelease}.1` : _nextPreVersionCases( - tags.map((tag) => getVersionFromTag(pkg, tag)).filter((tag) => tag), - lastVersion, + tags ? tags.map((tag) => getVersionFromTag(pkg, tag)).filter((tag) => tag) : [], + lastVersionForCurrentRelease, pkg._nextType, pkg._preRelease ); @@ -101,23 +113,23 @@ const getPreReleaseTag = (version) => { /** * Resolve next prerelease special cases: highest version from tags or major/minor/patch. * - * @param {Array} tags List of all released tags from package. - * @param {string} lastVersion Last package version released. + * @param {Array} tags - if non-empty, we will use these tags as part fo the comparison + * @param {string} lastVersionForCurrentMultiRelease Last package version released from multi-semantic-release * @param {string} pkgNextType Next type evaluated for the next package type. * @param {string} pkgPreRelease Package prerelease suffix. * @returns {string|undefined} Next pkg version. * @internal */ -const _nextPreVersionCases = (tags, lastVersion, pkgNextType, pkgPreRelease) => { +const _nextPreVersionCases = (tags, lastVersionForCurrentMultiRelease, pkgNextType, pkgPreRelease) => { // Case 1: Normal release on last version and is now converted to a prerelease - if (!semver.prerelease(lastVersion)) { - const { major, minor, patch } = semver.parse(lastVersion); + if (!semver.prerelease(lastVersionForCurrentMultiRelease)) { + const { major, minor, patch } = semver.parse(lastVersionForCurrentMultiRelease); return `${semver.inc(`${major}.${minor}.${patch}`, pkgNextType || "patch")}-${pkgPreRelease}.1`; } // Case 2: Validates version with tags const latestTag = getLatestVersion(tags, { withPrerelease: true }); - return _nextPreHighestVersion(latestTag, lastVersion, pkgPreRelease); + return _nextPreHighestVersion(latestTag, lastVersionForCurrentMultiRelease, pkgPreRelease); }; /** diff --git a/package.json b/package.json index 84882bb..a785d24 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint": "eslint ./", "lint:fix": "eslint --fix ./", "test": "yarn lint && yarn test:unit", - "test:unit": "NODE_OPTIONS=--experimental-vm-modules ./node_modules/.bin/jest --coverage", + "test:unit": "NODE_OPTIONS=\"${NODE_OPTIONS} --experimental-vm-modules\" ./node_modules/.bin/jest --coverage", "build": "echo 'There is no need for build' && exit 0", "postupdate": "yarn && npx yarn-audit-fix && yarn build && yarn test", "publish:beta": "npm publish --no-git-tag-version --tag beta" diff --git a/test/helpers/file.js b/test/helpers/file.js index bc117aa..fe1fc5d 100644 --- a/test/helpers/file.js +++ b/test/helpers/file.js @@ -1,4 +1,4 @@ -import { basename, join } from "path"; +import { basename, join, resolve } from "path"; import { copyFileSync, existsSync, mkdirSync, lstatSync, readdirSync, readFileSync, writeFileSync } from "fs"; // Deep copy a directory. @@ -44,5 +44,12 @@ function createNewTestingFiles(folders, cwd) { }); } +function addPrereleaseToPackageRootConfig(rootDir, releaseBranch) { + const packageUri = resolve(join(rootDir, "package.json")); + const packageJson = JSON.parse(readFileSync(packageUri).toString()); + + packageJson.release.branches = ["master", { name: releaseBranch, prerelease: true }]; +} + // Exports. -export { copyDirectory, isDirectory, createNewTestingFiles }; +export { copyDirectory, isDirectory, createNewTestingFiles, addPrereleaseToPackageRootConfig }; diff --git a/test/lib/multiSemanticRelease.test.js b/test/lib/multiSemanticRelease.test.js index 2454f74..b430ef9 100644 --- a/test/lib/multiSemanticRelease.test.js +++ b/test/lib/multiSemanticRelease.test.js @@ -1,9 +1,9 @@ -import { writeFileSync } from "fs"; +import { readFileSync, writeFileSync } from "fs"; import { createRequire } from "module"; import { jest } from "@jest/globals"; import { WritableStreamBuffer } from "stream-buffers"; -import { copyDirectory, createNewTestingFiles } from "../helpers/file.js"; +import { addPrereleaseToPackageRootConfig, copyDirectory, createNewTestingFiles } from "../helpers/file.js"; import { gitInit, gitAdd, gitCommit, gitCommitAll, gitInitOrigin, gitPush, gitTag, gitGetLog } from "../helpers/git"; import multiSemanticRelease from "../../lib/multiSemanticRelease.js"; @@ -784,6 +784,571 @@ describe("multiSemanticRelease()", () => { }); }); + // Bug state that we need to ensure doesn't happen again + test("Changes in some packages with correct prerelease bumping from stable", async () => { + const preReleaseBranch = "alpha"; + // Create Git repo. + const cwd = gitInit(preReleaseBranch); + // Initial commit. + copyDirectory(`test/fixtures/yarnWorkspaces/`, cwd); + addPrereleaseToPackageRootConfig(cwd, preReleaseBranch); + + const sha1 = gitCommitAll(cwd, "feat: Initial release"); + gitTag(cwd, "msr-test-a@1.0.0"); + gitTag(cwd, "msr-test-b@1.0.0"); + gitTag(cwd, "msr-test-c@1.0.0"); + gitTag(cwd, "msr-test-d@1.0.0"); + // Second commit. + writeFileSync(`${cwd}/packages/a/aaa.txt`, "AAA"); + const sha2 = gitCommitAll(cwd, "feat(aaa): Add missing text file"); + const url = gitInitOrigin(cwd); + gitPush(cwd); + + // Capture output. + const stdout = new WritableStreamBuffer(); + const stderr = new WritableStreamBuffer(); + + // Call multiSemanticRelease() + // Doesn't include plugins that actually publish. + const result = await multiSemanticRelease( + [ + `packages/d/package.json`, + `packages/b/package.json`, + `packages/a/package.json`, + `packages/c/package.json`, + ], + {}, + { cwd, stdout, stderr, env }, + { deps: {}, dryRun: false } + ); + + // Get stdout and stderr output. + const err = stderr.getContentsAsString("utf8"); + expect(err).toBe(false); + const out = stdout.getContentsAsString("utf8"); + expect(out).toMatch("Started multirelease! Loading 4 packages..."); + expect(out).toMatch("Loaded package msr-test-a"); + expect(out).toMatch("Loaded package msr-test-b"); + expect(out).toMatch("Loaded package msr-test-c"); + expect(out).toMatch("Loaded package msr-test-d"); + expect(out).toMatch("Queued 4 packages! Starting release..."); + expect(out).toMatch(`Created tag msr-test-a@1.1.0-${preReleaseBranch}.1`); + expect(out).toMatch(`Created tag msr-test-b@1.0.1-${preReleaseBranch}.1`); + expect(out).toMatch(`Created tag msr-test-c@1.0.1-${preReleaseBranch}.1`); + expect(out).toMatch("There are no relevant changes, so no new version is released"); + expect(out).toMatch("Released 3 of 4 packages, semantically!"); + + // A. + expect(result[0].name).toBe("msr-test-a"); + expect(result[0].result.lastRelease).toMatchObject({ + gitHead: sha1, + gitTag: "msr-test-a@1.0.0", + version: "1.0.0", + }); + expect(result[0].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-a@1.1.0-${preReleaseBranch}.1`, + type: "minor", + version: `1.1.0-${preReleaseBranch}.1`, + }); + expect(result[0].result.nextRelease.notes).toMatch(`# msr-test-a [1.1.0-${preReleaseBranch}.1]`); + expect(result[0].result.nextRelease.notes).toMatch("### Features\n\n* **aaa:** Add missing text file"); + // expect(result[3].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-c:** upgraded to 1.0.1"); + + // B. + expect(result[2].name).toBe("msr-test-b"); + expect(result[2].result.lastRelease).toEqual({ + channels: [null], + gitHead: sha1, + gitTag: "msr-test-b@1.0.0", + name: "msr-test-b@1.0.0", + version: "1.0.0", + }); + expect(result[2].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-b@1.0.1-${preReleaseBranch}.1`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result[2].result.nextRelease.notes).toMatch(`# msr-test-b [1.0.1-${preReleaseBranch}.1]`); + expect(result[2].result.nextRelease.notes).not.toMatch("### Features"); + expect(result[2].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result[2].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-a:** upgraded to 1.1.0"); + + // C. + expect(result[3].name).toBe("msr-test-c"); + expect(result[3].result.lastRelease).toEqual({ + channels: [null], + gitHead: sha1, + gitTag: "msr-test-c@1.0.0", + name: "msr-test-c@1.0.0", + version: "1.0.0", + }); + expect(result[3].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-c@1.0.1-${preReleaseBranch}.1`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result[3].result.nextRelease.notes).toMatch(`# msr-test-c [1.0.1-${preReleaseBranch}.1]`); + expect(result[3].result.nextRelease.notes).not.toMatch("### Features"); + expect(result[3].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result[3].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-b:** upgraded to 1.0.1"); + + // D. + expect(result[1].name).toBe("msr-test-d"); + expect(result[1].result).toBe(false); + + // ONLY four times. + expect(result[4]).toBe(undefined); + + // Check manifests. + expect(require(`${cwd}/packages/a/package.json`)).toMatchObject({ + peerDependencies: { + "left-pad": "latest", + }, + }); + expect(require(`${cwd}/packages/b/package.json`)).toMatchObject({ + dependencies: { + "msr-test-a": `1.1.0-${preReleaseBranch}.1`, + }, + devDependencies: { + "msr-test-d": "1.0.0", + "left-pad": "latest", + }, + }); + expect(require(`${cwd}/packages/c/package.json`)).toMatchObject({ + devDependencies: { + "msr-test-b": `1.0.1-${preReleaseBranch}.1`, + "msr-test-d": "1.0.0", + }, + }); + + // Commit this like the git plugin would (with a skippable syntax) + gitCommitAll(cwd, "docs(release): Release everything"); + + // Release a second time to verify prerelease incrementation + writeFileSync(`${cwd}/packages/a/bbb.txt`, "BBB"); + const sha3 = gitCommitAll(cwd, "feat(bbb): Add missing text file"); + gitPush(cwd); + + // Capture output. + const stdout2 = new WritableStreamBuffer(); + const stderr2 = new WritableStreamBuffer(); + + // NOTE: we call this again because we want to verify semantic-release + // channel tagging instead of simulating + // Call multiSemanticRelease() + // Doesn't include plugins that actually publish. + const result2 = await multiSemanticRelease( + [ + `packages/d/package.json`, + `packages/b/package.json`, + `packages/a/package.json`, + `packages/c/package.json`, + ], + {}, + { cwd, stdout: stdout2, stderr: stderr2, env }, + { deps: {}, dryRun: false } + ); + + // Get stdout and stderr output. + const err2 = stderr2.getContentsAsString("utf8"); + expect(err2).toBe(false); + const out2 = stdout2.getContentsAsString("utf8"); + expect(out2).toMatch("Started multirelease! Loading 4 packages..."); + expect(out2).toMatch("Loaded package msr-test-a"); + expect(out2).toMatch("Loaded package msr-test-b"); + expect(out2).toMatch("Loaded package msr-test-c"); + expect(out2).toMatch("Loaded package msr-test-d"); + expect(out2).toMatch("Queued 4 packages! Starting release..."); + expect(out2).toMatch(`Created tag msr-test-a@1.1.0-${preReleaseBranch}.2`); + // Default behavior minor bumps + expect(out2).toMatch(`Created tag msr-test-b@1.0.1-${preReleaseBranch}.2`); + expect(out2).toMatch(`Created tag msr-test-c@1.0.1-${preReleaseBranch}.2`); + expect(out2).toMatch("There are no relevant changes, so no new version is released"); + expect(out2).toMatch("Released 3 of 4 packages, semantically!"); + + // A. + expect(result2[0].name).toBe("msr-test-a"); + expect(result2[0].result.lastRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-a@1.1.0-${preReleaseBranch}.1`, + version: `1.1.0-${preReleaseBranch}.1`, + }); + expect(result2[0].result.nextRelease).toMatchObject({ + gitHead: sha3, + gitTag: `msr-test-a@1.1.0-${preReleaseBranch}.2`, + type: "minor", + version: `1.1.0-${preReleaseBranch}.2`, + }); + expect(result2[0].result.nextRelease.notes).toMatch(`# msr-test-a [1.1.0-${preReleaseBranch}.2]`); + expect(result2[0].result.nextRelease.notes).toMatch("### Features\n\n* **bbb:** Add missing text file"); + // expect(result[3].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-c:** upgraded to 1.0.1"); + + // B. + expect(result2[2].name).toBe("msr-test-b"); + expect(result2[2].result.lastRelease).toEqual({ + channels: [preReleaseBranch], + gitHead: sha2, + gitTag: `msr-test-b@1.0.1-${preReleaseBranch}.1`, + name: `msr-test-b@1.0.1-${preReleaseBranch}.1`, + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result2[2].result.nextRelease).toMatchObject({ + gitHead: sha3, + gitTag: `msr-test-b@1.0.1-${preReleaseBranch}.2`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.2`, + }); + expect(result2[2].result.nextRelease.notes).toMatch(`# msr-test-b [1.0.1-${preReleaseBranch}.2]`); + expect(result2[2].result.nextRelease.notes).not.toMatch("### Features"); + expect(result2[2].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result2[2].result.nextRelease.notes).toMatch( + `### Dependencies\n\n* **msr-test-a:** upgraded to 1.1.0-${preReleaseBranch}.2` + ); + + // C. + expect(result2[3].name).toBe("msr-test-c"); + expect(result2[3].result.lastRelease).toEqual({ + channels: [preReleaseBranch], + gitHead: sha2, + gitTag: `msr-test-c@1.0.1-${preReleaseBranch}.1`, + name: `msr-test-c@1.0.1-${preReleaseBranch}.1`, + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result2[3].result.nextRelease).toMatchObject({ + gitHead: sha3, + gitTag: `msr-test-c@1.0.1-${preReleaseBranch}.2`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.2`, + }); + expect(result2[3].result.nextRelease.notes).toMatch(`# msr-test-c [1.0.1-${preReleaseBranch}.2]`); + expect(result2[3].result.nextRelease.notes).not.toMatch("### Features"); + expect(result2[3].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result2[3].result.nextRelease.notes).toMatch( + `### Dependencies\n\n* **msr-test-b:** upgraded to 1.0.1-${preReleaseBranch}.2` + ); + + // D. + expect(result2[1].name).toBe("msr-test-d"); + expect(result2[1].result).toBe(false); + + // ONLY four times. + expect(result2[4]).toBe(undefined); + + // Check manifests. + expect(JSON.parse(readFileSync(`${cwd}/packages/a/package.json`).toString())).toMatchObject({ + peerDependencies: { + "left-pad": "latest", + }, + }); + expect(JSON.parse(readFileSync(`${cwd}/packages/b/package.json`).toString())).toMatchObject({ + dependencies: { + "msr-test-a": `1.1.0-${preReleaseBranch}.2`, + }, + devDependencies: { + "msr-test-d": "1.0.0", + "left-pad": "latest", + }, + }); + expect(JSON.parse(readFileSync(`${cwd}/packages/c/package.json`).toString())).toMatchObject({ + devDependencies: { + "msr-test-b": `1.0.1-${preReleaseBranch}.2`, + "msr-test-d": "1.0.0", + }, + }); + }); + + // Bug state that we want to keep for now in case of other people who have triaged it + test("Changes in some packages with bugged prerelease bumping (pullTagsForPrerelease: true)", async () => { + const preReleaseBranch = "alpha"; + // Create Git repo. + const cwd = gitInit(preReleaseBranch); + // Initial commit. + copyDirectory(`test/fixtures/yarnWorkspaces/`, cwd); + addPrereleaseToPackageRootConfig(cwd, preReleaseBranch); + + const sha1 = gitCommitAll(cwd, "feat: Initial release"); + gitTag(cwd, "msr-test-a@1.0.0"); + gitTag(cwd, "msr-test-b@1.0.0"); + gitTag(cwd, "msr-test-c@1.0.0"); + gitTag(cwd, "msr-test-d@1.0.0"); + // Second commit. + writeFileSync(`${cwd}/packages/a/aaa.txt`, "AAA"); + const sha2 = gitCommitAll(cwd, "feat(aaa): Add missing text file"); + const url = gitInitOrigin(cwd); + gitPush(cwd); + + // Capture output. + const stdout = new WritableStreamBuffer(); + const stderr = new WritableStreamBuffer(); + + // Call multiSemanticRelease() + // Doesn't include plugins that actually publish. + const result = await multiSemanticRelease( + [ + `packages/d/package.json`, + `packages/b/package.json`, + `packages/a/package.json`, + `packages/c/package.json`, + ], + {}, + { cwd, stdout, stderr, env }, + { + deps: { + pullTagsForPrerelease: true, + }, + dryRun: false, + } + ); + + // Get stdout and stderr output. + const err = stderr.getContentsAsString("utf8"); + expect(err).toBe(false); + const out = stdout.getContentsAsString("utf8"); + expect(out).toMatch("Started multirelease! Loading 4 packages..."); + expect(out).toMatch("Loaded package msr-test-a"); + expect(out).toMatch("Loaded package msr-test-b"); + expect(out).toMatch("Loaded package msr-test-c"); + expect(out).toMatch("Loaded package msr-test-d"); + expect(out).toMatch("Queued 4 packages! Starting release..."); + expect(out).toMatch(`Created tag msr-test-a@1.1.0-${preReleaseBranch}.1`); + expect(out).toMatch(`Created tag msr-test-b@1.0.1-${preReleaseBranch}.1`); + expect(out).toMatch(`Created tag msr-test-c@1.0.1-${preReleaseBranch}.1`); + expect(out).toMatch("There are no relevant changes, so no new version is released"); + expect(out).toMatch("Released 3 of 4 packages, semantically!"); + + // A. + expect(result[0].name).toBe("msr-test-a"); + expect(result[0].result.lastRelease).toMatchObject({ + gitHead: sha1, + gitTag: "msr-test-a@1.0.0", + version: "1.0.0", + }); + expect(result[0].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-a@1.1.0-${preReleaseBranch}.1`, + type: "minor", + version: `1.1.0-${preReleaseBranch}.1`, + }); + expect(result[0].result.nextRelease.notes).toMatch(`# msr-test-a [1.1.0-${preReleaseBranch}.1]`); + expect(result[0].result.nextRelease.notes).toMatch("### Features\n\n* **aaa:** Add missing text file"); + // expect(result[3].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-c:** upgraded to 1.0.1"); + + // B. + expect(result[2].name).toBe("msr-test-b"); + expect(result[2].result.lastRelease).toEqual({ + channels: [null], + gitHead: sha1, + gitTag: "msr-test-b@1.0.0", + name: "msr-test-b@1.0.0", + version: "1.0.0", + }); + expect(result[2].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-b@1.0.1-${preReleaseBranch}.1`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result[2].result.nextRelease.notes).toMatch(`# msr-test-b [1.0.1-${preReleaseBranch}.1]`); + expect(result[2].result.nextRelease.notes).not.toMatch("### Features"); + expect(result[2].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result[2].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-a:** upgraded to 1.1.0"); + + // C. + expect(result[3].name).toBe("msr-test-c"); + expect(result[3].result.lastRelease).toEqual({ + channels: [null], + gitHead: sha1, + gitTag: "msr-test-c@1.0.0", + name: "msr-test-c@1.0.0", + version: "1.0.0", + }); + expect(result[3].result.nextRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-c@1.0.1-${preReleaseBranch}.1`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result[3].result.nextRelease.notes).toMatch(`# msr-test-c [1.0.1-${preReleaseBranch}.1]`); + expect(result[3].result.nextRelease.notes).not.toMatch("### Features"); + expect(result[3].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result[3].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-b:** upgraded to 1.0.1"); + + // D. + expect(result[1].name).toBe("msr-test-d"); + expect(result[1].result).toBe(false); + + // ONLY four times. + expect(result[4]).toBe(undefined); + + // Check manifests. + expect(require(`${cwd}/packages/a/package.json`)).toMatchObject({ + peerDependencies: { + "left-pad": "latest", + }, + }); + expect(require(`${cwd}/packages/b/package.json`)).toMatchObject({ + dependencies: { + "msr-test-a": `1.1.0-${preReleaseBranch}.1`, + }, + devDependencies: { + "msr-test-d": "1.0.0", + "left-pad": "latest", + }, + }); + expect(require(`${cwd}/packages/c/package.json`)).toMatchObject({ + devDependencies: { + "msr-test-b": `1.0.1-${preReleaseBranch}.1`, + "msr-test-d": "1.0.0", + }, + }); + + // Commit this like the git plugin would (with a skippable syntax) + gitCommitAll(cwd, "docs(release): Release everything"); + + // Release a second time to verify prerelease incrementation + writeFileSync(`${cwd}/packages/a/bbb.txt`, "BBB"); + const sha3 = gitCommitAll(cwd, "feat(bbb): Add missing text file"); + gitPush(cwd); + + // Capture output. + const stdout2 = new WritableStreamBuffer(); + const stderr2 = new WritableStreamBuffer(); + + // NOTE: we call this again because we want to verify semantic-release + // channel tagging instead of simulating + // Call multiSemanticRelease() + // Doesn't include plugins that actually publish. + const result2 = await multiSemanticRelease( + [ + `packages/d/package.json`, + `packages/b/package.json`, + `packages/a/package.json`, + `packages/c/package.json`, + ], + {}, + { cwd, stdout: stdout2, stderr: stderr2, env }, + { + deps: { + pullTagsForPrerelease: true, + }, + dryRun: false, + } + ); + + // Get stdout and stderr output. + const err2 = stderr2.getContentsAsString("utf8"); + expect(err2).toBe(false); + const out2 = stdout2.getContentsAsString("utf8"); + expect(out2).toMatch("Started multirelease! Loading 4 packages..."); + expect(out2).toMatch("Loaded package msr-test-a"); + expect(out2).toMatch("Loaded package msr-test-b"); + expect(out2).toMatch("Loaded package msr-test-c"); + expect(out2).toMatch("Loaded package msr-test-d"); + expect(out2).toMatch("Queued 4 packages! Starting release..."); + expect(out2).toMatch(`Created tag msr-test-a@1.1.0-${preReleaseBranch}.2`); + // Default behavior minor bumps + expect(out2).toMatch(`Created tag msr-test-b@1.0.1-${preReleaseBranch}.2`); + expect(out2).toMatch(`Created tag msr-test-c@1.0.1-${preReleaseBranch}.2`); + expect(out2).toMatch("There are no relevant changes, so no new version is released"); + expect(out2).toMatch("Released 3 of 4 packages, semantically!"); + + // A. + expect(result2[0].name).toBe("msr-test-a"); + expect(result2[0].result.lastRelease).toMatchObject({ + gitHead: sha2, + gitTag: `msr-test-a@1.1.0-${preReleaseBranch}.1`, + version: `1.1.0-${preReleaseBranch}.1`, + }); + expect(result2[0].result.nextRelease).toMatchObject({ + gitHead: sha3, + gitTag: `msr-test-a@1.1.0-${preReleaseBranch}.2`, + type: "minor", + version: `1.1.0-${preReleaseBranch}.2`, + }); + expect(result2[0].result.nextRelease.notes).toMatch(`# msr-test-a [1.1.0-${preReleaseBranch}.2]`); + expect(result2[0].result.nextRelease.notes).toMatch("### Features\n\n* **bbb:** Add missing text file"); + // expect(result[3].result.nextRelease.notes).toMatch("### Dependencies\n\n* **msr-test-c:** upgraded to 1.0.1"); + + // B. + expect(result2[2].name).toBe("msr-test-b"); + expect(result2[2].result.lastRelease).toEqual({ + channels: [preReleaseBranch], + gitHead: sha2, + gitTag: `msr-test-b@1.0.1-${preReleaseBranch}.1`, + name: `msr-test-b@1.0.1-${preReleaseBranch}.1`, + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result2[2].result.nextRelease).toMatchObject({ + gitHead: sha3, + gitTag: `msr-test-b@1.0.1-${preReleaseBranch}.2`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.2`, + }); + expect(result2[2].result.nextRelease.notes).toMatch(`# msr-test-b [1.0.1-${preReleaseBranch}.2]`); + expect(result2[2].result.nextRelease.notes).not.toMatch("### Features"); + expect(result2[2].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result2[2].result.nextRelease.notes).toMatch( + `### Dependencies\n\n* **msr-test-a:** upgraded to 1.1.0-${preReleaseBranch}.2` + ); + + // C. + expect(result2[3].name).toBe("msr-test-c"); + expect(result2[3].result.lastRelease).toEqual({ + channels: [preReleaseBranch], + gitHead: sha2, + gitTag: `msr-test-c@1.0.1-${preReleaseBranch}.1`, + name: `msr-test-c@1.0.1-${preReleaseBranch}.1`, + version: `1.0.1-${preReleaseBranch}.1`, + }); + expect(result2[3].result.nextRelease).toMatchObject({ + gitHead: sha3, + gitTag: `msr-test-c@1.0.1-${preReleaseBranch}.2`, + type: "patch", + version: `1.0.1-${preReleaseBranch}.2`, + }); + expect(result2[3].result.nextRelease.notes).toMatch(`# msr-test-c [1.0.1-${preReleaseBranch}.2]`); + expect(result2[3].result.nextRelease.notes).not.toMatch("### Features"); + expect(result2[3].result.nextRelease.notes).not.toMatch("### Bug Fixes"); + expect(result2[3].result.nextRelease.notes).toMatch( + `### Dependencies\n\n* **msr-test-b:** upgraded to 1.0.1-${preReleaseBranch}.2` + ); + + // D. + expect(result2[1].name).toBe("msr-test-d"); + expect(result2[1].result).toBe(false); + + // ONLY four times. + expect(result2[4]).toBe(undefined); + + const pkgA = JSON.parse(readFileSync(`${cwd}/packages/a/package.json`).toString()); + const pkgB = JSON.parse(readFileSync(`${cwd}/packages/b/package.json`).toString()); + const pkgC = JSON.parse(readFileSync(`${cwd}/packages/c/package.json`).toString()); + // Check manifests. (They have non-existent state) + expect(JSON.parse(readFileSync(`${cwd}/packages/a/package.json`).toString())).toMatchObject({ + peerDependencies: { + "left-pad": "latest", + }, + }); + expect(JSON.parse(readFileSync(`${cwd}/packages/b/package.json`).toString())).toMatchObject({ + dependencies: { + "msr-test-a": `1.1.0-${preReleaseBranch}.3`, + }, + devDependencies: { + "msr-test-d": "1.0.0", + "left-pad": "latest", + }, + }); + expect(JSON.parse(readFileSync(`${cwd}/packages/c/package.json`).toString())).toMatchObject({ + devDependencies: { + "msr-test-b": `1.0.1-${preReleaseBranch}.3`, + "msr-test-d": "1.0.0", + }, + }); + }); + test("Changes in child packages with sequentialPrepare", async () => { const mockPrepare = jest.fn(); // Create Git repo. diff --git a/test/lib/updateDeps.test.js b/test/lib/updateDeps.test.js index fa7e8c8..3ab2e98 100644 --- a/test/lib/updateDeps.test.js +++ b/test/lib/updateDeps.test.js @@ -1,11 +1,26 @@ -import { - resolveReleaseType, +import { beforeAll, beforeEach, jest } from "@jest/globals"; +jest.unstable_mockModule("../../lib/git.js", () => ({ + getTags: jest.fn(), +})); +let resolveReleaseType, resolveNextVersion, getNextVersion, getNextPreVersion, getPreReleaseTag, getVersionFromTag, -} from "../../lib/updateDeps.js"; + getTags; + +beforeAll(async () => { + ({ getTags } = await import("../../lib/git.js")); + ({ + resolveReleaseType, + resolveNextVersion, + getNextVersion, + getNextPreVersion, + getPreReleaseTag, + getVersionFromTag, + } = await import("../../lib/updateDeps.js")); +}); describe("resolveNextVersion()", () => { // prettier-ignore @@ -201,6 +216,9 @@ describe("getNextVersion()", () => { }); describe("getNextPreVersion()", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); // prettier-ignore const cases = [ [undefined, "patch", "rc", [], "1.0.0-rc.1"], @@ -226,10 +244,74 @@ describe("getNextPreVersion()", () => { _nextType: releaseType, _lastRelease: {version: lastVersion}, _preRelease: preRelease, - _branch: "master", - name: "testing-package" + _branch: "master", + name: "testing-package", + pullTagsForPrerelease: true, + }, + lastTags, + )).toBe(nextVersion); + }); + it(`${lastVersion} and ${releaseType} ${ + lastTags.length ? "with looked up branch tags " : "" + }gives ${nextVersion}`, () => { + getTags.mockImplementation(() => { + return lastTags; + }); + // prettier-ignore + expect(getNextPreVersion( + { + _nextType: releaseType, + _lastRelease: {version: lastVersion}, + _preRelease: preRelease, + _branch: "master", + name: "testing-package", + pullTagsForPrerelease: true, + }, + // No Tags array means we look them up + )).toBe(nextVersion); + expect(getTags).toHaveBeenCalledTimes(1); + }); + }); + it("does not allow tags if pullTagsForPrerelease = false", () => { + expect(() => + getNextPreVersion( + { + _nextType: "patch", + _lastRelease: { version: "1.0.0" }, + _preRelease: "dev", + _branch: "master", + name: "testing-package", + pullTagsForPrerelease: false, + }, + [] + ) + ).toThrowError("Supplied tags for NextPreVersion but the package does not use tags for next prerelease"); + }); + // Simulates us not using tags as criteria + + const noTagCases = [ + // prerelease channels just bump up the pre-release + ["1.0.0-rc.0", "minor", "rc", "1.0.0-rc.1"], + ["1.0.0-dev.0", "major", "dev", "1.0.0-dev.1"], + ["1.0.0-dev.0", "major", "dev", "1.0.0-dev.1"], + ["1.0.1-dev.0", "major", "dev", "1.0.1-dev.1"], + // main channels obey the release type + ["11.0.0", "major", "beta", "12.0.0-beta.1"], + ["1.0.0", "minor", "beta", "1.1.0-beta.1"], + ["1.0.0", "patch", "beta", "1.0.1-beta.1"], + ]; + noTagCases.forEach(([lastVersion, releaseType, preRelease, nextVersion]) => { + it(`${lastVersion} and ${releaseType} for channel ${preRelease} gives ${nextVersion} when pullTagsForPrerelease = false`, () => { + // prettier-ignore + expect(getNextPreVersion( + { + _nextType: releaseType, + _lastRelease: {version: lastVersion}, + _preRelease: preRelease, + _branch: "master", + name: "testing-package", + pullTagsForPrerelease: false, }, - lastTags )).toBe(nextVersion); }); });