Skip to content

Commit

Permalink
feat: make compatible with semantic-release plugin system
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This plugin no longer supports `semantic-release` versions below 16.
BREAKING CHANGE: The base branch configuration in now done through the `baseBranch`
option.
  • Loading branch information
pmowrer committed Jan 29, 2020
1 parent d900640 commit 885d03e
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 423 deletions.
77 changes: 9 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ Preview the semantic release notes that would result from merging a Github PR.

![image](https://user-images.githubusercontent.com/356320/33625928-257bc906-d9c7-11e7-9adb-de85726952eb.png)

This set of [`semantic-release`](https://github.com/semantic-release/semantic-release) plugins will post a Github PR comment with a preview of the release that would result from merging.
This [`semantic-release`](https://github.com/semantic-release/semantic-release) plugin will post a Github PR comment with a preview of the release that would result from merging.

## Install

```bash
npm install -D semantic-release@~15.9.x semantic-release-github-pr
npm install -D semantic-release semantic-release-github-pr
```

NOTE: The current version of this plugin only supports `semantic-release` versions 15.7.x-15.9.x. The next major version will support the current version of `semantic-release`.

## Usage

```bash
Expand All @@ -24,7 +22,7 @@ npx semantic-release-github-pr

It helps to think about `semantic-release-github-pr` as a variation on `semantic-release`'s default behavior, using the latter's plugin system to modify some behaviors:

* If a new release would result from running `semantic-release`, _instead of publishing a new release of a package to `npm`_, it posts a comment with the changelog to matching Github PRs.
* If a new release would result from running `semantic-release`, _instead of publishing a new release of a package_, it posts a comment with the changelog to matching Github PRs.

* It posts a static message when there's no release (for clarity).

Expand All @@ -34,54 +32,33 @@ It helps to think about `semantic-release-github-pr` as a variation on `semantic

A PR gets a comment if:

1. The PR's "from" branch matches the current branch (that this command is being run against).
1. The PR's _from_ branch matches the current branch (that this plugin is being run on).

To cover multiple CI scenarios ([see below](#ci)), either of:

1. The PR's [_test_ merge commit](https://developer.github.com/v3/pulls/#response-1) matches the current branch's `git` HEAD.
2. The PR and the current branch have the same `git` HEAD.

2. The PR's base branch matches `master` (default) unless the [`githubPr.branch` configuration option](https://github.com/semantic-release/semantic-release#Release-config) is set.
2. The PR's base branch matches `master` (default) unless otherwise configured via the `baseBranch` option.

## Configuration

It is assumed the user is already fully familiar with `semantic-release` and [`its workflow`](https://github.com/semantic-release/semantic-release#how-does-it-work).

### Github

Github authentication must be configured, exactly the same as for `semantic-relase`'s default [`@semantic-release/github`](https://github.com/semantic-release/github/#github-repository-authentication) plugin.
Github authentication must be configured, exactly the same as for `semantic-relase`'s default [`@semantic-release/github`](https://github.com/semantic-release/github/#github-authentication) plugin. PR comments will be posted by the associated GitHub user.

### Release config

It's possible to configure the expected base branch [when matching a PR](#which-prs-get-a-comment).

E.g., `package.json`:

```json
{
"release": {
"githubPr": {
"branch": "staging"
}
}
}
```

#### Advanced

Due to limitations in how plugins may be composed, `semantic-release-github-pr` must unfortunately hard-code the [`analyzeCommits`](#analyzecommits) and [`generateNotes`](#generatenotes) [plugins](https://github.com/semantic-release/semantic-release/blob/caribou/docs/usage/plugins.md) (see discussion [here](https://github.com/semantic-release/semantic-release/issues/550)).

Users may still want to define a custom versions of these plugins, or want to pass options to the default implementations. To work around this problem, set the desired configuration in the [release plugin config](https://github.com/semantic-release/semantic-release#plugins) inside the `githubPr` key instead.
It's possible to configure the expected base branch [when matching a PR](#which-prs-get-a-comment) via a `baseBranch` option set in the [release config](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration-file).

E.g., `package.json`:
E.g., in a `package.json` release config:

```json
{
"release": {
"githubPr": {
"analyzeCommits": "myCommitsAnalyzer",
"generateNotes": "myGenerateNotes"
}
"baseBranch": "staging"
}
}
```
Expand Down Expand Up @@ -109,39 +86,3 @@ Unfortunately, CircleCI only supports building on push, [not when a PR is create
post:
- "[[ $CI_PULL_REQUEST != '' ]] && npx semantic-release-github-pr || exit 0"
```

### Advanced

Running `semantic-release-github-pr` is equivalent to running `semantic-release` with the following [configuration](https://github.com/semantic-release/semantic-release/blob/caribou/docs/usage/configuration.md#configuration.) (as encapsulated in [the `semantic-release-github-pr` command](https://github.com/Updater/semantic-release-github-pr/blob/master/bin/semantic-release-github-pr.js)):

```json
{
"release": {
"verifyConditions": "@semantic-release/github",
"analyzeCommits": "semantic-release-github-pr",
"generateNotes": "semantic-release-github-pr"
}
}
```

### verifyConditions

The `@semantic-release/github` plugin is set as a default.

#### analyzeCommits

Used as a hook to clean up previous changelog PR comments made by the `generateNotes` plugin, keeping it from flooding a PR with (eventually stale) changelog comments over time.

If `semantic-release` determines that there's no new version, this plugin will also post a "no release" comment on a matching PR.

This plugin doesn't actually analyze commits to determine when to make a release, but defers to the plugin it decorates ([`@semantic-release/commit-analyzer`](https://github.com/semantic-release/commit-analyzer/) by default).

See the [`Release config`](#release-config) section for how to configure a custom `analyzeCommits` plugin and/or set options.

#### generateNotes

Creates a comment on matching PRs with the changelog for the release that would result from merging.

This plugin doesn't actually generate the changelog that ends up in the PR comment, but defers to the plugin it decorates ([`@semantic-release/release-notes-generator`](https://github.com/semantic-release/release-notes-generator) by default).

See the [`Release config`](#release-config) section for how to configure a custom `generateNotes` plugin and/or set options.
15 changes: 1 addition & 14 deletions bin/semantic-release-github-pr.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,7 @@ const { getCurrentBranchName } = require('../src/git-utils');
`--no-ci`,
// Set `dry-run` to keep `semantic-release` from publishing an actual release.
`--dry-run`,
`--branch=${branch}`,
// We hard-set our version of `analyze-commits (preventing accidental override)`.
`--analyze-commits=${plugins}`,

// TODO: Used to hard-set our version of `generateNotes` as well, but no
// longer seems possible after it became a "multi plugin" (array)
// configuration in `semantic-release` 15.7.0. Instead, it's soft-set set
// via the shareable config option below (`--extends`). It doesn't matter
// other than it allows the user to break the plugin by (inadvertently)
// overriding our version of `generateNotes`.

// We use `extends` here to pick up a soft-set default for `verifyConditions`,
// allowing users to override it (setting a plugin directly from the CLI
// trumps plugins read from a config file).
`--branches=${branch}`,
`--extends=${plugins}`,
]);

Expand Down
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"bin": "./bin/semantic-release-github-pr.js",
"license": "MIT",
"peerDependencies": {
"semantic-release": "^15.7.0-15.9.x"
"semantic-release": ">=16.0.0"
},
"dependencies": {
"debug": "^4.1.1",
Expand All @@ -25,15 +25,14 @@
"github": "^13.0.0",
"parse-github-url": "^1.0.2",
"ramda": "^0.26.1",
"read-pkg": "^5.2.0",
"semantic-release-plugin-decorators": "^2.0.0"
"read-pkg": "^5.2.0"
},
"devDependencies": {
"husky": "^4.2.1",
"jest": "^25.1.0",
"lint-staged": "^10.0.3",
"prettier": "^1.19.1",
"semantic-release": "15.9.x"
"semantic-release": "^17.0.0"
},
"husky": {
"hooks": {
Expand All @@ -45,4 +44,4 @@
"yarn format"
]
}
}
}
6 changes: 5 additions & 1 deletion src/create-changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ const HEADER = {
*/
const createChangelog = (
{ githubRepo, npmPackage: { name: npmPackageName } },
{ logger, nextRelease: { gitHead, gitTag = null, notes } }
{
logger,
envCi: { commit: gitHead },
nextRelease: { gitTag = null, notes } = {},
}
) => async ({ number, title }) => {
const body =
create(gitHead, npmPackageName, gitTag) +
Expand Down
2 changes: 1 addition & 1 deletion src/delete-changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const matchStaleComment = (
*/
const deleteStaleChangelogs = skipNoRelease => (
{ githubRepo, npmPackage: { name: npmPackageName } },
{ logger, nextRelease: { gitHead } }
{ logger, envCi: { commit: gitHead } }
) => async ({ number, title }) => {
const { data: comments } = await githubRepo.getIssueComments({ number });
const isStaleComment = matchStaleComment(
Expand Down
1 change: 0 additions & 1 deletion src/git-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const { pipeP, split } = require('ramda');
const execa = require('execa');

const git = async (args, options = {}) => {
Expand Down
100 changes: 59 additions & 41 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,86 +1,104 @@
const { compose } = require('ramda');
const {
wrapPlugin,
appendMultiPlugin,
} = require('semantic-release-plugin-decorators');
const pluginDefinitions = require('semantic-release/lib/definitions/plugins');

const { parse } = require('./comment-tag');
const createChangelog = require('./create-changelog');
const deleteStaleChangelogs = require('./delete-changelog');
const withGithub = require('./with-github');
const withGitHead = require('./with-git-head');
const withNpmPackage = require('./with-npm-package');
const withMatchingPullRequests = require('./with-matching-pull-requests');

const NAMESPACE = 'githubPr';

const decoratePlugin = compose(
withGithub,
withGitHead,
withMatchingPullRequests,
withNpmPackage
);

// Use `analyzeCommits` plugin as a hook to post a "no release" PR comment if
// there isn't a new release. We can't do this in `generateNotes` since it only runs
// if there's a new release.
const analyzeCommits = wrapPlugin(
NAMESPACE,
'analyzeCommits',
plugin => async (pluginConfig, context) => {
// Use the `analyzeCommits` step to post a "no release" PR comment when there
// isn't a new release (we can't do this in `generateNotes` since it only runs
// if there's a new release).
const analyzeCommits = async (pluginConfig, context, results) => {
return Promise.all(results).then(async results => {
const { githubRepo, pullRequests } = pluginConfig;
const nextRelease = await plugin(pluginConfig, context);
const hasNextRelease = results.some(result => !!result);

if (!nextRelease) {
if (!hasNextRelease) {
await pullRequests.forEach(async pr => {
const { number } = pr;
const createChangelogOnPr = createChangelog(pluginConfig, context);
const { data: comments } = await githubRepo.getIssueComments({
number,
});

// Create "no release" comment if there are no other comments posted
// by this set of plugins. We want to avoid duplicating the "no release"
// comment and/or posting it when another package has a release (monorepo).
// Create a "no release" comment.
// We only do this if there are no other comments posted by this plugin,
// avoiding duplication of the "no release" comment and/or incorrectly posting
// it when a different package in the same PR has a release (monorepo).
if (!comments.some(comment => !!parse(comment.body))) {
createChangelogOnPr(pr);
}
});
}

// Clean up stale changelog comments, possibly sparing the "no release"
// comment if this package doesn't have a new release.
// Clean up stale changelog comments from previous runs of this plugin.
await pullRequests.forEach(
deleteStaleChangelogs(!nextRelease)(pluginConfig, context)
deleteStaleChangelogs(!hasNextRelease)(pluginConfig, context)
);

return nextRelease;
},
pluginDefinitions.analyzeCommits.default
);
return;
});
};

// Append a plugin that generates PR comments from the release notes resulting
// from the configured `generateNotes` plugins that run ahead of it.
const generateNotes = appendMultiPlugin(
NAMESPACE,
'generateNotes',
decoratePlugin(async (pluginConfig, context) => {
// Use the "generateNotes" step to post a PR comments with the release notes
// generated for the pending release.
const generateNotes = async (pluginConfig, context, results) => {
return Promise.all(results).then(async results => {
const { pullRequests } = pluginConfig;
const { nextRelease } = context;

await pullRequests.forEach(
// Create "release" comment
// Create a "release" comment.
createChangelog(pluginConfig, context)
);

return nextRelease.notes;
}),
pluginDefinitions.generateNotes.default
);
});
};

const appendStep = (stepName, stepFn) => {
const results = [];

return Array(10)
.fill(null)
.map((value, index) => {
return async (pluginConfig, context) => {
const {
options: { plugins },
} = context;
const pluginName = plugins[index];

if (index === plugins.length) {
return stepFn(pluginConfig, context, results);
}

if (!pluginName) {
return '';
}

const plugin = require(pluginName);
const step = plugin && plugin[stepName];

if (!step) {
return '';
}

const result = step(pluginConfig, context);
results.push(result);
return result;
};
});
};

module.exports = {
verifyConditions: '@semantic-release/github',
analyzeCommits: decoratePlugin(analyzeCommits),
generateNotes,
analyzeCommits: appendStep('analyzeCommits', decoratePlugin(analyzeCommits)),
generateNotes: appendStep('generateNotes', decoratePlugin(generateNotes)),
};
5 changes: 3 additions & 2 deletions src/with-github.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const githubInit = require('./github-init');
const githubRepo = require('./github-repo');
const parseGithubUrl = require('parse-github-url');

const withGithub = plugin => (pluginConfig, context) => {
const withGithub = plugin => (...args) => {
const [pluginConfig, context] = args;
const github = githubInit(pluginConfig, context);
const {
options: { repositoryUrl },
Expand All @@ -14,7 +15,7 @@ const withGithub = plugin => (pluginConfig, context) => {
...pluginConfig,
githubRepo: githubRepo(github, { owner, repo }),
},
context
...args.slice(1)
);
};

Expand Down
Loading

0 comments on commit 885d03e

Please sign in to comment.