Skip to content

Commit b63d613

Browse files
committed
Cache issues to reduce API calls
1 parent dd19f93 commit b63d613

File tree

3 files changed

+207
-153
lines changed

3 files changed

+207
-153
lines changed

src/reporter/github.js

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export default class GitHub {
2828
const [ owner, repo ] = repository.split('/');
2929

3030
this.commonParams = { owner, repo };
31+
32+
this.issuesCache = new Map();
33+
this.cacheLoadedPromise = null;
3134
}
3235

3336
async initialize() {
@@ -53,6 +56,49 @@ export default class GitHub {
5356
}
5457
}
5558

59+
async loadAllIssues() {
60+
try {
61+
const issues = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
62+
...this.commonParams,
63+
state: GitHub.ISSUE_STATE_ALL,
64+
per_page: 100,
65+
});
66+
67+
const onlyIssues = issues.filter(issue => !issue.pull_request); // Filter out pull requests since GitHub treats them as a special type of issue
68+
69+
onlyIssues.forEach(issue => {
70+
const cachedIssue = this.issuesCache.get(issue.title);
71+
72+
if (!cachedIssue || new Date(issue.created_at) < new Date(cachedIssue.created_at)) { // Only cache the oldest issue if there are duplicates
73+
this.issuesCache.set(issue.title, issue);
74+
}
75+
});
76+
77+
logger.info(`Cached ${onlyIssues.length} issues from the repository`);
78+
} catch (error) {
79+
logger.error(`Failed to load issues: ${error.message}`);
80+
}
81+
}
82+
83+
async refreshIssuesCache() {
84+
try {
85+
logger.info('Refreshing issues cache from GitHub…');
86+
this.issuesCache.clear();
87+
this.cacheLoadedPromise = this.loadAllIssues();
88+
await this.cacheLoadedPromise;
89+
logger.info('Issues cache refreshed successfully');
90+
} catch (error) {
91+
logger.error(`Failed to refresh issues cache: ${error.message}`);
92+
}
93+
}
94+
95+
async ensureCacheLoaded() {
96+
if (!this.cacheLoadedPromise) {
97+
this.cacheLoadedPromise = this.loadAllIssues();
98+
}
99+
await this.cacheLoadedPromise;
100+
}
101+
56102
async getRepositoryLabels() {
57103
const { data: labels } = await this.octokit.request('GET /repos/{owner}/{repo}/labels', { ...this.commonParams });
58104

@@ -69,40 +115,42 @@ export default class GitHub {
69115
}
70116

71117
async createIssue({ title, description: body, labels }) {
118+
await this.ensureCacheLoaded();
119+
72120
const { data: issue } = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
73121
...this.commonParams,
74122
title,
75123
body,
76124
labels,
77125
});
78126

127+
this.issuesCache.set(issue.title, issue);
128+
79129
return issue;
80130
}
81131

82132
async updateIssue(issue, { state, labels }) {
133+
await this.ensureCacheLoaded();
134+
83135
const { data: updatedIssue } = await this.octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
84136
...this.commonParams,
85137
issue_number: issue.number,
86138
state,
87139
labels,
88140
});
89141

142+
this.issuesCache.set(updatedIssue.title, updatedIssue);
143+
90144
return updatedIssue;
91145
}
92146

93-
async getIssue({ title, ...searchParams }) {
94-
const issues = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
95-
...this.commonParams,
96-
per_page: 100,
97-
...searchParams,
98-
}, response => response.data);
99-
100-
const [issue] = issues.filter(item => item.title === title); // Since only one is expected, use the first one
101-
102-
return issue;
147+
getIssue(title) {
148+
return this.issuesCache.get(title) || null;
103149
}
104150

105151
async addCommentToIssue({ issue, comment: body }) {
152+
await this.ensureCacheLoaded();
153+
106154
const { data: comment } = await this.octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
107155
...this.commonParams,
108156
issue_number: issue.number,
@@ -114,25 +162,30 @@ export default class GitHub {
114162

115163
async closeIssueWithCommentIfExists({ title, comment }) {
116164
try {
117-
const openedIssue = await this.getIssue({ title, state: GitHub.ISSUE_STATE_OPEN });
165+
await this.ensureCacheLoaded();
118166

119-
if (!openedIssue) {
167+
const issue = this.getIssue(title);
168+
169+
if (!issue || issue.state == GitHub.ISSUE_STATE_CLOSED) {
120170
return;
121171
}
122172

123-
await this.addCommentToIssue({ issue: openedIssue, comment });
124-
logger.info(`Added comment to issue #${openedIssue.number}: ${openedIssue.html_url}`);
173+
await this.addCommentToIssue({ issue, comment });
174+
175+
const updatedIssue = await this.updateIssue(issue, { state: GitHub.ISSUE_STATE_CLOSED });
125176

126-
await this.updateIssue(openedIssue, { state: GitHub.ISSUE_STATE_CLOSED });
127-
logger.info(`Closed issue #${openedIssue.number}: ${openedIssue.html_url}`);
177+
this.issuesCache.set(updatedIssue.title, updatedIssue);
178+
logger.info(`Closed issue with comment #${updatedIssue.number}: ${updatedIssue.html_url}`);
128179
} catch (error) {
129-
logger.error(`Failed to update issue "${title}": ${error.message}`);
180+
logger.error(`Failed to close issue with comment "${title}": ${error.stack}`);
130181
}
131182
}
132183

133184
async createOrUpdateIssue({ title, description, label }) {
134185
try {
135-
const issue = await this.getIssue({ title, state: GitHub.ISSUE_STATE_ALL });
186+
await this.ensureCacheLoaded();
187+
188+
const issue = this.getIssue(title);
136189

137190
if (!issue) {
138191
const createdIssue = await this.createIssue({ title, description, labels: [label] });
@@ -141,23 +194,22 @@ export default class GitHub {
141194
}
142195

143196
const managedLabelsNames = this.MANAGED_LABELS.map(label => label.name);
197+
144198
const labelsNotManagedToKeep = issue.labels.map(label => label.name).filter(label => !managedLabelsNames.includes(label));
145199
const [managedLabel] = issue.labels.filter(label => managedLabelsNames.includes(label.name)); // It is assumed that only one specific reason for failure is possible at a time, making managed labels mutually exclusive
146200

147201
if (issue.state !== GitHub.ISSUE_STATE_CLOSED && managedLabel?.name === label) {
148202
return;
149203
}
150204

151-
await this.updateIssue(issue, {
152-
state: GitHub.ISSUE_STATE_OPEN,
153-
labels: [ label, ...labelsNotManagedToKeep ],
154-
});
155-
logger.info(`Updated issue #${issue.number}: ${issue.html_url}`);
205+
const updatedIssue = await this.updateIssue(issue, { state: GitHub.ISSUE_STATE_OPEN, labels: [ label, ...labelsNotManagedToKeep ].filter(label => label) });
206+
156207
await this.addCommentToIssue({ issue, comment: description });
157208

158-
logger.info(`Added comment to issue #${issue.number}: ${issue.html_url}`);
209+
this.issuesCache.set(updatedIssue.title, updatedIssue);
210+
logger.info(`Updated issue with comment #${updatedIssue.number}: ${updatedIssue.html_url}`);
159211
} catch (error) {
160-
logger.error(`Failed to update issue "${title}": ${error.message}`);
212+
logger.error(`Failed to update issue "${title}": ${error.stack}`);
161213
}
162214
}
163215
}

0 commit comments

Comments
 (0)