@@ -28,6 +28,9 @@ export default class GitHub {
28
28
const [ owner , repo ] = repository . split ( '/' ) ;
29
29
30
30
this . commonParams = { owner, repo } ;
31
+
32
+ this . issuesCache = new Map ( ) ;
33
+ this . cacheLoadedPromise = null ;
31
34
}
32
35
33
36
async initialize ( ) {
@@ -53,6 +56,49 @@ export default class GitHub {
53
56
}
54
57
}
55
58
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
+
56
102
async getRepositoryLabels ( ) {
57
103
const { data : labels } = await this . octokit . request ( 'GET /repos/{owner}/{repo}/labels' , { ...this . commonParams } ) ;
58
104
@@ -69,40 +115,42 @@ export default class GitHub {
69
115
}
70
116
71
117
async createIssue ( { title, description : body , labels } ) {
118
+ await this . ensureCacheLoaded ( ) ;
119
+
72
120
const { data : issue } = await this . octokit . request ( 'POST /repos/{owner}/{repo}/issues' , {
73
121
...this . commonParams ,
74
122
title,
75
123
body,
76
124
labels,
77
125
} ) ;
78
126
127
+ this . issuesCache . set ( issue . title , issue ) ;
128
+
79
129
return issue ;
80
130
}
81
131
82
132
async updateIssue ( issue , { state, labels } ) {
133
+ await this . ensureCacheLoaded ( ) ;
134
+
83
135
const { data : updatedIssue } = await this . octokit . request ( 'PATCH /repos/{owner}/{repo}/issues/{issue_number}' , {
84
136
...this . commonParams ,
85
137
issue_number : issue . number ,
86
138
state,
87
139
labels,
88
140
} ) ;
89
141
142
+ this . issuesCache . set ( updatedIssue . title , updatedIssue ) ;
143
+
90
144
return updatedIssue ;
91
145
}
92
146
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 ;
103
149
}
104
150
105
151
async addCommentToIssue ( { issue, comment : body } ) {
152
+ await this . ensureCacheLoaded ( ) ;
153
+
106
154
const { data : comment } = await this . octokit . request ( 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments' , {
107
155
...this . commonParams ,
108
156
issue_number : issue . number ,
@@ -114,25 +162,30 @@ export default class GitHub {
114
162
115
163
async closeIssueWithCommentIfExists ( { title, comment } ) {
116
164
try {
117
- const openedIssue = await this . getIssue ( { title , state : GitHub . ISSUE_STATE_OPEN } ) ;
165
+ await this . ensureCacheLoaded ( ) ;
118
166
119
- if ( ! openedIssue ) {
167
+ const issue = this . getIssue ( title ) ;
168
+
169
+ if ( ! issue || issue . state == GitHub . ISSUE_STATE_CLOSED ) {
120
170
return ;
121
171
}
122
172
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 } ) ;
125
176
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 } ` ) ;
128
179
} catch ( error ) {
129
- logger . error ( `Failed to update issue "${ title } ": ${ error . message } ` ) ;
180
+ logger . error ( `Failed to close issue with comment "${ title } ": ${ error . stack } ` ) ;
130
181
}
131
182
}
132
183
133
184
async createOrUpdateIssue ( { title, description, label } ) {
134
185
try {
135
- const issue = await this . getIssue ( { title, state : GitHub . ISSUE_STATE_ALL } ) ;
186
+ await this . ensureCacheLoaded ( ) ;
187
+
188
+ const issue = this . getIssue ( title ) ;
136
189
137
190
if ( ! issue ) {
138
191
const createdIssue = await this . createIssue ( { title, description, labels : [ label ] } ) ;
@@ -141,23 +194,22 @@ export default class GitHub {
141
194
}
142
195
143
196
const managedLabelsNames = this . MANAGED_LABELS . map ( label => label . name ) ;
197
+
144
198
const labelsNotManagedToKeep = issue . labels . map ( label => label . name ) . filter ( label => ! managedLabelsNames . includes ( label ) ) ;
145
199
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
146
200
147
201
if ( issue . state !== GitHub . ISSUE_STATE_CLOSED && managedLabel ?. name === label ) {
148
202
return ;
149
203
}
150
204
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
+
156
207
await this . addCommentToIssue ( { issue, comment : description } ) ;
157
208
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 } ` ) ;
159
211
} catch ( error ) {
160
- logger . error ( `Failed to update issue "${ title } ": ${ error . message } ` ) ;
212
+ logger . error ( `Failed to update issue "${ title } ": ${ error . stack } ` ) ;
161
213
}
162
214
}
163
215
}
0 commit comments