Skip to content

Commit c0d3e56

Browse files
committed
Download and cache issue results
1 parent c81a670 commit c0d3e56

File tree

5 files changed

+180
-30
lines changed

5 files changed

+180
-30
lines changed

packages/toolkit/scripts/issue-triage/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ Or for development (builds and runs):
3838
npm run dev
3939
```
4040

41+
### Using Cached Data
42+
43+
The tool automatically saves fetched data to `cache/issues-data.json` after each run. To use cached data instead of fetching fresh data (useful for development and testing):
44+
45+
```bash
46+
npm start -- --use-cache
47+
```
48+
49+
Or with the dev command:
50+
51+
```bash
52+
npm run dev -- --use-cache
53+
```
54+
55+
**Note:** The `--use-cache` flag is primarily for development/testing purposes. It loads data from the cache file instantly without making API calls. If the cache file doesn't exist or is corrupted, the tool will automatically fetch fresh data.
56+
4157
## Project Structure
4258

4359
```

packages/toolkit/scripts/issue-triage/src/github/gh-client.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import { execFile } from 'node:child_process'
66
import { promisify } from 'node:util'
7+
import { promises as fs } from 'node:fs'
8+
import { join, dirname } from 'node:path'
79
import type {
810
GhIssueResponse,
911
GhPullRequestResponse,
@@ -20,6 +22,7 @@ const execFileAsync = promisify(execFile)
2022
const MAX_BUFFER = 10 * 1024 * 1024 // 10MB
2123
const TIMEOUT = 30000 // 30 seconds
2224
const REPO = 'reduxjs/redux-toolkit'
25+
const CACHE_FILE = 'cache/issues-data.json'
2326

2427
/**
2528
* Execute a gh CLI command and return stdout/stderr
@@ -248,17 +251,107 @@ export class GitHubClient {
248251
}
249252
}
250253

254+
/**
255+
* Save issues and PRs to cache file
256+
*/
257+
async saveToCache(issues: Issue[], prs: Issue[]): Promise<void> {
258+
try {
259+
console.log('💾 Saving data to cache...')
260+
261+
// Create cache directory if it doesn't exist
262+
const cacheDir = dirname(CACHE_FILE)
263+
await fs.mkdir(cacheDir, { recursive: true })
264+
265+
// Write cache file with pretty-printed JSON
266+
const cacheData = { issues, prs }
267+
await fs.writeFile(CACHE_FILE, JSON.stringify(cacheData, null, 2), 'utf8')
268+
269+
console.log(
270+
`✓ Saved ${issues.length} issues and ${prs.length} PRs to cache`,
271+
)
272+
} catch (error) {
273+
// Log warning but don't fail - caching is optional
274+
console.warn(
275+
'⚠️ Failed to save cache:',
276+
error instanceof Error ? error.message : error,
277+
)
278+
}
279+
}
280+
281+
/**
282+
* Load issues and PRs from cache file
283+
*/
284+
async loadFromCache(): Promise<{ issues: Issue[]; prs: Issue[] } | null> {
285+
try {
286+
const data = await fs.readFile(CACHE_FILE, 'utf8')
287+
const parsed = JSON.parse(data)
288+
289+
// Validate cache structure
290+
if (
291+
!parsed.issues ||
292+
!parsed.prs ||
293+
!Array.isArray(parsed.issues) ||
294+
!Array.isArray(parsed.prs)
295+
) {
296+
console.warn('⚠️ Cache file has invalid structure, fetching fresh data')
297+
return null
298+
}
299+
300+
// Reconstruct Date objects from ISO strings
301+
const reconstructDates = (item: any): Issue => ({
302+
...item,
303+
created_at: new Date(item.created_at),
304+
updated_at: new Date(item.updated_at),
305+
closed_at: item.closed_at ? new Date(item.closed_at) : null,
306+
merged_at: item.merged_at ? new Date(item.merged_at) : undefined,
307+
})
308+
309+
return {
310+
issues: parsed.issues.map(reconstructDates),
311+
prs: parsed.prs.map(reconstructDates),
312+
}
313+
} catch (error) {
314+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
315+
console.log('⚠️ Cache file not found, fetching fresh data')
316+
} else if (error instanceof SyntaxError) {
317+
console.warn('⚠️ Cache file corrupted, fetching fresh data')
318+
} else {
319+
console.warn(
320+
'⚠️ Failed to load cache:',
321+
error instanceof Error ? error.message : error,
322+
)
323+
}
324+
return null
325+
}
326+
}
327+
251328
/**
252329
* Fetch all open issues and PRs
253330
*/
254-
async fetchAll(): Promise<Issue[]> {
331+
async fetchAll(options?: { useCache?: boolean }): Promise<Issue[]> {
332+
// Try to load from cache if requested
333+
if (options?.useCache) {
334+
console.log('📦 Attempting to use cached data...')
335+
const cached = await this.loadFromCache()
336+
337+
if (cached) {
338+
console.log('📦 Using cached data')
339+
return [...cached.issues, ...cached.prs]
340+
}
341+
}
342+
343+
// Fetch fresh data
344+
console.log('🔄 Fetching fresh data...')
255345
console.log('Fetching all open issues and PRs...')
256346

257347
const [issues, prs] = await Promise.all([
258348
this.fetchOpenIssues(),
259349
this.fetchOpenPRs(),
260350
])
261351

352+
// Save to cache
353+
await this.saveToCache(issues, prs)
354+
262355
return [...issues, ...prs]
263356
}
264357
}

packages/toolkit/scripts/issue-triage/src/github/transformers.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,39 @@ import type {
66
GhIssueResponse,
77
GhPullRequestResponse,
88
GhIssueDetailResponse,
9+
GhComment,
10+
SimplifiedComment,
911
Issue,
1012
DetailedIssue,
1113
} from './types.js'
1214

15+
/**
16+
* Simplify a comment object for caching by removing unnecessary fields
17+
*/
18+
function simplifyComment(comment: GhComment): SimplifiedComment {
19+
return {
20+
author: comment.author.login,
21+
authorAssociation: comment.authorAssociation,
22+
body: comment.body,
23+
createdAt: comment.createdAt,
24+
}
25+
}
26+
1327
/**
1428
* Transform a raw GitHub issue or PR response to our internal format
1529
*/
1630
export function transformIssue(
1731
raw: GhIssueResponse | GhPullRequestResponse,
1832
type: 'issue' | 'pr',
1933
): Issue {
34+
// Handle comments - can be either a count or full array
35+
const commentCount =
36+
typeof raw.comments === 'number' ? raw.comments : raw.comments.length
37+
38+
const simplifiedComments = Array.isArray(raw.comments)
39+
? raw.comments.map(simplifyComment)
40+
: undefined
41+
2042
const base: Issue = {
2143
number: raw.number,
2244
title: raw.title,
@@ -28,8 +50,8 @@ export function transformIssue(
2850
url: raw.url,
2951
author: raw.author.login,
3052
labels: raw.labels.map((label) => label.name),
31-
assignees: raw.assignees.map((assignee) => assignee.login),
32-
comment_count: raw.comments,
53+
comment_count: commentCount,
54+
comments: simplifiedComments,
3355
body: raw.body || '',
3456
}
3557

@@ -62,18 +84,12 @@ export function transformDetailedIssue(
6284
url: raw.url,
6385
author: raw.author.login,
6486
labels: raw.labels.map((label) => label.name),
65-
assignees: raw.assignees.map((assignee) => assignee.login),
6687
comment_count: raw.comments.length,
6788
body: raw.body || '',
6889
}
6990

70-
// Transform comments
71-
const comments = raw.comments.map((comment) => ({
72-
author: comment.author.login,
73-
body: comment.body,
74-
created_at: new Date(comment.createdAt),
75-
updated_at: new Date(comment.updatedAt),
76-
}))
91+
// Transform comments to simplified format
92+
const comments = raw.comments.map(simplifyComment)
7793

7894
return {
7995
...baseIssue,

packages/toolkit/scripts/issue-triage/src/github/types.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@
22
* GitHub API response types and internal data models
33
*/
44

5+
/**
6+
* Comment object from GitHub API
7+
*/
8+
export interface GhComment {
9+
id: string
10+
author: {
11+
login: string
12+
}
13+
authorAssociation: string
14+
body: string
15+
createdAt: string
16+
includesCreatedEdit: boolean
17+
isMinimized: boolean
18+
minimizedReason: string
19+
reactionGroups: Array<{
20+
content: string
21+
users: {
22+
totalCount: number
23+
}
24+
}>
25+
url: string
26+
viewerDidAuthor: boolean
27+
}
28+
29+
/**
30+
* Simplified comment for caching
31+
*/
32+
export interface SimplifiedComment {
33+
author: string
34+
authorAssociation: string
35+
body: string
36+
createdAt: string
37+
}
38+
539
/**
640
* Raw response from `gh issue list --json` command
741
*/
@@ -23,7 +57,7 @@ export interface GhIssueResponse {
2357
assignees: Array<{
2458
login: string
2559
}>
26-
comments: number
60+
comments: number | GhComment[] // Can be count or full array
2761
body: string
2862
}
2963

@@ -77,14 +111,7 @@ export interface GhIssueDetailResponse {
77111
login: string
78112
}>
79113
body: string
80-
comments: Array<{
81-
author: {
82-
login: string
83-
}
84-
body: string
85-
createdAt: string
86-
updatedAt: string
87-
}>
114+
comments: GhComment[]
88115
}
89116

90117
/**
@@ -101,8 +128,8 @@ export interface Issue {
101128
url: string
102129
author: string
103130
labels: string[]
104-
assignees: string[]
105131
comment_count: number
132+
comments?: SimplifiedComment[] // Optional simplified comments for caching
106133
body: string
107134
// PR-specific fields
108135
is_draft?: boolean
@@ -114,10 +141,5 @@ export interface Issue {
114141
* Detailed issue with comments loaded
115142
*/
116143
export interface DetailedIssue extends Issue {
117-
comments: Array<{
118-
author: string
119-
body: string
120-
created_at: Date
121-
updated_at: Date
122-
}>
144+
comments: SimplifiedComment[]
123145
}

packages/toolkit/scripts/issue-triage/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ async function main() {
1212
console.log('==================================\n')
1313

1414
try {
15+
// Parse CLI arguments
16+
const useCache = process.argv.includes('--use-cache')
17+
1518
// Step 1: Verify gh CLI is available and authenticated
1619
console.log('Checking GitHub CLI...')
1720
await checkGhCli()
@@ -22,7 +25,7 @@ async function main() {
2225

2326
// Step 3: Fetch all open issues and PRs
2427
console.log('Fetching data from reduxjs/redux-toolkit...\n')
25-
const allItems = await client.fetchAll()
28+
const allItems = await client.fetchAll({ useCache })
2629

2730
// Step 4: Display summary statistics
2831
const issues = allItems.filter((item) => item.type === 'issue')
@@ -51,9 +54,9 @@ async function main() {
5154
console.log(`Author: ${detailed.author}`)
5255
console.log(`Created: ${detailed.created_at.toISOString()}`)
5356
console.log(`Labels: ${detailed.labels.join(', ') || 'none'}`)
54-
console.log(`Comments: ${detailed.comments.length}`)
57+
console.log(`Comments: ${detailed.comments?.length || 0}`)
5558

56-
if (detailed.comments.length > 0) {
59+
if (detailed.comments && detailed.comments.length > 0) {
5760
console.log(
5861
`Latest comment by: ${detailed.comments[detailed.comments.length - 1].author}`,
5962
)

0 commit comments

Comments
 (0)