Skip to content

Commit 861f2dd

Browse files
author
Nils Plaschke
committed
feat: add additional release links to the release
A new option `addReleases` has been added. Setting this option will instruct the plugin to append all additional releases to the Github release on the top. Closes #281
1 parent 32654fb commit 861f2dd

9 files changed

+329
-5
lines changed

lib/definitions/errors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ Your configuration for the \`assignees\` option is \`${stringify(assignees)}\`.`
5757
)}) if defined, must be an \`Array\` of non empty \`String\`.
5858
5959
Your configuration for the \`releasedLabels\` option is \`${stringify(releasedLabels)}\`.`,
60+
}),
61+
EINVALIDADDRELEASES: ({addReleases}) => ({
62+
message: 'Invalid `addReleases` option.',
63+
details: `The [addReleases option](${linkify('README.md#options')}) if defined, must be a \`Boolean\`.
64+
65+
Your configuration for the \`addReleases\` option is \`${stringify(addReleases)}\`.`,
6066
}),
6167
EINVALIDGITHUBURL: () => ({
6268
message: 'The git repository URL is not a valid GitHub URL.',

lib/get-release-links.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const {RELEASE_NAME} = require('./definitions/constants');
2+
3+
const linkify = (releaseInfo) =>
4+
`${
5+
releaseInfo.url
6+
? releaseInfo.url.startsWith('http')
7+
? `[${releaseInfo.name}](${releaseInfo.url})`
8+
: `${releaseInfo.name}: \`${releaseInfo.url}\``
9+
: `\`${releaseInfo.name}\``
10+
}`;
11+
12+
const filterReleases = (releaseInfos) =>
13+
releaseInfos.filter((releaseInfo) => releaseInfo.name && releaseInfo.name !== RELEASE_NAME);
14+
15+
module.exports = (releaseInfos) =>
16+
`${
17+
filterReleases(releaseInfos).length > 0
18+
? `This release is also available on:\n${filterReleases(releaseInfos)
19+
.map((releaseInfo) => `- ${linkify(releaseInfo)}`)
20+
.join('\n')}\n---\n`
21+
: ''
22+
}`;

lib/publish.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ module.exports = async (pluginConfig, context) => {
2828
// When there are no assets, we publish a release directly
2929
if (!assets || assets.length === 0) {
3030
const {
31-
data: {html_url: url},
31+
data: {html_url: url, id: releaseId},
3232
} = await github.repos.createRelease(release);
3333

3434
logger.log('Published GitHub release: %s', url);
35-
return {url, name: RELEASE_NAME};
35+
return {url, name: RELEASE_NAME, id: releaseId};
3636
}
3737

3838
// We'll create a draft release, append the assets to it, and then publish it.
@@ -94,5 +94,5 @@ module.exports = async (pluginConfig, context) => {
9494
} = await github.repos.updateRelease({owner, repo, release_id: releaseId, draft: false});
9595

9696
logger.log('Published GitHub release: %s', url);
97-
return {url, name: RELEASE_NAME};
97+
return {url, name: RELEASE_NAME, id: releaseId};
9898
};

lib/resolve-config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = (
1212
labels,
1313
assignees,
1414
releasedLabels,
15+
addReleases,
1516
},
1617
{env}
1718
) => ({
@@ -30,4 +31,5 @@ module.exports = (
3031
: releasedLabels === false
3132
? false
3233
: castArray(releasedLabels),
34+
addReleases: isNil(addReleases) ? false : addReleases,
3335
});

lib/success.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const {isNil, uniqBy, template, flatten} = require('lodash');
1+
const {isNil, uniqBy, template, flatten, isEmpty} = require('lodash');
22
const pFilter = require('p-filter');
33
const AggregateError = require('aggregate-error');
44
const issueParser = require('issue-parser');
@@ -9,6 +9,8 @@ const getClient = require('./get-client');
99
const getSearchQueries = require('./get-search-queries');
1010
const getSuccessComment = require('./get-success-comment');
1111
const findSRIssues = require('./find-sr-issues');
12+
const {RELEASE_NAME} = require('./definitions/constants');
13+
const getReleaseLinks = require('./get-release-links');
1214

1315
module.exports = async (pluginConfig, context) => {
1416
const {
@@ -17,6 +19,7 @@ module.exports = async (pluginConfig, context) => {
1719
nextRelease,
1820
releases,
1921
logger,
22+
notes,
2023
} = context;
2124
const {
2225
githubToken,
@@ -27,6 +30,7 @@ module.exports = async (pluginConfig, context) => {
2730
failComment,
2831
failTitle,
2932
releasedLabels,
33+
addReleases,
3034
} = resolveConfig(pluginConfig, context);
3135

3236
const github = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
@@ -140,6 +144,16 @@ module.exports = async (pluginConfig, context) => {
140144
);
141145
}
142146

147+
if (addReleases === true && errors.length === 0) {
148+
const releaseId = releases.find((release) => release.name && release.name === RELEASE_NAME)?.id;
149+
if (!isNil(releaseId)) {
150+
const additionalReleases = getReleaseLinks(releases);
151+
if (!isEmpty(additionalReleases)) {
152+
await github.repos.updateRelease({owner, repo, release_id: releaseId, body: additionalReleases.concat(notes)});
153+
}
154+
}
155+
}
156+
143157
if (errors.length > 0) {
144158
throw new AggregateError(errors);
145159
}

lib/verify.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const {isString, isPlainObject, isNil, isArray, isNumber} = require('lodash');
1+
const {isString, isPlainObject, isNil, isArray, isNumber, isBoolean} = require('lodash');
22
const urlJoin = require('url-join');
33
const AggregateError = require('aggregate-error');
44
const parseGithubUrl = require('./parse-github-url');
@@ -7,6 +7,7 @@ const getClient = require('./get-client');
77
const getError = require('./get-error');
88

99
const isNonEmptyString = (value) => isString(value) && value.trim();
10+
const isEnabled = (value) => isBoolean(value);
1011
const isStringOrStringArray = (value) =>
1112
isNonEmptyString(value) || (isArray(value) && value.every((string) => isNonEmptyString(string)));
1213
const isArrayOf = (validator) => (array) => isArray(array) && array.every((value) => validator(value));
@@ -24,6 +25,7 @@ const VALIDATORS = {
2425
labels: canBeDisabled(isArrayOf(isNonEmptyString)),
2526
assignees: isArrayOf(isNonEmptyString),
2627
releasedLabels: canBeDisabled(isArrayOf(isNonEmptyString)),
28+
addReleases: isEnabled,
2729
};
2830

2931
module.exports = async (pluginConfig, context) => {

test/get-release-links.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const test = require('ava');
2+
const getReleaseLinks = require('../lib/get-release-links');
3+
const {RELEASE_NAME} = require('../lib/definitions/constants');
4+
5+
test('Comment for release with multiple releases', (t) => {
6+
const releaseInfos = [
7+
{name: RELEASE_NAME, url: 'https://github.com/release'},
8+
{name: 'Http release', url: 'https://release.com/release'},
9+
{name: 'npm release', url: 'https://npm.com/release'},
10+
];
11+
const comment = getReleaseLinks(releaseInfos);
12+
13+
t.is(
14+
comment,
15+
`This release is also available on:
16+
- [Http release](https://release.com/release)
17+
- [npm release](https://npm.com/release)
18+
---
19+
`
20+
);
21+
});
22+
23+
test('Release with missing release URL', (t) => {
24+
const releaseInfos = [
25+
{name: RELEASE_NAME, url: 'https://github.com/release'},
26+
{name: 'Http release', url: 'https://release.com/release'},
27+
{name: 'npm release'},
28+
];
29+
const comment = getReleaseLinks(releaseInfos);
30+
31+
t.is(
32+
comment,
33+
`This release is also available on:
34+
- [Http release](https://release.com/release)
35+
- \`npm release\`
36+
---
37+
`
38+
);
39+
});
40+
41+
test('Release with one release', (t) => {
42+
const releaseInfos = [
43+
{name: RELEASE_NAME, url: 'https://github.com/release'},
44+
{name: 'Http release', url: 'https://release.com/release'},
45+
];
46+
const comment = getReleaseLinks(releaseInfos);
47+
48+
t.is(
49+
comment,
50+
`This release is also available on:
51+
- [Http release](https://release.com/release)
52+
---
53+
`
54+
);
55+
});
56+
57+
test('Release with non http releases', (t) => {
58+
const releaseInfos = [{name: 'S3', url: 's3://my-bucket/release-asset'}];
59+
const comment = getReleaseLinks(releaseInfos);
60+
61+
t.is(
62+
comment,
63+
`This release is also available on:
64+
- S3: \`s3://my-bucket/release-asset\`
65+
---
66+
`
67+
);
68+
});
69+
70+
test('Release with only github release', (t) => {
71+
const releaseInfos = [{name: RELEASE_NAME, url: 'https://github.com/release'}];
72+
const comment = getReleaseLinks(releaseInfos);
73+
74+
t.is(comment, '');
75+
});
76+
77+
test('Comment with no release object', (t) => {
78+
const releaseInfos = [];
79+
const comment = getReleaseLinks(releaseInfos);
80+
81+
t.is(comment, '');
82+
});

test/success.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const proxyquire = require('proxyquire');
77
const {ISSUE_ID} = require('../lib/definitions/constants');
88
const {authenticate} = require('./helpers/mock-github');
99
const rateLimit = require('./helpers/rate-limit');
10+
const getReleaseLinks = require('../lib/get-release-links');
1011

1112
/* eslint camelcase: ["error", {properties: "never"}] */
1213

@@ -624,6 +625,160 @@ test.serial('Comment on issue/PR without ading a label', async (t) => {
624625
t.true(github.isDone());
625626
});
626627

628+
test.serial('Editing the release to include all release links', async (t) => {
629+
const owner = 'test_user';
630+
const repo = 'test_repo';
631+
const env = {GITHUB_TOKEN: 'github_token'};
632+
const failTitle = 'The automated release is failing 🚨';
633+
const pluginConfig = {releasedLabels: false, addReleases: true};
634+
const prs = [{number: 1, pull_request: {}, state: 'closed'}];
635+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
636+
const nextRelease = {version: '2.0.0', gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
637+
const lastRelease = {version: '1.0.0'};
638+
const commits = [{hash: '123', message: 'Commit 1 message'}];
639+
const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`;
640+
const releaseId = 1;
641+
const releases = [
642+
{name: 'GitHub release', url: 'https://github.com/release', id: releaseId},
643+
{name: 'S3', url: 's3://my-bucket/release-asset'},
644+
{name: 'Docker: docker.io/python:slim'},
645+
];
646+
const github = authenticate(env)
647+
.get(`/repos/${owner}/${repo}`)
648+
.reply(200, {full_name: `${owner}/${repo}`})
649+
.get(
650+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${escape('is:merged')}+${commits
651+
.map((commit) => commit.hash)
652+
.join('+')}`
653+
)
654+
.reply(200, {items: prs})
655+
.get(`/repos/${owner}/${repo}/pulls/1/commits`)
656+
.reply(200, [{sha: commits[0].hash}])
657+
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
658+
.reply(200, {html_url: 'https://github.com/successcomment-1'})
659+
.get(
660+
`/search/issues?q=${escape('in:title')}+${escape(`repo:${owner}/${repo}`)}+${escape('type:issue')}+${escape(
661+
'state:open'
662+
)}+${escape(failTitle)}`
663+
)
664+
.reply(200, {items: []})
665+
.patch(`/repos/${owner}/${repo}/releases/${releaseId}`, {body: getReleaseLinks(releases).concat(nextRelease.body)})
666+
.reply(200, {html_url: releaseUrl});
667+
668+
await success(pluginConfig, {
669+
env,
670+
options,
671+
branch: {name: 'master'},
672+
lastRelease,
673+
commits,
674+
nextRelease,
675+
releases,
676+
logger: t.context.logger,
677+
});
678+
679+
t.true(t.context.log.calledWith('Added comment to issue #%d: %s', 1, 'https://github.com/successcomment-1'));
680+
t.true(github.isDone());
681+
});
682+
683+
test.serial('Editing the release to include all release links with no additional releases', async (t) => {
684+
const owner = 'test_user';
685+
const repo = 'test_repo';
686+
const env = {GITHUB_TOKEN: 'github_token'};
687+
const failTitle = 'The automated release is failing 🚨';
688+
const pluginConfig = {releasedLabels: false, addReleases: true};
689+
const prs = [{number: 1, pull_request: {}, state: 'closed'}];
690+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
691+
const nextRelease = {version: '2.0.0', gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
692+
const lastRelease = {version: '1.0.0'};
693+
const commits = [{hash: '123', message: 'Commit 1 message'}];
694+
const releaseId = 1;
695+
const releases = [{name: 'GitHub release', url: 'https://github.com/release', id: releaseId}];
696+
const github = authenticate(env)
697+
.get(`/repos/${owner}/${repo}`)
698+
.reply(200, {full_name: `${owner}/${repo}`})
699+
.get(
700+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${escape('is:merged')}+${commits
701+
.map((commit) => commit.hash)
702+
.join('+')}`
703+
)
704+
.reply(200, {items: prs})
705+
.get(`/repos/${owner}/${repo}/pulls/1/commits`)
706+
.reply(200, [{sha: commits[0].hash}])
707+
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
708+
.reply(200, {html_url: 'https://github.com/successcomment-1'})
709+
.get(
710+
`/search/issues?q=${escape('in:title')}+${escape(`repo:${owner}/${repo}`)}+${escape('type:issue')}+${escape(
711+
'state:open'
712+
)}+${escape(failTitle)}`
713+
)
714+
.reply(200, {items: []});
715+
716+
await success(pluginConfig, {
717+
env,
718+
options,
719+
branch: {name: 'master'},
720+
lastRelease,
721+
commits,
722+
nextRelease,
723+
releases,
724+
logger: t.context.logger,
725+
});
726+
727+
t.true(t.context.log.calledWith('Added comment to issue #%d: %s', 1, 'https://github.com/successcomment-1'));
728+
t.true(github.isDone());
729+
});
730+
731+
test.serial('Editing the release with no ID in the release', async (t) => {
732+
const owner = 'test_user';
733+
const repo = 'test_repo';
734+
const env = {GITHUB_TOKEN: 'github_token'};
735+
const failTitle = 'The automated release is failing 🚨';
736+
const pluginConfig = {releasedLabels: false, addReleases: true};
737+
const prs = [{number: 1, pull_request: {}, state: 'closed'}];
738+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
739+
const nextRelease = {version: '2.0.0', gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
740+
const lastRelease = {version: '1.0.0'};
741+
const commits = [{hash: '123', message: 'Commit 1 message'}];
742+
const releases = [
743+
{name: 'GitHub release', url: 'https://github.com/release'},
744+
{name: 'S3', url: 's3://my-bucket/release-asset'},
745+
{name: 'Docker: docker.io/python:slim'},
746+
];
747+
const github = authenticate(env)
748+
.get(`/repos/${owner}/${repo}`)
749+
.reply(200, {full_name: `${owner}/${repo}`})
750+
.get(
751+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${escape('is:merged')}+${commits
752+
.map((commit) => commit.hash)
753+
.join('+')}`
754+
)
755+
.reply(200, {items: prs})
756+
.get(`/repos/${owner}/${repo}/pulls/1/commits`)
757+
.reply(200, [{sha: commits[0].hash}])
758+
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
759+
.reply(200, {html_url: 'https://github.com/successcomment-1'})
760+
.get(
761+
`/search/issues?q=${escape('in:title')}+${escape(`repo:${owner}/${repo}`)}+${escape('type:issue')}+${escape(
762+
'state:open'
763+
)}+${escape(failTitle)}`
764+
)
765+
.reply(200, {items: []});
766+
767+
await success(pluginConfig, {
768+
env,
769+
options,
770+
branch: {name: 'master'},
771+
lastRelease,
772+
commits,
773+
nextRelease,
774+
releases,
775+
logger: t.context.logger,
776+
});
777+
778+
t.true(t.context.log.calledWith('Added comment to issue #%d: %s', 1, 'https://github.com/successcomment-1'));
779+
t.true(github.isDone());
780+
});
781+
627782
test.serial('Ignore errors when adding comments and closing issues', async (t) => {
628783
const owner = 'test_user';
629784
const repo = 'test_repo';

0 commit comments

Comments
 (0)