Skip to content

Commit 2367a71

Browse files
committed
feat: add fail hook
- 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
1 parent 06979d8 commit 2367a71

17 files changed

+1101
-127
lines changed

README.md

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ Publish a [GitHub release](https://help.github.com/articles/about-releases), opt
2121

2222
## success
2323

24-
Add a comment to each GitHub issue or pull request resolved by the release.
24+
Add a comment to each GitHub issue or pull request resolved by the release and close issues previously open by the [fail](#fail) step.
25+
26+
## fail
27+
28+
Open or update a GitHub issue with informations about the errors that caused the release to fail.
2529

2630
## Configuration
2731

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

4347
### Options
4448

45-
| Option | Description | Default |
46-
|-----------------------|------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
47-
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
48-
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
49-
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
50-
| `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>)` |
49+
| Option | Description | Default |
50+
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
51+
| `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. |
52+
| `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. |
53+
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
54+
| `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>)` |
55+
| `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. |
56+
| `failTitle` | The title of the issue created when a release fails. | `The automated release is failing :rotating_light:` |
57+
| `labels` | The [labels](https://help.github.com/articles/about-labels) to add to the issue created when a release fails. | `['semantic-release']` |
58+
| `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. | - |
5159

5260
#### assets
5361

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

103111
> This pull request is included in version 1.0.0
104112
113+
#### failComment
114+
115+
The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available:
116+
117+
| Parameter | Description |
118+
|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
119+
| `branch` | The branch from which the release had failed. |
120+
| `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. |
121+
122+
##### failComment examples
123+
124+
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:
125+
126+
> This release from branch master had failed due to the following errors:
127+
> - Error message 1
128+
> - Error message 2
129+
105130
### Usage
106131

107132
The plugins are used by default by [Semantic-release](https://github.com/semantic-release/semantic-release) so no
@@ -114,7 +139,8 @@ Each individual plugin can be disabled, replaced or used with other plugins in t
114139
"release": {
115140
"verifyConditions": ["@semantic-release/github", "@semantic-release/npm", "verify-other-condition"],
116141
"publish": ["@semantic-release/npm", "@semantic-release/github", "other-publish"],
117-
"success": ["@semantic-release/github", "other-success"]
142+
"success": ["@semantic-release/github", "other-success"],
143+
"fail": ["@semantic-release/github", "other-fail"]
118144
}
119145
}
120146
```

index.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
const verifyGitHub = require('./lib/verify');
22
const publishGitHub = require('./lib/publish');
33
const successGitHub = require('./lib/success');
4+
const failGitHub = require('./lib/fail');
45

56
let verified;
67

78
async function verifyConditions(pluginConfig, context) {
89
const {options} = context;
9-
// 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
10+
// 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
1011
if (options.publish) {
11-
const publishPlugin = (Array.isArray(options.publish) ? options.publish : [options.publish]).find(
12-
config => config.path && config.path === '@semantic-release/github'
13-
);
14-
if (publishPlugin && publishPlugin.assets) {
15-
pluginConfig.assets = publishPlugin.assets;
16-
}
17-
if (publishPlugin && publishPlugin.successComment) {
18-
pluginConfig.successComment = publishPlugin.successComment;
19-
}
12+
const publishPlugin =
13+
(Array.isArray(options.publish) ? options.publish : [options.publish]).find(
14+
config => config.path && config.path === '@semantic-release/github'
15+
) || {};
16+
17+
pluginConfig.assets = pluginConfig.assets || publishPlugin.assets;
18+
pluginConfig.successComment = pluginConfig.successComment || publishPlugin.successComment;
19+
pluginConfig.failComment = pluginConfig.failComment || publishPlugin.failComment;
20+
pluginConfig.failTitle = pluginConfig.failTitle || publishPlugin.failTitle;
21+
pluginConfig.labels = pluginConfig.labels || publishPlugin.labels;
22+
pluginConfig.assignees = pluginConfig.assignees || publishPlugin.assignees;
2023
}
2124

2225
await verifyGitHub(pluginConfig, context);
@@ -39,4 +42,12 @@ async function success(pluginConfig, context) {
3942
await successGitHub(pluginConfig, context);
4043
}
4144

42-
module.exports = {verifyConditions, publish, success};
45+
async function fail(pluginConfig, context) {
46+
if (!verified) {
47+
await verifyGitHub(pluginConfig, context);
48+
verified = true;
49+
}
50+
await failGitHub(pluginConfig, context);
51+
}
52+
53+
module.exports = {verifyConditions, publish, success, fail};

lib/definitions/errors.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ Your configuration for the \`assets\` option is \`${stringify(assets)}\`.`,
2323
)}) option, if defined, must be a non empty \`String\`.
2424
2525
Your configuration for the \`successComment\` option is \`${stringify(successComment)}\`.`,
26+
}),
27+
EINVALIDFAILTITLE: ({failTitle}) => ({
28+
message: 'Invalid `failTitle` option.',
29+
details: `The [failTitle option](${linkify(
30+
'README.md#failtitle'
31+
)}) option, if defined, must be a non empty \`String\`.
32+
33+
Your configuration for the \`failTitle\` option is \`${stringify(failTitle)}\`.`,
34+
}),
35+
EINVALIDFAILCOMMENT: ({failComment}) => ({
36+
message: 'Invalid `failComment` option.',
37+
details: `The [failComment option](${linkify(
38+
'README.md#failcomment'
39+
)}) option, if defined, must be a non empty \`String\`.
40+
41+
Your configuration for the \`failComment\` option is \`${stringify(failComment)}\`.`,
42+
}),
43+
EINVALIDLABELS: ({labels}) => ({
44+
message: 'Invalid `labels` option.',
45+
details: `The [labels option](${linkify(
46+
'README.md#labels'
47+
)}) option, if defined, must be an \`Array\` of non empty \`String\`.
48+
49+
Your configuration for the \`labels\` option is \`${stringify(labels)}\`.`,
50+
}),
51+
EINVALIDASSIGNEES: ({assignees}) => ({
52+
message: 'Invalid `assignees` option.',
53+
details: `The [assignees option](${linkify(
54+
'README.md#assignees'
55+
)}) option must be an \`Array\` of non empty \`Strings\`.
56+
57+
Your configuration for the \`assignees\` option is \`${stringify(assignees)}\`.`,
2658
}),
2759
EINVALIDGITHUBURL: () => ({
2860
message: 'The git repository URL is not a valid GitHub URL.',

lib/definitions/sr-issue-id.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = '<!-- semantic-release:github -->';

lib/fail.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const {template} = require('lodash');
2+
const parseGithubUrl = require('parse-github-url');
3+
const debug = require('debug')('semantic-release:github');
4+
const ISSUE_ID = require('./definitions/sr-issue-id');
5+
const resolveConfig = require('./resolve-config');
6+
const getClient = require('./get-client');
7+
const findSRIssues = require('./find-sr-issues');
8+
const getFailComment = require('./get-fail-comment');
9+
10+
module.exports = async (pluginConfig, {options: {branch, repositoryUrl}, errors, logger}) => {
11+
const {githubToken, githubUrl, githubApiPathPrefix, failComment, failTitle, labels, assignees} = resolveConfig(
12+
pluginConfig
13+
);
14+
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
15+
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
16+
const body = failComment ? template(failComment)({branch, errors}) : getFailComment(branch, errors);
17+
const srIssue = (await findSRIssues(github, failTitle, owner, repo))[0];
18+
19+
if (srIssue) {
20+
logger.log('Found existing semantic-release issue #%d.', srIssue.number);
21+
const comment = {owner, repo, number: srIssue.number, body};
22+
debug('create comment: %O', comment);
23+
const {data: {html_url: url}} = await github.issues.createComment(comment);
24+
logger.log('Added comment to issue #%d: %s.', srIssue.number, url);
25+
} else {
26+
const newIssue = {owner, repo, title: failTitle, body: `${body}\n\n${ISSUE_ID}`, labels, assignees};
27+
debug('create issue: %O', newIssue);
28+
const {data: {html_url: url, number}} = await github.issues.create(newIssue);
29+
logger.log('Created issue #%d: %s.', number, url);
30+
}
31+
};

lib/find-sr-issues.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const ISSUE_ID = require('./definitions/sr-issue-id');
2+
3+
module.exports = async (github, title, owner, repo) => {
4+
const {data: {items: issues}} = await github.search.issues({
5+
q: `title:${title}+repo:${owner}/${repo}+type:issue+state:open`,
6+
});
7+
8+
return issues.filter(issue => issue.body && issue.body.includes(ISSUE_ID));
9+
};

lib/get-fail-comment.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const HOME_URL = 'https://github.com/semantic-release/semantic-release';
2+
const FAQ_URL = `${HOME_URL}/blob/caribou/docs/support/FAQ.md`;
3+
const GET_HELP_URL = `${HOME_URL}#get-help`;
4+
const USAGE_DOC_URL = `${HOME_URL}/blob/caribou/docs/usage/README.md`;
5+
const NEW_ISSUE_URL = `${HOME_URL}/issues/new`;
6+
7+
const formatError = error => `### ${error.message}
8+
9+
${error.details ||
10+
`Unfortunatly this error doesn't have any additionnal information.${
11+
error.pluginName
12+
? ` Feel free to kindly ask the author of the \`${error.pluginName}\` plugin to add more helpful informations.`
13+
: ''
14+
}`}`;
15+
16+
module.exports = (
17+
branch,
18+
errors
19+
) => `## :rotating_light: The automated release from the \`${branch}\` branch failed. :rotating_light:
20+
21+
I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.
22+
23+
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 💪.
24+
25+
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.
26+
27+
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**.
28+
29+
If you are not sure how to resolve this, here is some links that can help you:
30+
- [Usage documentation](${USAGE_DOC_URL})
31+
- [Frequently Asked Questions](${FAQ_URL})
32+
- [Support channels](${GET_HELP_URL})
33+
34+
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})**.
35+
36+
---
37+
38+
${errors.map(formatError).join('\n\n---\n\n')}
39+
40+
---
41+
42+
Good luck with your project ✨
43+
44+
Your **[semantic-release](${HOME_URL})** bot :package::rocket:`;

lib/resolve-config.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1-
const {castArray} = require('lodash');
1+
const {isUndefined, castArray} = require('lodash');
22

3-
module.exports = ({githubUrl, githubApiPathPrefix, assets, successComment}) => ({
3+
module.exports = ({
4+
githubUrl,
5+
githubApiPathPrefix,
6+
assets,
7+
successComment,
8+
failComment,
9+
failTitle,
10+
labels,
11+
assignees,
12+
}) => ({
413
githubToken: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
514
githubUrl: githubUrl || process.env.GH_URL || process.env.GITHUB_URL,
615
githubApiPathPrefix: githubApiPathPrefix || process.env.GH_PREFIX || process.env.GITHUB_PREFIX || '',
716
assets: assets ? castArray(assets) : assets,
817
successComment,
18+
failComment,
19+
failTitle:
20+
isUndefined(failTitle) || failTitle === false ? 'The automated release is failing :rotating_light:' : failTitle,
21+
labels: isUndefined(labels) ? ['semantic-release'] : labels === false ? [] : castArray(labels),
22+
assignees: assignees ? castArray(assignees) : assignees,
923
});

lib/success.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ const debug = require('debug')('semantic-release:github');
77
const resolveConfig = require('./resolve-config');
88
const getClient = require('./get-client');
99
const getSuccessComment = require('./get-success-comment');
10+
const findSRIssues = require('./find-sr-issues');
1011

1112
module.exports = async (
1213
pluginConfig,
1314
{options: {branch, repositoryUrl}, lastRelease, commits, nextRelease, releases, logger}
1415
) => {
15-
const {githubToken, githubUrl, githubApiPathPrefix, successComment} = resolveConfig(pluginConfig);
16+
const {githubToken, githubUrl, githubApiPathPrefix, successComment, failTitle} = resolveConfig(pluginConfig);
1617
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
1718
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
1819
const releaseInfos = releases.filter(release => Boolean(release.name));
@@ -56,6 +57,25 @@ module.exports = async (
5657
// Don't throw right away and continue to update other issues
5758
}
5859
});
60+
61+
const srIssues = await findSRIssues(github, failTitle, owner, repo);
62+
63+
debug('found semantic-release issues: %O', srIssues);
64+
65+
await pReduce(srIssues, async (_, issue) => {
66+
debug('close issue: %O', issue);
67+
try {
68+
const updateIssue = {owner, repo, number: issue.number, state: 'closed'};
69+
debug('closing issue: %O', updateIssue);
70+
const {data: {html_url: url}} = await github.issues.edit(updateIssue);
71+
logger.log('Closed issue #%d: %s.', issue.number, url);
72+
} catch (err) {
73+
errors.push(err);
74+
logger.error('Failed to close the issue #%d.', issue.number);
75+
// Don't throw right away and continue to close other issues
76+
}
77+
});
78+
5979
if (errors.length > 0) {
6080
throw new AggregateError(errors);
6181
}

0 commit comments

Comments
 (0)