Skip to content

Commit

Permalink
Merge pull request #11 from Updater/refactor
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
pmowrer authored Dec 13, 2017
2 parents f34137f + d034bf6 commit 25c3b63
Show file tree
Hide file tree
Showing 19 changed files with 2,319 additions and 282 deletions.
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ This plugin allows using `semantic-release` with a single Github repository cont
## 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.

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

A push may release multiple package versions. In order to avoid version collisions, git tags are namespaced using the given package's name.
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.
Expand All @@ -31,8 +31,31 @@ In `package.json`:
"analyzeCommits": "semantic-release-monorepo",
"generateNotes": "semantic-release-monorepo",
"getLastRelease": "semantic-release-monorepo",
"publish": ["semantic-release-monorepo/npm", "semantic-release-monorepo/github"]
"publish": ["@semantic-release/npm", "semantic-release-monorepo/github"]
}
}
```

## What each plugin does
All `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).

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

### publish (Github)
* Maps the `gitTag` field of `nextRelease` to use the [monorepo git tag format](#how).
12 changes: 10 additions & 2 deletions github.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
const { publish } = require('@semantic-release/github');
const withGitTag = require('./src/with-git-tag');
const versionToGitTag = require('./src/version-to-git-tag');

module.exports = withGitTag(publish);
const {
mapNextReleaseVersionToNextReleaseGitTag,
} = require('./src/options-transforms');

module.exports = async (pluginConf, options) =>
publish(
pluginConf,
await mapNextReleaseVersionToNextReleaseGitTag(versionToGitTag)(options)
);
19 changes: 0 additions & 19 deletions index.js

This file was deleted.

4 changes: 0 additions & 4 deletions npm.js

This file was deleted.

17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
"name": "semantic-release-monorepo",
"version": "0.0.0-development",
"description": "Plugins for `semantic-release` allowing it to be used with a monorepo.",
"main": "index.js",
"main": "src/index.js",
"scripts": {
"test": "echo \"No test specified\" && exit 0"
"format": "prettier --write --single-quote --trailing-comma es5",
"format:all": "yarn format \"./**/*.js\"",
"precommit": "lint-staged",
"test": "jest"
},
"license": "MIT",
"peerDependencies": {
Expand All @@ -18,7 +21,17 @@
"read-pkg": "^3.0.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"
},
"lint-staged": {
"*.js": [
"yarn format",
"git add"
]
}
}
12 changes: 12 additions & 0 deletions src/analyze-commits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const commitAnalyzer = require('@semantic-release/commit-analyzer');
const { filterCommits } = require('./options-transforms');
const onlyPackageCommits = require('./only-package-commits');

async function analyzeCommits(pluginConf, options) {
return commitAnalyzer(
pluginConf,
await filterCommits(onlyPackageCommits)(options)
);
}

module.exports = analyzeCommits;
25 changes: 25 additions & 0 deletions src/generate-notes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { pipeP } = require('ramda');
const releaseNotesGenerator = require('@semantic-release/release-notes-generator');
const withPackageCommits = require('./only-package-commits');
const versionToGitTag = require('./version-to-git-tag');

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

async function generateNotes(pluginConf, options) {
return releaseNotesGenerator(
pluginConf,
await pipeP(
filterCommits(withPackageCommits),
mapLastReleaseVersionToLastReleaseGitTag(versionToGitTag),
mapNextReleaseVersionToNextReleaseGitTag(versionToGitTag),
mapNextReleaseVersion(versionToGitTag)
)(options)
);
}

module.exports = generateNotes;
4 changes: 2 additions & 2 deletions src/get-last-release.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { getLastRelease } = require('@semantic-release/npm');
const getVersionHead = require('semantic-release/lib/get-version-head');
const gitTag = require('./git-tag');
const gitTag = require('./version-to-git-tag');

module.exports = async (pluginConfig, options) => {
const result = await getLastRelease(pluginConfig, options);
Expand All @@ -18,7 +18,7 @@ module.exports = async (pluginConfig, options) => {
if (result && !result.gitHead) {
return {
...result,
...await getVersionHead(null, await gitTag(result.version))
...(await getVersionHead(null, await gitTag(result.version))),
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const analyzeCommits = require('./analyze-commits');
const generateNotes = require('./generate-notes');
const getLastRelease = require('./get-last-release');

module.exports = {
analyzeCommits,
generateNotes,
getLastRelease,
};
23 changes: 23 additions & 0 deletions src/lens-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const { curry, set, view } = require('ramda');

/**
* Async version of Ramda's `over` lens utility.
*/
const overA = curry(async (lens, f, x) => {
const value = await f(view(lens, 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,
};
41 changes: 41 additions & 0 deletions src/only-package-commits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const debug = require('debug')('semantic-release:monorepo');
const pkgUp = require('pkg-up');

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

const getPackagePath = async () => {
const path = await pkgUp();
const gitRoot = await getGitRoot();
return path.replace('package.json', '').replace(`${gitRoot}/`, '');
};

const withFiles = async commits => {
return Promise.all(
commits.map(async commit => {
const files = await getCommitFiles(commit.hash);
return { ...commit, files };
})
);
};

const onlyPackageCommits = async commits => {
const packagePath = await getPackagePath();
debug('Filter commits by package path: "%s"', packagePath);
const commitsWithFiles = await withFiles(commits);

return commitsWithFiles.filter(({ files, subject }) => {
const packageFile = files.find(path => path.indexOf(packagePath) === 0);

if (packageFile) {
debug(
'Including commit "%s" because it modified package file "%s".',
subject,
packageFile
);
}

return !!packageFile;
});
};

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

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

const filterCommits = 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)
);

module.exports = {
filterCommits,
mapNextReleaseVersion,
mapLastReleaseVersionToLastReleaseGitTag,
mapNextReleaseVersionToNextReleaseGitTag,
};
71 changes: 71 additions & 0 deletions src/options-transforms.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const {
filterCommits,
mapNextReleaseVersion,
mapLastReleaseVersionToLastReleaseGitTag,
mapNextReleaseVersionToNextReleaseGitTag,
} = require('./options-transforms');

const OPTIONS = {
commits: [1, 2, 3, 4],
lastRelease: {
version: '1.2.3',
},
nextRelease: {
version: '4.5.6',
},
};

const even = n => n % 2 === 0;
const toTag = x => `tag-${x}`;

describe('semantic-release plugin options transforms', () => {
describe('#filterCommits', () => {
it('allows transforming the "commits" option', async () => {
const fn = commits => commits.filter(even);

expect(await filterCommits(fn)(OPTIONS)).toEqual({
...OPTIONS,
commits: [2, 4],
});
});
});

describe('#mapNextReleaseVersion', () => {
it('maps the nextRelease.version option', async () => {
expect(await mapNextReleaseVersion(toTag)(OPTIONS)).toEqual({
...OPTIONS,
nextRelease: {
version: 'tag-4.5.6',
},
});
});
});

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',
},
});
});
});
});
6 changes: 0 additions & 6 deletions src/override-option.js

This file was deleted.

File renamed without changes.
9 changes: 0 additions & 9 deletions src/with-git-tag.js

This file was deleted.

Loading

0 comments on commit 25c3b63

Please sign in to comment.