Skip to content

Commit

Permalink
Merge pull request #26 from Updater/semantic-release-13
Browse files Browse the repository at this point in the history
feat: add support for `semantic-release` version 13
  • Loading branch information
pmowrer authored Feb 11, 2018
2 parents a81ffff + ba0849f commit 4d0f756
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 1,147 deletions.
56 changes: 29 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,96 @@
# semantic-release-monorepo

[![Build Status](https://travis-ci.org/Updater/semantic-release-monorepo.svg?branch=master)](https://travis-ci.org/Updater/semantic-release-monorepo) [![npm](https://img.shields.io/npm/v/semantic-release-monorepo.svg)](https://www.npmjs.com/package/semantic-release-monorepo) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

Apply [`semantic-release`'s](https://github.com/semantic-release/semantic-release) automatic publishing to a monorepo.

## Why

The default configuration of `semantic-release` assumes a one-to-one relationship between a Github repository and an `npm` package.

This set of plugins allows using `semantic-release` with a single Github repository containing many `npm` packages.

## How

Instead of attributing all commits to a single package, commits are assigned to packages based on the files that a commit touched.

If a commit touched a file in or below a package's root, it will be considered for that package's next release. A single commit can belong to multiple packages.

A push may release multiple package versions. In order to avoid version collisions, git tags are namespaced using the given package's name: `<package-name>-<version>`.

## Install

```bash
npm install -D semantic-release semantic-release-monorepo @semantic-release/[email protected]
npm install -D semantic-release semantic-release-monorepo
```

### Peer dependency on @semantic-release/npm
The `@semantic-release/npm` package is a `semantic-release` plugin that provides functionality for releasing on `npm`. While it's technically optional, it covers a very likely use case and is applied by default by `semantic-release` (minor version dependency, `^2.0.0`).

Unfortunately, `@semantic-release/npm` version `2.6.1` and above is incompatible with `semantic-release-monorepo` ([discussion](https://github.com/semantic-release/npm/issues/36)). Until a solution is found, `semantic-release-monorepo` only supports the version range `2 - 2.6.0`.

## Usage

Run `semantic-release-monorepo` for the package in the current working directory:

```bash
npx semantic-release -e semantic-release-monorepo
```
It helps to think about `semantic-release-monorepo` as a variation on `semantic-release`'s default behavior, using the latter's plugin system to adapt it to work with a monorepo.

It helps to think about `semantic-release-monorepo` as a variation on `semantic-release`'s default behavior, using the latter's plugin system to adapt it to work with a monorepo.

### With Lerna

The monorepo management tool [`lerna`](https://github.com/lerna/lerna) can be used to run `semantic-release-monorepo` across all packages in a monorepo:

```bash
lerna exec --concurrency 1 -- npx --no-install semantic-release -e semantic-release-monorepo
```

Note that this requires installing `semantic-release` and `semantic-release-monorepo` for each package.

Alternatively, thanks to how [`npx's package resolution works`](https://github.com/zkat/npx#description), if the repository root is in `$PATH` (typically true on CI), `semantic-release` and `semantic-release-monorepo` can be installed in the repo root instead of for each package, likely saving both time and disk space.
Alternatively, thanks to how [`npx's package resolution works`](https://github.com/zkat/npx#description), if the repository root is in `$PATH` (typically true on CI), `semantic-release` and `semantic-release-monorepo` can be installed once in the repo root instead of in each individual package, likely saving both time and disk space.

## Configuration

The set of plugins in this package wrap other `semantic-release` plugins to modify their behavior. By default, the same plugin configuration as `semantic-release` is used, but any plugin configuration should be compatible.

### Release config
Plugins can be configured in the [release config](https://github.com/semantic-release/semantic-release#plugins), with one important caveat:

Due to limitations in how plugins may be composed ([discussion](https://github.com/semantic-release/semantic-release/issues/550)), `semantic-release-monorepo` must unfortunately "hard-code" the set of plugins it wraps: `analyze-commits`, `generateNotes`, `getLastRelease` and `publish`.
Plugins can be configured in the [release config](https://github.com/semantic-release/semantic-release/blob/caribou/docs/usage/configuration.md#configuration), with one important caveat:

Due to limitations in how plugins may be composed ([discussion](https://github.com/semantic-release/semantic-release/issues/550)), `semantic-release-monorepo` must unfortunately "hard-code" the set of plugins it wraps: `analyze-commits` and `generateNotes`.

Users may still want to define a custom versions of the plugin set, or want to pass options to the default versions. To work around this problem, set the desired plugin configuration under the `monorepo` property instead.
Users may still want to define a custom versions of the plugin set, or want to pass options to the default versions. To work around this problem, set the desired plugin configuration under the `monorepo` property instead.

#### Example of use with non-default set of plugins

`package.json`

```json
{
"release": {
"verifyConditions": ["@semantic-release/git"],
"monorepo": {
"analyzeCommits": {
"format": "atom"
},
"generateNotes": "myNotesGenerator",
"getLastRelease": "@semantic-release/git",
"publish": ["@semantic-release/npm", "@semantic-release/git"]
}
"generateNotes": "myNotesGenerator"
},
"publish": ["@semantic-release/npm", "@semantic-release/git"],
"verifyConditions": ["@semantic-release/git"]
}
}
```

### Advanced

The set of `semantic-release-monorepo` plugins wrap the default `semantic-release` workflow, augmenting it to work with a monorepo.

#### analyzeCommits

* Filters the repo commits to only include those that touched files in the given monorepo package.

#### generateNotes
* Filters the repo commits to only include those that touched files in the given monorepo package.

* Maps the `gitTag` fields of `lastRelease` and `nextRelease` to use the [monorepo git tag format](#how).
* Filters the repo commits to only include those that touched files in the given monorepo package.

* Maps the `version` field of `nextRelease` to use the [monorepo git tag format](#how). The wrapped (default) `generateNotes` implementation uses `version` as the header for the release notes. Since all release notes end up in the same Github repository, using just the version as a header introduces ambiguity.

#### getLastRelease
Addresses multiple problems identifying the last release for a monorepo package:

1. The wrapped (default) `getLastRelease` plugin uses `gitHead` from the `npm` package metadata to identify the last release. However, `npm` doesn't publish `gitHead` as part of a package's metadata unless its `package.json` and the repo's `.git` are in the same folder (never true for a monorepo).
https://github.com/npm/read-package-json/issues/66#issuecomment-222036879

2. We can use `semantic-release`'s fallback strategy, searching for a git tag matching the latest `npm` version, but we must map the git tag to the [monorepo git tag format](#how).
#### tagFormat

#### publish
* Maps the `gitTag` field of `nextRelease` to use the [monorepo git tag format](#how).
Pre-configures the [`tagFormat` option](https://github.com/semantic-release/semantic-release/blob/caribou/docs/usage/configuration.md#tagformat) to use the [monorepo git tag format](#how).
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
},
"license": "MIT",
"peerDependencies": {
"@semantic-release/npm": "2 - 2.6.0",
"semantic-release": ">=11.1.0"
"semantic-release": "^13.1.0"
},
"dependencies": {
"debug": "^3.1.0",
Expand All @@ -27,8 +26,8 @@
"jest": "^21.2.1",
"lint-staged": "^6.0.0",
"prettier": "^1.9.2",
"semantic-release": "^11.1.0",
"semantic-release-github-pr": "^2.0.3"
"semantic-release": "^13.2.0",
"semantic-release-github-pr": "^3.1.2"
},
"lint-staged": {
"*.js": [
Expand Down
25 changes: 0 additions & 25 deletions src/git-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,7 @@ const getCommitFiles = pipeP(
*/
const getRoot = () => git(['rev-parse', '--show-toplevel']);

/**
* Get the commit sha for a given tag.
* https://github.com/semantic-release/semantic-release/blob/996305d69c36158f771bd20b6b416aa3461fb309/lib/git.js#L12
*
* @param {string} tagName Tag name for which to retrieve the commit sha.
* @return {string} The commit sha of the tag in parameter or `null`.
*/
const getTagHead = tagName =>
git(['rev-list', '-1', tagName], { reject: false });

/**
* Fetch tags from the repository's origin.
*/
const fetchTags = () => git(['fetch', '--tags']);

/**
* Unshallow the git repository (retrieving every commit and tags).
* Adapted from: https://github.com/semantic-release/npm/blob/cf039fdafda1a5ce43c2a5f033160cd46487f102/lib/git.js
*/
const unshallow = () =>
git(['fetch', '--unshallow', '--tags'], { reject: false });

module.exports = {
getCommitFiles,
getRoot,
getTagHead,
fetchTags,
unshallow,
};
30 changes: 3 additions & 27 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const readPkg = require('read-pkg');
const { compose } = require('ramda');
const pluginDefinitions = require('semantic-release/lib/plugins/definitions');
const withOnlyPackageCommits = require('./only-package-commits');
const versionToGitTag = require('./version-to-git-tag');
const withVersionHead = require('./with-version-head');
const logPluginVersion = require('./log-plugin-version');
const {
wrapPlugin,
Expand Down Expand Up @@ -31,37 +31,13 @@ const generateNotes = wrapPlugin(
compose(
logPluginVersion('generateNotes'),
withOnlyPackageCommits,
withOptionsTransforms([
mapLastReleaseVersionToLastReleaseGitTag(versionToGitTag),
mapNextReleaseVersionToNextReleaseGitTag(versionToGitTag),
mapNextReleaseVersion(versionToGitTag),
])
withOptionsTransforms([mapNextReleaseVersion(versionToGitTag)])
),
pluginDefinitions.generateNotes.default
);

const getLastRelease = wrapPlugin(
NAMESPACE,
'getLastRelease',
compose(logPluginVersion('getLastRelease'), withVersionHead),
pluginDefinitions.getLastRelease.default
);

const publish = wrapMultiPlugin(
NAMESPACE,
'publish',
compose(
logPluginVersion('publish'),
withOptionsTransforms([
mapNextReleaseVersionToNextReleaseGitTag(versionToGitTag),
])
),
pluginDefinitions.publish.default
);

module.exports = {
analyzeCommits,
generateNotes,
getLastRelease,
publish,
tagFormat: readPkg.sync().name + '-v${version}',
};
10 changes: 0 additions & 10 deletions src/lens-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ const overA = curry(async (lens, f, x) => {
return set(lens, value, x);
});

/**
* Specialization of `overA`, using another lens as the source of the
* data for the `over` transformation.
*/
const overFromA = curry(async (lens1, lens2, f, x) => {
const value = await f(view(lens2, x));
return set(lens1, value, x);
});

module.exports = {
overA,
overFromA,
};
16 changes: 1 addition & 15 deletions src/options-transforms.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
const { compose, composeP, lensProp } = require('ramda');
const { overA, overFromA } = require('./lens-utils');
const { overA } = require('./lens-utils');

const commits = lensProp('commits');
const lastRelease = lensProp('lastRelease');
const nextRelease = lensProp('nextRelease');
const gitTag = lensProp('gitTag');
const version = lensProp('version');

const mapCommits = fn => overA(commits, async commits => await fn(commits));

const mapNextReleaseVersion = overA(compose(nextRelease, version));

const mapLastReleaseVersionToLastReleaseGitTag = overFromA(
compose(lastRelease, gitTag),
compose(lastRelease, version)
);

const mapNextReleaseVersionToNextReleaseGitTag = overFromA(
compose(nextRelease, gitTag),
compose(nextRelease, version)
);

const withOptionsTransforms = transforms => plugin => async (
pluginConfig,
config
Expand All @@ -31,7 +19,5 @@ const withOptionsTransforms = transforms => plugin => async (
module.exports = {
mapCommits,
mapNextReleaseVersion,
mapLastReleaseVersionToLastReleaseGitTag,
mapNextReleaseVersionToNextReleaseGitTag,
withOptionsTransforms,
};
28 changes: 0 additions & 28 deletions src/options-transforms.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,4 @@ describe('semantic-release plugin options transforms', () => {
});
});
});

describe('#mapLastReleaseVersionToLastReleaseGitTag', () => {
it('maps the lastRelease.version option to lastRelease.gitTag', async () => {
const fn = mapLastReleaseVersionToLastReleaseGitTag(toTag);

expect(await fn(OPTIONS)).toEqual({
...OPTIONS,
lastRelease: {
gitTag: 'tag-1.2.3',
version: '1.2.3',
},
});
});
});

describe('#mapNextReleaseVersionToNextReleaseGitTag', () => {
it('maps the nextRelease.version option to nextRelease.gitTag', async () => {
const fn = mapNextReleaseVersionToNextReleaseGitTag(toTag);

expect(await fn(OPTIONS)).toEqual({
...OPTIONS,
nextRelease: {
gitTag: 'tag-4.5.6',
version: '4.5.6',
},
});
});
});
});
63 changes: 0 additions & 63 deletions src/with-version-head.js

This file was deleted.

Loading

0 comments on commit 4d0f756

Please sign in to comment.