Skip to content

Commit

Permalink
Merge pull request #14 from Updater/wrapped-plugins
Browse files Browse the repository at this point in the history
Wrapped plugins
  • Loading branch information
pmowrer authored Dec 29, 2017
2 parents 00f6ca4 + ae47456 commit 28625ed
Show file tree
Hide file tree
Showing 15 changed files with 223 additions and 146 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
after_success:
- npx semantic-release-github-pr
- "[[ $TRAVIS_PULL_REQUEST != 'false' ]] && npx semantic-release-github-pr --debug"
- npx semantic-release --debug
62 changes: 43 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,80 @@ Apply [`semantic-release`'s](https://github.com/semantic-release/semantic-releas
## Why
The default configuration of `semantic-release` assumes a one-to-one relationship between a Github repository and an `npm` package.

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

## How
Rather than attributing all commits to a single package, this plugin will automatically assign commits to packages based on the files that a commit touched.
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 within a package's root, it will be considered for that package's next release. Yes, this means a single commit could belong to multiple packages.
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>`.

## Configuration
This package is a complement to `semantic-release`. It is assumed the user is already fully familiar with that package and its workflow.

## Install
```bash
npm install -D semantic-release-monorepo
npm install -D semantic-release semantic-release-monorepo
```

## Usage
In `package.json`:
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-releaase-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
Run `semantic-release-monorepo` for each package in a monorepo managed by [`lerna`](https://github.com/lerna/lerna):
```bash
lerna exec --concurrency 1 -- npx semantic-release -e -semantic-release-monorepo
```

## 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`.

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": {
"analyzeCommits": "semantic-release-monorepo",
"generateNotes": "semantic-release-monorepo",
"getLastRelease": "semantic-release-monorepo",
"publish": ["@semantic-release/npm", "semantic-release-monorepo/github"]
"verifyConditions": ["@semantic-release/git"],
"monorepo": {
"analyzeCommits": {
"format": "atom"
},
"generateNotes": "myNotesGenerator",
"getLastRelease": "@semantic-release/git",
"publish": ["@semantic-release/npm", "@semantic-release/git"]
}
}
}
```
### Advanced
The set of `semantic-release-monorepo` plugins wrap the default `semantic-release` workflow, augmenting it to work with a monorepo.

## What each plugin does
All `semantic-release-monorepo` plugins wrap the default `semantic-release` workflow, augmenting it to work with a monorepo.

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

### generateNotes
#### 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).

* 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
#### 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).

### publish (Github)
#### publish
* Maps the `gitTag` field of `nextRelease` to use the [monorepo git tag format](#how).
12 changes: 0 additions & 12 deletions github.js

This file was deleted.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@
},
"license": "MIT",
"peerDependencies": {
"semantic-release": "^11.0.0"
"semantic-release": "^11.1.0"
},
"dependencies": {
"debug": "^3.1.0",
"execa": "^0.8.0",
"pkg-up": "^2.0.0",
"ramda": "^0.25.0",
"read-pkg": "^3.0.0"
"read-pkg": "^3.0.0",
"semantic-release-plugin-decorators": "^1.2.0"
},
"devDependencies": {
"husky": "^0.14.3",
"jest": "^21.2.1",
"lint-staged": "^6.0.0",
"prettier": "^1.9.2",
"semantic-release": "^11.0.0",
"semantic-release-github-pr": "^1.1.1"
"semantic-release": "^11.1.0",
"semantic-release-github-pr": "^2.0.0"
},
"lint-staged": {
"*.js": [
Expand Down
35 changes: 0 additions & 35 deletions src/analyze-commits.js

This file was deleted.

25 changes: 0 additions & 25 deletions src/generate-notes.js

This file was deleted.

26 changes: 0 additions & 26 deletions src/get-last-release.js

This file was deleted.

59 changes: 56 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,62 @@
const analyzeCommits = require('./analyze-commits');
const generateNotes = require('./generate-notes');
const getLastRelease = require('./get-last-release');
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 {
wrapPlugin,
wrapMultiPlugin,
} = require('semantic-release-plugin-decorators');

const {
mapNextReleaseVersion,
mapLastReleaseVersionToLastReleaseGitTag,
mapNextReleaseVersionToNextReleaseGitTag,
withOptionsTransforms,
} = require('./options-transforms');

const NAMESPACE = 'monorepo';

const analyzeCommits = wrapPlugin(
NAMESPACE,
'analyzeCommits',
withOnlyPackageCommits,
pluginDefinitions.analyzeCommits.default
);

const generateNotes = wrapPlugin(
NAMESPACE,
'generateNotes',
compose(
withOnlyPackageCommits,
withOptionsTransforms([
mapLastReleaseVersionToLastReleaseGitTag(versionToGitTag),
mapNextReleaseVersionToNextReleaseGitTag(versionToGitTag),
mapNextReleaseVersion(versionToGitTag),
])
),
pluginDefinitions.generateNotes.default
);

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

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

module.exports = {
analyzeCommits,
generateNotes,
getLastRelease,
publish,
};
35 changes: 33 additions & 2 deletions src/only-package-commits.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const debug = require('debug')('semantic-release:monorepo');
const { pipeP } = require('ramda');
const pkgUp = require('pkg-up');
const readPkg = require('read-pkg');
const debug = require('debug')('semantic-release:monorepo');

const { getCommitFiles, getGitRoot } = require('./git-utils');
const { mapCommits } = require('./options-transforms');

const getPackagePath = async () => {
const path = await pkgUp();
Expand Down Expand Up @@ -38,4 +41,32 @@ const onlyPackageCommits = async commits => {
});
};

module.exports = onlyPackageCommits;
// Async version of Ramda's `tap`
const tapA = fn => async x => {
await fn(x);
return x;
};

const logFilteredCommitCount = logger => async ({ commits }) => {
const { name } = await readPkg();

logger.log(
'Found %s commits for package %s since last release',
commits.length,
name
);
};

const withOnlyPackageCommits = plugin => async (pluginConfig, config) => {
const { logger } = config;

return plugin(
pluginConfig,
await pipeP(
mapCommits(onlyPackageCommits),
tapA(logFilteredCommitCount(logger))
)(config)
);
};

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

const commits = lensProp('commits');
Expand All @@ -7,7 +7,7 @@ const nextRelease = lensProp('nextRelease');
const gitTag = lensProp('gitTag');
const version = lensProp('version');

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

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

Expand All @@ -21,9 +21,17 @@ const mapNextReleaseVersionToNextReleaseGitTag = overFromA(
compose(nextRelease, version)
);

const withOptionsTransforms = transforms => plugin => async (
pluginConfig,
config
) => {
return plugin(pluginConfig, await composeP(...transforms)(config));
};

module.exports = {
filterCommits,
mapCommits,
mapNextReleaseVersion,
mapLastReleaseVersionToLastReleaseGitTag,
mapNextReleaseVersionToNextReleaseGitTag,
withOptionsTransforms,
};
Loading

0 comments on commit 28625ed

Please sign in to comment.