Skip to content

Commit c556426

Browse files
committed
fix: make search queries of 256 characters maximum
1 parent 73052e8 commit c556426

File tree

5 files changed

+141
-25
lines changed

5 files changed

+141
-25
lines changed

lib/get-search-queries.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = (base, commits, separator = '+') => {
2+
return commits.reduce((searches, commit) => {
3+
const lastSearch = searches[searches.length - 1];
4+
5+
if (lastSearch && lastSearch.length + commit.length <= 256 - separator.length) {
6+
searches[searches.length - 1] = `${lastSearch}${separator}${commit}`;
7+
} else {
8+
searches.push(`${base}${separator}${commit}`);
9+
}
10+
return searches;
11+
}, []);
12+
};

lib/success.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const issueParser = require('issue-parser')('github');
66
const debug = require('debug')('semantic-release:github');
77
const resolveConfig = require('./resolve-config');
88
const getClient = require('./get-client');
9+
const getSearchQueries = require('./get-search-queries');
910
const getSuccessComment = require('./get-success-comment');
1011
const findSRIssues = require('./find-sr-issues');
1112

@@ -18,10 +19,14 @@ module.exports = async (
1819
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
1920
const releaseInfos = releases.filter(release => Boolean(release.name));
2021

21-
// Search for PRs associated with any commit in the release
22-
const {data: {items: prs}} = await github.search.issues({
23-
q: `${commits.map(commit => commit.hash).join('+')}+repo:${owner}/${repo}+type:pr`,
24-
});
22+
const prs = await pReduce(
23+
getSearchQueries(`repo:${owner}/${repo}+type:pr`, commits.map(commit => commit.hash)),
24+
async (prs, q) => {
25+
const {data: {items}} = await github.search.issues({q});
26+
return [...prs, ...items];
27+
},
28+
[]
29+
);
2530

2631
debug('found pull requests: %O', prs.map(pr => pr.number));
2732

test/get-search-queries.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import test from 'ava';
2+
import {repeat} from 'lodash';
3+
import getSearchQueries from '../lib/get-search-queries';
4+
5+
test('Generate queries of 256 characters maximum', t => {
6+
const commits = [
7+
repeat('a', 40),
8+
repeat('b', 40),
9+
repeat('c', 40),
10+
repeat('d', 40),
11+
repeat('e', 40),
12+
repeat('f', 40),
13+
];
14+
15+
t.deepEqual(getSearchQueries(repeat('0', 51), commits), [
16+
`${repeat('0', 51)}+${commits[0]}+${commits[1]}+${commits[2]}+${commits[3]}+${commits[4]}`,
17+
`${repeat('0', 51)}+${commits[5]}`,
18+
]);
19+
20+
t.deepEqual(getSearchQueries(repeat('0', 52), commits), [
21+
`${repeat('0', 52)}+${commits[0]}+${commits[1]}+${commits[2]}+${commits[3]}`,
22+
`${repeat('0', 52)}+${commits[4]}+${commits[5]}`,
23+
]);
24+
});
25+
26+
test('Generate one query if it is less tahn 256 characters', t => {
27+
const commits = [repeat('a', 40), repeat('b', 40)];
28+
29+
t.deepEqual(getSearchQueries(repeat('0', 20), commits), [`${repeat('0', 20)}+${commits[0]}+${commits[1]}`]);
30+
});
31+
32+
test('Return emty Array if there is no commits', t => {
33+
t.deepEqual(getSearchQueries('base', []), []);
34+
});

test/integration.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,9 @@ test.serial('Comment on PR included in the releases', async t => {
192192
.get(`/repos/${owner}/${repo}`)
193193
.reply(200, {permissions: {push: true}})
194194
.get(
195-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
196-
'type:pr'
197-
)}`
195+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
196+
.map(commit => commit.hash)
197+
.join('+')}`
198198
)
199199
.reply(200, {items: prs})
200200
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
@@ -281,9 +281,9 @@ test.serial('Verify, release and notify success', async t => {
281281
})
282282
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl})
283283
.get(
284-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
285-
'type:pr'
286-
)}`
284+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
285+
.map(commit => commit.hash)
286+
.join('+')}`
287287
)
288288
.reply(200, {items: prs})
289289
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})

test/success.test.js

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {escape} from 'querystring';
22
import test from 'ava';
3+
import {repeat} from 'lodash';
34
import nock from 'nock';
45
import {stub} from 'sinon';
56
import ISSUE_ID from '../lib/definitions/sr-issue-id';
@@ -49,9 +50,9 @@ test.serial('Add comment to PRs associated with release commits and issues close
4950
const releases = [{name: 'GitHub release', url: 'https://github.com/release'}];
5051
const github = authenticate()
5152
.get(
52-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
53-
'type:pr'
54-
)}`
53+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
54+
.map(commit => commit.hash)
55+
.join('+')}`
5556
)
5657
.reply(200, {items: prs})
5758
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
@@ -78,6 +79,70 @@ test.serial('Add comment to PRs associated with release commits and issues close
7879
t.true(github.isDone());
7980
});
8081

82+
test.serial('Make multiple search queries if necessary', async t => {
83+
const owner = 'test_user';
84+
const repo = 'test_repo';
85+
process.env.GITHUB_TOKEN = 'github_token';
86+
const failTitle = 'The automated release is failing :rotating_light:';
87+
const pluginConfig = {failTitle};
88+
const prs = [
89+
{number: 1, pull_request: {}},
90+
{number: 2, pull_request: {}},
91+
{number: 3, pull_request: {}},
92+
{number: 4, pull_request: {}},
93+
{number: 5, pull_request: {}},
94+
{number: 6, pull_request: {}},
95+
];
96+
const options = {branch: 'master', repositoryUrl: `https://github.com/${owner}/${repo}.git`};
97+
const commits = [
98+
{hash: repeat('a', 40), message: 'Commit 1 message'},
99+
{hash: repeat('b', 40), message: 'Commit 2 message'},
100+
{hash: repeat('c', 40), message: 'Commit 3 message'},
101+
{hash: repeat('d', 40), message: 'Commit 4 message'},
102+
{hash: repeat('e', 40), message: 'Commit 5 message'},
103+
{hash: repeat('f', 40), message: 'Commit 6 message'},
104+
];
105+
const nextRelease = {version: '1.0.0'};
106+
const releases = [{name: 'GitHub release', url: 'https://github.com/release'}];
107+
const github = authenticate()
108+
.get(
109+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits[0].hash}+${commits[1].hash}+${
110+
commits[2].hash
111+
}+${commits[3].hash}+${commits[4].hash}`
112+
)
113+
.reply(200, {items: [prs[0], prs[1], prs[2], prs[3], prs[4]]})
114+
.get(`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits[5].hash}`)
115+
.reply(200, {items: [prs[5]]})
116+
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
117+
.reply(200, {html_url: 'https://github.com/successcomment-1'})
118+
.post(`/repos/${owner}/${repo}/issues/2/comments`, {body: /This PR is included/})
119+
.reply(200, {html_url: 'https://github.com/successcomment-2'})
120+
.post(`/repos/${owner}/${repo}/issues/3/comments`, {body: /This PR is included/})
121+
.reply(200, {html_url: 'https://github.com/successcomment-3'})
122+
.post(`/repos/${owner}/${repo}/issues/4/comments`, {body: /This PR is included/})
123+
.reply(200, {html_url: 'https://github.com/successcomment-4'})
124+
.post(`/repos/${owner}/${repo}/issues/5/comments`, {body: /This PR is included/})
125+
.reply(200, {html_url: 'https://github.com/successcomment-5'})
126+
.post(`/repos/${owner}/${repo}/issues/6/comments`, {body: /This PR is included/})
127+
.reply(200, {html_url: 'https://github.com/successcomment-6'})
128+
.get(
129+
`/search/issues?q=${escape(`title:${failTitle}`)}+${escape(`repo:${owner}/${repo}`)}+${escape(
130+
'type:issue'
131+
)}+${escape('state:open')}`
132+
)
133+
.reply(200, {items: []});
134+
135+
await success(pluginConfig, {options, commits, nextRelease, releases, logger: t.context.logger});
136+
137+
t.deepEqual(t.context.log.args[0], ['Added comment to issue #%d: %s', 1, 'https://github.com/successcomment-1']);
138+
t.deepEqual(t.context.log.args[1], ['Added comment to issue #%d: %s', 2, 'https://github.com/successcomment-2']);
139+
t.deepEqual(t.context.log.args[2], ['Added comment to issue #%d: %s', 3, 'https://github.com/successcomment-3']);
140+
t.deepEqual(t.context.log.args[3], ['Added comment to issue #%d: %s', 4, 'https://github.com/successcomment-4']);
141+
t.deepEqual(t.context.log.args[4], ['Added comment to issue #%d: %s', 5, 'https://github.com/successcomment-5']);
142+
t.deepEqual(t.context.log.args[5], ['Added comment to issue #%d: %s', 6, 'https://github.com/successcomment-6']);
143+
t.true(github.isDone());
144+
});
145+
81146
test.serial('Do not add comment if no PR is associated with release commits', async t => {
82147
const owner = 'test_user';
83148
const repo = 'test_repo';
@@ -90,9 +155,9 @@ test.serial('Do not add comment if no PR is associated with release commits', as
90155
const releases = [{name: 'GitHub release', url: 'https://github.com/release'}];
91156
const github = authenticate()
92157
.get(
93-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
94-
'type:pr'
95-
)}`
158+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
159+
.map(commit => commit.hash)
160+
.join('+')}`
96161
)
97162
.reply(200, {items: []})
98163
.get(
@@ -124,9 +189,9 @@ test.serial('Add custom comment', async t => {
124189
const releases = [{name: 'GitHub release', url: 'https://github.com/release'}];
125190
const github = authenticate()
126191
.get(
127-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
128-
'type:pr'
129-
)}`
192+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
193+
.map(commit => commit.hash)
194+
.join('+')}`
130195
)
131196
.reply(200, {items: prs})
132197
.post(`/repos/${owner}/${repo}/issues/1/comments`, {
@@ -163,9 +228,9 @@ test.serial('Ignore errors when adding comments and closing issues', async t =>
163228
const releases = [{name: 'GitHub release', url: 'https://github.com/release'}];
164229
const github = authenticate()
165230
.get(
166-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
167-
'type:pr'
168-
)}`
231+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
232+
.map(commit => commit.hash)
233+
.join('+')}`
169234
)
170235
.reply(200, {items: prs})
171236
.post(`/repos/${owner}/${repo}/issues/1/comments`, {body: /This PR is included/})
@@ -213,9 +278,9 @@ test.serial('Close open issues when a release is successful', async t => {
213278
const releases = [{name: 'GitHub release', url: 'https://github.com/release'}];
214279
const github = authenticate()
215280
.get(
216-
`/search/issues?q=${commits.map(commit => commit.hash).join('+')}+${escape(`repo:${owner}/${repo}`)}+${escape(
217-
'type:pr'
218-
)}`
281+
`/search/issues?q=${escape(`repo:${owner}/${repo}`)}+${escape('type:pr')}+${commits
282+
.map(commit => commit.hash)
283+
.join('+')}`
219284
)
220285
.reply(200, {items: []})
221286
.get(

0 commit comments

Comments
 (0)