Skip to content

Commit

Permalink
feat: add fail hook
Browse files Browse the repository at this point in the history
- Open or update a GitHub issue with informations about the errors that caused the release to fail
- Update the `success` hook to close the issue when a release is successful
  • Loading branch information
pvdlg committed Feb 12, 2018
1 parent 06979d8 commit 2367a71
Show file tree
Hide file tree
Showing 17 changed files with 1,101 additions and 127 deletions.
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ Publish a [GitHub release](https://help.github.com/articles/about-releases), opt

## success

Add a comment to each GitHub issue or pull request resolved by the release.
Add a comment to each GitHub issue or pull request resolved by the release and close issues previously open by the [fail](#fail) step.

## fail

Open or update a GitHub issue with informations about the errors that caused the release to fail.

## Configuration

Expand All @@ -42,12 +46,16 @@ Follow the [Creating a personal access token for the command line](https://help.

### Options

| Option | Description | Default |
|-----------------------|------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
| `successComment` | The comment added to each issue and pull request resolved by the release. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release](<github_release_url>)` |
| Option | Description | Default |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
| `successComment` | The comment added to each issue and pull request resolved by the release. See [successComment](#successcomment). | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release](<github_release_url>)` |
| `failComment` | The content of the issue created when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. |
| `failTitle` | The title of the issue created when a release fails. | `The automated release is failing :rotating_light:` |
| `labels` | The [labels](https://help.github.com/articles/about-labels) to add to the issue created when a release fails. | `['semantic-release']` |
| `assignees` | The [assignees](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users) to add to the issue created when a release fails. | - |

#### assets

Expand Down Expand Up @@ -102,6 +110,23 @@ The `successComment` `This ${issue.pull_request ? 'pull request' : 'issue'} is i

> This pull request is included in version 1.0.0
#### failComment

The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available:

| Parameter | Description |
|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `branch` | The branch from which the release had failed. |
| `errors` | An `Array` of [SemanticReleaseError](https://github.com/semantic-release/error). Each error has the `message`, `code`, `pluginName` and `details` properties.<br>`pluginName` contains the package name of the plugin that threw the error.<br>`details` contains a informations about the error formatted in markdown. |

##### failComment examples

The `failComment` `This release from branch ${branch} had failed due to the following errors:\n- ${errors.map(err => err.message).join('\\n- ')}` will generate the comment:

> This release from branch master had failed due to the following errors:
> - Error message 1
> - Error message 2
### Usage

The plugins are used by default by [Semantic-release](https://github.com/semantic-release/semantic-release) so no
Expand All @@ -114,7 +139,8 @@ Each individual plugin can be disabled, replaced or used with other plugins in t
"release": {
"verifyConditions": ["@semantic-release/github", "@semantic-release/npm", "verify-other-condition"],
"publish": ["@semantic-release/npm", "@semantic-release/github", "other-publish"],
"success": ["@semantic-release/github", "other-success"]
"success": ["@semantic-release/github", "other-success"],
"fail": ["@semantic-release/github", "other-fail"]
}
}
```
Expand Down
33 changes: 22 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
const verifyGitHub = require('./lib/verify');
const publishGitHub = require('./lib/publish');
const successGitHub = require('./lib/success');
const failGitHub = require('./lib/fail');

let verified;

async function verifyConditions(pluginConfig, context) {
const {options} = context;
// If the GitHub publish plugin is used and has `assets` configured, validate it now in order to prevent any release if the configuration is wrong
// If the GitHub publish plugin is used and has `assets`, `successComment`, `failComment`, `failTitle`, `labels` or `assignees` configured, validate it now in order to prevent any release if the configuration is wrong
if (options.publish) {
const publishPlugin = (Array.isArray(options.publish) ? options.publish : [options.publish]).find(
config => config.path && config.path === '@semantic-release/github'
);
if (publishPlugin && publishPlugin.assets) {
pluginConfig.assets = publishPlugin.assets;
}
if (publishPlugin && publishPlugin.successComment) {
pluginConfig.successComment = publishPlugin.successComment;
}
const publishPlugin =
(Array.isArray(options.publish) ? options.publish : [options.publish]).find(
config => config.path && config.path === '@semantic-release/github'
) || {};

pluginConfig.assets = pluginConfig.assets || publishPlugin.assets;
pluginConfig.successComment = pluginConfig.successComment || publishPlugin.successComment;
pluginConfig.failComment = pluginConfig.failComment || publishPlugin.failComment;
pluginConfig.failTitle = pluginConfig.failTitle || publishPlugin.failTitle;
pluginConfig.labels = pluginConfig.labels || publishPlugin.labels;
pluginConfig.assignees = pluginConfig.assignees || publishPlugin.assignees;
}

await verifyGitHub(pluginConfig, context);
Expand All @@ -39,4 +42,12 @@ async function success(pluginConfig, context) {
await successGitHub(pluginConfig, context);
}

module.exports = {verifyConditions, publish, success};
async function fail(pluginConfig, context) {
if (!verified) {
await verifyGitHub(pluginConfig, context);
verified = true;
}
await failGitHub(pluginConfig, context);
}

module.exports = {verifyConditions, publish, success, fail};
32 changes: 32 additions & 0 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@ Your configuration for the \`assets\` option is \`${stringify(assets)}\`.`,
)}) option, if defined, must be a non empty \`String\`.
Your configuration for the \`successComment\` option is \`${stringify(successComment)}\`.`,
}),
EINVALIDFAILTITLE: ({failTitle}) => ({
message: 'Invalid `failTitle` option.',
details: `The [failTitle option](${linkify(
'README.md#failtitle'
)}) option, if defined, must be a non empty \`String\`.
Your configuration for the \`failTitle\` option is \`${stringify(failTitle)}\`.`,
}),
EINVALIDFAILCOMMENT: ({failComment}) => ({
message: 'Invalid `failComment` option.',
details: `The [failComment option](${linkify(
'README.md#failcomment'
)}) option, if defined, must be a non empty \`String\`.
Your configuration for the \`failComment\` option is \`${stringify(failComment)}\`.`,
}),
EINVALIDLABELS: ({labels}) => ({
message: 'Invalid `labels` option.',
details: `The [labels option](${linkify(
'README.md#labels'
)}) option, if defined, must be an \`Array\` of non empty \`String\`.
Your configuration for the \`labels\` option is \`${stringify(labels)}\`.`,
}),
EINVALIDASSIGNEES: ({assignees}) => ({
message: 'Invalid `assignees` option.',
details: `The [assignees option](${linkify(
'README.md#assignees'
)}) option must be an \`Array\` of non empty \`Strings\`.
Your configuration for the \`assignees\` option is \`${stringify(assignees)}\`.`,
}),
EINVALIDGITHUBURL: () => ({
message: 'The git repository URL is not a valid GitHub URL.',
Expand Down
1 change: 1 addition & 0 deletions lib/definitions/sr-issue-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = '<!-- semantic-release:github -->';
31 changes: 31 additions & 0 deletions lib/fail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const {template} = require('lodash');
const parseGithubUrl = require('parse-github-url');
const debug = require('debug')('semantic-release:github');
const ISSUE_ID = require('./definitions/sr-issue-id');
const resolveConfig = require('./resolve-config');
const getClient = require('./get-client');
const findSRIssues = require('./find-sr-issues');
const getFailComment = require('./get-fail-comment');

module.exports = async (pluginConfig, {options: {branch, repositoryUrl}, errors, logger}) => {
const {githubToken, githubUrl, githubApiPathPrefix, failComment, failTitle, labels, assignees} = resolveConfig(
pluginConfig
);
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
const body = failComment ? template(failComment)({branch, errors}) : getFailComment(branch, errors);
const srIssue = (await findSRIssues(github, failTitle, owner, repo))[0];

if (srIssue) {
logger.log('Found existing semantic-release issue #%d.', srIssue.number);
const comment = {owner, repo, number: srIssue.number, body};
debug('create comment: %O', comment);
const {data: {html_url: url}} = await github.issues.createComment(comment);
logger.log('Added comment to issue #%d: %s.', srIssue.number, url);
} else {
const newIssue = {owner, repo, title: failTitle, body: `${body}\n\n${ISSUE_ID}`, labels, assignees};
debug('create issue: %O', newIssue);
const {data: {html_url: url, number}} = await github.issues.create(newIssue);
logger.log('Created issue #%d: %s.', number, url);
}
};
9 changes: 9 additions & 0 deletions lib/find-sr-issues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const ISSUE_ID = require('./definitions/sr-issue-id');

module.exports = async (github, title, owner, repo) => {
const {data: {items: issues}} = await github.search.issues({
q: `title:${title}+repo:${owner}/${repo}+type:issue+state:open`,
});

return issues.filter(issue => issue.body && issue.body.includes(ISSUE_ID));
};
44 changes: 44 additions & 0 deletions lib/get-fail-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const HOME_URL = 'https://github.com/semantic-release/semantic-release';
const FAQ_URL = `${HOME_URL}/blob/caribou/docs/support/FAQ.md`;
const GET_HELP_URL = `${HOME_URL}#get-help`;
const USAGE_DOC_URL = `${HOME_URL}/blob/caribou/docs/usage/README.md`;
const NEW_ISSUE_URL = `${HOME_URL}/issues/new`;

const formatError = error => `### ${error.message}
${error.details ||
`Unfortunatly this error doesn't have any additionnal information.${
error.pluginName
? ` Feel free to kindly ask the author of the \`${error.pluginName}\` plugin to add more helpful informations.`
: ''
}`}`;

module.exports = (
branch,
errors
) => `## :rotating_light: The automated release from the \`${branch}\` branch failed. :rotating_light:
I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.
You can find below the list of errors reported by **semantic-release**. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this 💪.
Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.
Once all the errors are resolved, **semantic-release** will release your package the next time you push a commit the \`${branch}\` branch. You can also manually restart the failed CI job that runs **semantic-release**.
If you are not sure how to resolve this, here is some links that can help you:
- [Usage documentation](${USAGE_DOC_URL})
- [Frequently Asked Questions](${FAQ_URL})
- [Support channels](${GET_HELP_URL})
If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind **[semantic-release](${NEW_ISSUE_URL})**.
---
${errors.map(formatError).join('\n\n---\n\n')}
---
Good luck with your project ✨
Your **[semantic-release](${HOME_URL})** bot :package::rocket:`;
18 changes: 16 additions & 2 deletions lib/resolve-config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
const {castArray} = require('lodash');
const {isUndefined, castArray} = require('lodash');

module.exports = ({githubUrl, githubApiPathPrefix, assets, successComment}) => ({
module.exports = ({
githubUrl,
githubApiPathPrefix,
assets,
successComment,
failComment,
failTitle,
labels,
assignees,
}) => ({
githubToken: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
githubUrl: githubUrl || process.env.GH_URL || process.env.GITHUB_URL,
githubApiPathPrefix: githubApiPathPrefix || process.env.GH_PREFIX || process.env.GITHUB_PREFIX || '',
assets: assets ? castArray(assets) : assets,
successComment,
failComment,
failTitle:
isUndefined(failTitle) || failTitle === false ? 'The automated release is failing :rotating_light:' : failTitle,
labels: isUndefined(labels) ? ['semantic-release'] : labels === false ? [] : castArray(labels),
assignees: assignees ? castArray(assignees) : assignees,
});
22 changes: 21 additions & 1 deletion lib/success.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ const debug = require('debug')('semantic-release:github');
const resolveConfig = require('./resolve-config');
const getClient = require('./get-client');
const getSuccessComment = require('./get-success-comment');
const findSRIssues = require('./find-sr-issues');

module.exports = async (
pluginConfig,
{options: {branch, repositoryUrl}, lastRelease, commits, nextRelease, releases, logger}
) => {
const {githubToken, githubUrl, githubApiPathPrefix, successComment} = resolveConfig(pluginConfig);
const {githubToken, githubUrl, githubApiPathPrefix, successComment, failTitle} = resolveConfig(pluginConfig);
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
const releaseInfos = releases.filter(release => Boolean(release.name));
Expand Down Expand Up @@ -56,6 +57,25 @@ module.exports = async (
// Don't throw right away and continue to update other issues
}
});

const srIssues = await findSRIssues(github, failTitle, owner, repo);

debug('found semantic-release issues: %O', srIssues);

await pReduce(srIssues, async (_, issue) => {
debug('close issue: %O', issue);
try {
const updateIssue = {owner, repo, number: issue.number, state: 'closed'};
debug('closing issue: %O', updateIssue);
const {data: {html_url: url}} = await github.issues.edit(updateIssue);
logger.log('Closed issue #%d: %s.', issue.number, url);
} catch (err) {
errors.push(err);
logger.error('Failed to close the issue #%d.', issue.number);
// Don't throw right away and continue to close other issues
}
});

if (errors.length > 0) {
throw new AggregateError(errors);
}
Expand Down
Loading

0 comments on commit 2367a71

Please sign in to comment.