Skip to content

Commit

Permalink
feat: provide ignore packages CLI directive
Browse files Browse the repository at this point in the history
  • Loading branch information
davikawasaki authored Mar 7, 2021
1 parent a034cb3 commit e77905d
Show file tree
Hide file tree
Showing 30 changed files with 817 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/node_modules/
**/node_modules/**
/coverage/
*.log
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ _But_ in multi-semantic-release this configuration can be done globally (in your

multi-semantic-release does not support any command line arguments (this wasn't possible without duplicating files from semantic-release, which I've tried to avoid).

Make sure to have a `workspaces` attribute inside your `package.json` project file. In there, you can set a list of packages that you might want to process in the msr process, as well as ignore others. For example, let's say your project has 4 packages (i.e. a, b, c and d) and you want to process only a and d (ignore b and c). You can set the following structure in your `package.json` file:
```json
{
"name": "msr-test-yarn",
"author": "Dave Houlbrooke <[email protected]",
"version": "0.0.0-semantically-released",
"private": true,
"license": "0BSD",
"engines": {
"node": ">=8.3"
},
"workspaces": [
"packages/*",
"!packages/b/**",
"!packages/c/**"
],
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator"
],
"noCi": true
}
}
```

## CLI
There are several tweaks to adapt **msr** to some corner cases:

Expand All @@ -43,6 +69,17 @@ There are several tweaks to adapt **msr** to some corner cases:
|`--deps.bump`|string| Define deps version update rule. `override` — replace any prev version with the next one, `satisfy` — check the next pkg version against its current references. If it matches (`*` matches to any, `1.1.0` matches `1.1.x`, `1.5.0` matches to `^1.0.0` and so on) release will not be triggered, if not `override` strategy will be applied instead; `inherit` will try to follow the current declaration version/range. `~1.0.0` + `minor` turns into `~1.1.0`, `1.x` + `major` gives `2.x`, but `1.x` + `minor` gives `1.x` so there will be no release, etc. + **Experimental feat** | `override`
|`--deps.release`|string| Define release type for dependent package if any of its deps changes. `patch`, `minor`, `major` — strictly declare the release type that occurs when any dependency is updated; `inherit` — applies the "highest" release of updated deps to the package. For example, if any dep has a breaking change, `major` release will be applied to the all dependants up the chain. **Experimental feat** | `patch`
|`--dry-run`|bool |Dry run mode| `false`
|`--ignore-packages`|string|Packages list to be ignored on bumping process (append to the ones that already exist at package.json workspaces)|`null`

Examples:

```
$ multi-semantic-release --debug
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
$ multi-semantic-release --ignore-packages=packages/a/**,packages/b/**
```

You can also combine the CLI `--ignore-packages` options with the `!` operator at each package inside `package.json.workspaces` attribute. Even though you can use the CLI to ignore options, you can't use it to set which packages to be released – i.e. you still need to set the `workspaces` attribute inside the `package.json`.

## API

Expand Down
12 changes: 11 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ const cli = meow(
--first-parent Apply commit filtering to current branch only.
--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.
--ignore-packages Packages' list to be ignored on bumping process
--help Help info.
Examples
$ multi-semantic-release --debug
$ multi-semantic-release --deps.bump=satisfy --deps.release=patch
$ multi-semantic-release --ignore-packages=packages/a/**,packages/b/**
`,
{
flags: {
Expand All @@ -40,13 +42,21 @@ const cli = meow(
type: "string",
default: "patch",
},
ignorePackages: {
type: "string",
},
dryRun: {
type: "boolean",
},
},
}
);

const processFlags = (flags) => toPairs(flags).reduce((m, [k, v]) => set(m, k, v), {});
const processFlags = (flags) => {
return toPairs(flags).reduce((m, [k, v]) => {
if (k === "ignorePackages" && v) return set(m, k, v.split(","));
return set(m, k, v);
}, {});
};

runner(processFlags(cli.flags));
2 changes: 1 addition & 1 deletion bin/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = (flags) => {
console.log(`flags: ${JSON.stringify(flags, null, 2)}`);

// Get list of package.json paths according to Yarn workspaces.
const paths = getWorkspacesYarn(cwd);
const paths = getWorkspacesYarn(cwd, flags.ignorePackages);
console.log("yarn paths", paths);

// Do multirelease (log out any errors).
Expand Down
1 change: 1 addition & 0 deletions lib/createInlinePluginCreator.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
const analyzeCommits = async (pluginOptions, context) => {
const firstParentBranch = flags.firstParent ? context.branch.name : undefined;
pkg._preRelease = context.branch.prerelease || null;
pkg._branch = context.branch.name;

// Filter commits by directory.
commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead, firstParentBranch);
Expand Down
10 changes: 7 additions & 3 deletions lib/getWorkspacesYarn.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ const { checker } = require("./blork");
* Return array of package.json for Yarn workspaces.
*
* @param {string} cwd The current working directory where a package.json file can be found.
* @param {string[]|null} ignorePackages (Optional) Packages to be ignored passed via cli.
* @returns {string[]} An array of package.json files corresponding to the workspaces setting in package.json
*/
function getWorkspacesYarn(cwd) {
function getWorkspacesYarn(cwd, ignorePackages = null) {
// Load package.json
const manifest = getManifest(`${cwd}/package.json`);

Expand All @@ -22,13 +23,16 @@ function getWorkspacesYarn(cwd) {
throw new TypeError("package.json: workspaces or workspaces.packages: Must be non-empty array of string");
}

// If packages to be ignored come from CLI, we need to combine them with the ones from manifest workspaces
if (Array.isArray(ignorePackages)) packages.push(...ignorePackages.map((p) => `!${p}`));

// Turn workspaces into list of package.json files.
const workspaces = glob(
packages.map((p) => p.replace(/\/?$/, "/package.json")),
{
cwd: cwd,
realpath: true,
ignore: "**/node_modules/**",
absolute: true,
gitignore: true,
}
);

Expand Down
30 changes: 30 additions & 0 deletions lib/git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const execa = require("execa");

/**
* Get all the tags for a given branch.
*
* @param {String} branch The branch for which to retrieve the tags.
* @param {Object} [execaOptions] Options to pass to `execa`.
* @param {Array<String>} filters List of prefixes/sufixes to be checked inside tags.
*
* @return {Array<String>} List of git tags.
* @throws {Error} If the `git` command fails.
* @internal
*/
function getTags(branch, execaOptions, filters) {
let tags = execa.sync("git", ["tag", "--merged", branch], execaOptions).stdout;
tags = tags
.split("\n")
.map((tag) => tag.trim())
.filter(Boolean);

if (!filters || !filters.length) return tags;

const validateSubstr = (t, f) => !!f.find((v) => t.includes(v));

return tags.filter((tag) => validateSubstr(tag, filters));
}

module.exports = {
getTags,
};
9 changes: 3 additions & 6 deletions lib/glob.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
const bashGlob = require("bash-glob");
const bashPath = require("bash-path");
const globby = require("globby");

module.exports = (...args) => {
if (!bashPath) {
throw new TypeError("`bash` must be installed"); // TODO move this check to bash-glob
}
const [pattern, ...options] = args;

return bashGlob.sync(...args);
return globby.sync(pattern, ...options);
};
147 changes: 144 additions & 3 deletions lib/updateDeps.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const { writeFileSync } = require("fs");
const recognizeFormat = require("./recognizeFormat");
const semver = require("semver");
const { isObject, isEqual, transform } = require("lodash");
const recognizeFormat = require("./recognizeFormat");
const getManifest = require("./getManifest");
const { getHighestVersion, getLatestVersion } = require("./utils");
const { getTags } = require("./git");
const debug = require("debug")("msr:updateDeps");

/**
* Resolve next package version.
Expand All @@ -17,17 +22,106 @@ const getNextVersion = (pkg) => {
: lastVersion || "1.0.0";
};

/**
* Resolve the package version from a tag
*
* @param {Package} pkg Package object.
* @param {string} tag The tag containing the version to resolve
* @returns {string} The version of the package
* @returns {string|null} The version of the package or null if no tag was passed
* @internal
*/
const getVersionFromTag = (pkg, tag) => {
if (!pkg.name) return tag || null;
if (!tag) return null;

const strMatch = tag.match(/[0-9].[0-9].[0-9].*/);
return strMatch && strMatch[0] && semver.valid(strMatch[0]) ? strMatch[0] : null;
};

/**
* Resolve next package version on prereleases.
*
* @param {Package} pkg Package object.
* @param {Array<string>} tags Override list of tags from specific pkg and branch.
* @returns {string|undefined} Next pkg version.
* @internal
*/
const getNextPreVersion = (pkg) => {
const getNextPreVersion = (pkg, tags) => {
const tagFilters = [pkg._preRelease];
const lastVersion = pkg._lastRelease && pkg._lastRelease.version;
// Extract tags:
// 1. Set filter to extract only package tags
// 2. Get tags from a branch considering the filters established
// 3. Resolve the versions from the tags
// TODO: replace {cwd: '.'} with multiContext.cwd
if (pkg.name) tagFilters.push(pkg.name);
if (!tags || !tags.length) {
tags = getTags(pkg._branch, { cwd: "." }, tagFilters);
}
const lastPreRelTag = getPreReleaseTag(lastVersion);
const isNewPreRelTag = lastPreRelTag && lastPreRelTag !== pkg._preRelease;
const versionToSet =
isNewPreRelTag || !lastVersion
? `1.0.0-${pkg._preRelease}.1`
: _nextPreVersionCases(
tags.map((tag) => getVersionFromTag(pkg, tag)).filter((tag) => tag),
lastVersion,
pkg._nextType,
pkg._preRelease
);
return versionToSet;
};

/**
* Parse the prerelease tag from a semver version.
*
* @param {string} version Semver version in a string format.
* @returns {string|null} preReleaseTag Version prerelease tag or null.
* @internal
*/
const getPreReleaseTag = (version) => {
const parsed = semver.parse(version);
if (!parsed) return null;
return parsed.prerelease[0] || null;
};

/**
* 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 {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) => {
// 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);
return `${semver.inc(`${major}.${minor}.${patch}`, pkgNextType || "patch")}-${pkgPreRelease}.1`;
}

return lastVersion ? semver.inc(lastVersion, "prerelease", pkg._preRelease) : `1.0.0-${pkg._preRelease}.1`;
// Case 2: Validates version with tags
const latestTag = getLatestVersion(tags, { withPrerelease: true });
return _nextPreHighestVersion(latestTag, lastVersion, pkgPreRelease);
};

/**
* Resolve next prerelease comparing bumped tags versions with last version.
*
* @param {string|null} latestTag Last released tag from branch or null if non-existent.
* @param {string} lastVersion Last version released.
* @param {string} pkgPreRelease Prerelease tag from package to-be-released.
* @returns {string} Next pkg version.
* @internal
*/
const _nextPreHighestVersion = (latestTag, lastVersion, pkgPreRelease) => {
const bumpFromTags = latestTag ? semver.inc(latestTag, "prerelease", pkgPreRelease) : null;
const bumpFromLast = semver.inc(lastVersion, "prerelease", pkgPreRelease);

return bumpFromTags ? getHighestVersion(bumpFromLast, bumpFromTags) : bumpFromLast;
};

/**
Expand Down Expand Up @@ -181,14 +275,61 @@ const updateManifestDeps = (pkg) => {
throw Error(`Cannot release because dependency ${d.name} has not been released`);
});

if (!auditManifestChanges(manifest, path)) {
return;
}

// Write package.json back out.
writeFileSync(path, JSON.stringify(manifest, null, indent) + trailingWhitespace);
};

// https://gist.github.com/Yimiprod/7ee176597fef230d1451
const difference = (object, base) =>
transform(object, (result, value, key) => {
if (!isEqual(value, base[key])) {
result[key] =
isObject(value) && isObject(base[key]) ? difference(value, base[key]) : `${base[key]}${value}`;
}
});

/**
* Clarify what exactly was changed in manifest file.
* @param {object} actualManifest manifest object
* @param {string} path manifest path
* @returns {boolean} has changed or not
* @internal
*/
const auditManifestChanges = (actualManifest, path) => {
const debugPrefix = `[${actualManifest.name}]`;
const oldManifest = getManifest(path);
const depScopes = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
const changes = depScopes.reduce((res, scope) => {
const diff = difference(actualManifest[scope], oldManifest[scope]);

if (Object.keys(diff).length) {
res[scope] = diff;
}

return res;
}, {});

debug(debugPrefix, "package.json path=", path);

if (Object.keys(changes).length) {
debug(debugPrefix, "changes=", changes);
return true;
}

debug(debugPrefix, "no deps changes");
return false;
};

module.exports = {
getNextVersion,
getNextPreVersion,
getPreReleaseTag,
updateManifestDeps,
resolveReleaseType,
resolveNextVersion,
getVersionFromTag,
};
Loading

0 comments on commit e77905d

Please sign in to comment.