diff --git a/lib/modules/platform/github/graphql.ts b/lib/modules/platform/github/graphql.ts index 21d122e786d5ff6e193f976c01784935ab4b3055..09f0addcc06a7c65637a471bf2641850185a2ab6 100644 --- a/lib/modules/platform/github/graphql.ts +++ b/lib/modules/platform/github/graphql.ts @@ -93,6 +93,36 @@ query($owner: String!, $name: String!, $count: Int, $cursor: String) { } `; +export const getIssuesQuery = ` +query( + $owner: String!, + $name: String!, + $user: String!, + $count: Int, + $cursor: String +) { + repository(owner: $owner, name: $name) { + issues( + orderBy: { field: UPDATED_AT, direction: DESC }, + filterBy: { createdBy: $user }, + first: $count, + after: $cursor + ) { + pageInfo { + endCursor + hasNextPage + } + nodes { + number + state + title + body + } + } + } +} +`; + export const vulnerabilityAlertsQuery = (filterByState: boolean): string => ` query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index 24a754a5976a3c1eeb81f35c68ffbb78cc24d85a..11b8efe42f3857bf9a924575de3e69914346bec6 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -1573,76 +1573,36 @@ describe('modules/platform/github/index', () => { }); }); - describe('getIssue()', () => { - it('defaults to use cache', async () => { - const scope = httpMock.scope(githubApiHost); - initRepoMock(scope, 'test/repo'); - await github.initRepo({ repository: 'test/repo' }); - scope - .get('/repos/test/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 1, - title: 'title-1', - body: 'body-1', - state: 'open', - labels: [ - { - name: 'label-1', - }, - ], - }, - { - number: 2, - title: 'title-1', - body: 'body-1', - }, - ]); - - const res = await github.getIssue(1); - expect(res).not.toBeNull(); - }); - - it('cache breaks', async () => { - const scope = httpMock.scope(githubApiHost); - initRepoMock(scope, 'test/repo'); - await github.initRepo({ repository: 'test/repo' }); - scope.get('/repos/test/repo/issues/1').reply(200, { - number: 1, - title: 'title-1b', - body: 'body-1b', - state: 'open', - labels: [ - { - name: 'label-1', - }, - ], - }); - - const res = await github.getIssue(1, false); - expect(res?.body).toBe('body-1b'); - }); - }); - describe('findIssue()', () => { it('returns null if no issue', async () => { httpMock .scope(githubApiHost) - .get('/repos/undefined/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]); + }); const res = await github.findIssue('title-3'); expect(res).toBeNull(); }); @@ -1650,21 +1610,34 @@ describe('modules/platform/github/index', () => { it('finds issue', async () => { httpMock .scope(githubApiHost) - .get('/repos/undefined/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]); + }) + .get('/repos/undefined/issues/2') + .reply(200, { body: 'new-content' }); const res = await github.findIssue('title-2'); expect(res).not.toBeNull(); }); @@ -1676,8 +1649,32 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, []) + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }) .post('/repos/some/repo/issues') .reply(200); const res = await github.ensureIssue({ @@ -1692,21 +1689,32 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'closed', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/1') .reply(404); const res = await github.ensureIssue({ @@ -1720,22 +1728,31 @@ describe('modules/platform/github/index', () => { const scope = httpMock.scope(githubApiHost); initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); - scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'closed', - title: 'title-1', - body: '', + scope.post('/graphql').reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'closed', + title: 'title-1', + }, + ], + }, }, - ]); + }, + }); const once = true; const res = await github.ensureIssue({ title: 'title-1', @@ -1745,44 +1762,26 @@ describe('modules/platform/github/index', () => { expect(res).toBeNull(); }); - it('reopens issue', async () => { - const scope = httpMock.scope(githubApiHost); - initRepoMock(scope, 'some/repo'); - await github.initRepo({ repository: 'some/repo' }); - scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'closed', - title: 'title-1', - body: '', - }, - ]) - .get('/repos/some/repo/issues/1') - .reply(200) - .patch('/repos/some/repo/issues/1') - .reply(200); - const res = await github.ensureIssue({ - title: 'title-1', - body: 'new-content', - }); - expect(res).not.toBeNull(); - }); - it('creates issue with labels', async () => { const scope = httpMock.scope(githubApiHost); initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, []) + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [], + }, + }, + }, + }) .post('/repos/some/repo/issues') .reply(200); const res = await github.ensureIssue({ @@ -1798,27 +1797,37 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 3, - state: 'open', - title: 'title-1', - body: '', - }, - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'closed', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 3, + state: 'open', + title: 'title-1', + }, + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'closed', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/3') .reply(404); const once = true; @@ -1835,21 +1844,32 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/2') .reply(200, { body: 'new-content' }) .patch('/repos/some/repo/issues/2') @@ -1867,21 +1887,32 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/2') .reply(200, { body: 'new-content' }) .patch('/repos/some/repo/issues/2') @@ -1900,21 +1931,32 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/2') .reply(200, { body: 'newer-content' }); const res = await github.ensureIssue({ @@ -1929,21 +1971,32 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-1', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-1', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .patch('/repos/some/repo/issues/1') .reply(200) .get('/repos/some/repo/issues/2') @@ -1960,15 +2013,27 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'closed', - title: 'title-2', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'close', + title: 'title-2', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/2') .reply(200, { body: 'new-content' }) .post('/repos/some/repo/issues') @@ -1987,15 +2052,27 @@ describe('modules/platform/github/index', () => { initRepoMock(scope, 'some/repo'); await github.initRepo({ repository: 'some/repo' }); scope - .get('/repos/some/repo/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + ], + }, + }, }, - ]) + }) .get('/repos/some/repo/issues/2') .reply(200, { body: 'new-content' }); const res = await github.ensureIssue({ @@ -2012,21 +2089,32 @@ describe('modules/platform/github/index', () => { it('closes issue', async () => { httpMock .scope(githubApiHost) - .get('/repos/undefined/issues?creator=undefined&state=all') - .reply(200, [ - { - number: 2, - state: 'open', - title: 'title-2', - body: '', - }, - { - number: 1, - state: 'open', - title: 'title-1', - body: '', + .post('/graphql') + .reply(200, { + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, }, - ]) + }) .patch('/repos/undefined/issues/2') .reply(200); await expect(github.ensureIssueClosing('title-2')).toResolve(); diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index 9790dc31766455a5b099a122cc5b4785e9b816e4..c34abf4abcbf14be17cf62b6ebb2617ba6555edd 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -69,12 +69,12 @@ import { remoteBranchExists } from './branch'; import { coerceRestPr, githubApi } from './common'; import { enableAutoMergeMutation, + getIssuesQuery, repoInfoQuery, vulnerabilityAlertsQuery, } from './graphql'; import { massageMarkdownLinks } from './massage-markdown-links'; import { getPrCache, updatePrCache } from './pr'; -import { IssuesSchema } from './schema'; import type { BranchProtection, CombinedBranchStatus, @@ -1181,22 +1181,23 @@ export async function setBranchStatus({ /* istanbul ignore next */ async function getIssues(): Promise<Issue[]> { - const issuesUrl = `repos/${config.parentRepo ?? config.repository}/issues?creator=${ - config.renovateUsername - }&state=all`; - const result = ( - await githubApi.getJson( - issuesUrl, - { - repoCache: true, - paginate: true, + const result = await githubApi.queryRepoField<Issue>( + getIssuesQuery, + 'issues', + { + variables: { + owner: config.repositoryOwner, + name: config.repositoryName, + user: config.renovateUsername, }, - IssuesSchema, - ) - ).body; + }, + ); logger.debug(`Retrieved ${result.length} issues`); - return result; + return result.map((issue) => ({ + ...issue, + state: issue.state?.toLowerCase(), + })); } export async function getIssueList(): Promise<Issue[]> { @@ -1219,14 +1220,6 @@ export async function getIssue( if (config.hasIssuesEnabled === false) { return null; } - if (useCache) { - const issueList = await getIssueList(); - const issue = issueList.find((i) => i.number === number); - if (issue) { - logger.debug(`Returning issue from cache`); - return issue; - } - } try { const issueBody = ( await githubApi.getJson<{ body: string }>( diff --git a/lib/modules/platform/github/schema.ts b/lib/modules/platform/github/schema.ts deleted file mode 100644 index 7a849fbccc1c8e32342883c58ef07a3a49058843..0000000000000000000000000000000000000000 --- a/lib/modules/platform/github/schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; -import { LooseArray } from '../../../util/schema-utils'; - -export const IssueSchema = z.object({ - number: z.number(), - state: z.string().transform((state) => state.toLowerCase()), - title: z.string(), - body: z.string(), -}); - -export const IssuesSchema = LooseArray(IssueSchema);