master
1import fs from 'node:fs'
2import { GraphqlResponseError } from '@octokit/graphql'
3import { Data } from './data.js'
4import { TaskName } from './task.js'
5import allTasks from './task/index.js'
6
7import { type Context } from './context.js'
8import { createBatcher } from './batch.js'
9import { getOctokit } from './utils.js'
10import { log } from './log.js'
11
12const MAX_ATTEMPTS = 3
13
14export async function processTasks(
15 ctx: Pick<
16 Context,
17 | 'ghUser'
18 | 'ghToken'
19 | 'dataDir'
20 | 'dataFile'
21 | 'dataTasks'
22 | 'taskName'
23 | 'taskSkip'
24 | 'taskParams'
25 >,
26): Promise<[boolean, Data]> {
27 const {
28 ghToken,
29 ghUser: username,
30 dataFile,
31 dataTasks,
32 taskSkip,
33 taskName,
34 taskParams,
35 } = ctx
36
37 if (!ghToken) throw new Error('GitHub token is required for data gathering')
38 if (!username)
39 throw new Error('GitHub username is required for data gathering')
40
41 const taskSkipSet = new Set(taskSkip?.split(',') || [])
42 const octokit = getOctokit(ghToken)
43
44 let data: Data = {
45 user: null!,
46 starredRepositories: [],
47 repos: [],
48 pulls: [],
49 issues: [],
50 issueComments: [],
51 discussionComments: [],
52 }
53
54 if (fs.existsSync(dataFile)) {
55 data = JSON.parse(fs.readFileSync(dataFile, 'utf8')) as Data
56 }
57
58 type Todo = {
59 taskName: TaskName
60 params: any
61 attempts: number
62 }
63
64 let todo: Todo[] = [
65 { taskName: 'user', params: { username }, attempts: 0 },
66 { taskName: 'pulls', params: { username }, attempts: 0 },
67 { taskName: 'issues', params: { username }, attempts: 0 },
68 { taskName: 'issue-comments', params: { username }, attempts: 0 },
69 { taskName: 'discussion-comments', params: { username }, attempts: 0 },
70 { taskName: 'stars', params: { username }, attempts: 0 },
71 ]
72
73 if (taskName && taskParams) {
74 todo = [
75 {
76 taskName: taskName as TaskName,
77 params: Object.fromEntries(new URLSearchParams(taskParams).entries()),
78 attempts: 0,
79 },
80 ]
81 } else if (fs.existsSync(dataTasks)) {
82 const savedTodo = JSON.parse(fs.readFileSync(dataTasks, 'utf8')) as Todo[]
83 if (savedTodo.length > 0) {
84 todo = savedTodo
85 }
86 }
87
88 while (todo.length > 0) {
89 const { taskName, params, attempts } = todo.shift()!
90 if (taskSkipSet.has(taskName)) {
91 log.info(`Skipping task ${taskName}`)
92 continue
93 }
94
95 const task = allTasks.find(({ default: t }) => t.name === taskName)?.default
96 if (!task) {
97 throw new Error(`Unknown task ${taskName}`)
98 }
99
100 const next = (taskName: TaskName, params: any) => {
101 todo.push({ taskName, params, attempts: 0 })
102 }
103 const { batch, flush } = createBatcher(next)
104
105 log.info(
106 `==> Running task ${taskName}`,
107 new URLSearchParams(params).toString(),
108 attempts > 0 ? `(attempt: ${attempts + 1})` : '',
109 )
110 try {
111 await task.run({ octokit, data, next, batch }, params)
112 } catch (e) {
113 let retry = true
114 if (e instanceof GraphqlResponseError) {
115 retry = e.errors?.some((error) => error.type == 'NOT_FOUND') ?? true
116 }
117
118 if (attempts >= MAX_ATTEMPTS || !retry) {
119 log.error(
120 `!!! Failed to run task ${taskName}`,
121 new URLSearchParams(params).toString(),
122 `after ${attempts} attempts`,
123 )
124 log.error(e)
125 } else {
126 log.error(
127 `!!! Failed to run task ${taskName}`,
128 new URLSearchParams(params).toString(),
129 `retrying`,
130 `(will try ${MAX_ATTEMPTS - attempts} more times)`,
131 )
132 log.error(e)
133 todo.push({ taskName, params, attempts: attempts + 1 })
134 }
135 }
136 log.info(`<== Finished ${taskName} (${todo.length} tasks left)`)
137
138 flush()
139
140 fs.writeFileSync(dataFile, JSON.stringify(data, null, 2))
141 fs.writeFileSync(dataTasks, JSON.stringify(todo, null, 2))
142 }
143
144 return [true, data]
145}