@goodandready/dsh-kanban 0.2.19 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/board-tool.js +18 -48
- package/lib/client.js +85 -176
- package/lib/commands.js +21 -38
- package/lib/config.js +29 -54
- package/lib/cron.js +13 -13
- package/lib/events.js +1 -1
- package/lib/filters.js +58 -82
- package/lib/git-provider.js +23 -23
- package/lib/gitea.js +51 -109
- package/lib/github.js +27 -31
- package/lib/import.js +46 -67
- package/lib/index.js +66 -132
- package/lib/intake.js +18 -26
- package/lib/launcher.js +100 -167
- package/lib/lifecycle.js +11 -17
- package/lib/markdown.js +16 -23
- package/lib/metrics.js +23 -25
- package/lib/order.js +25 -28
- package/lib/outbound.js +25 -26
- package/lib/plan.js +23 -45
- package/lib/power.js +2 -2
- package/lib/progress-dump.js +17 -17
- package/lib/routes.js +86 -145
- package/lib/store.js +59 -77
- package/lib/sync.js +56 -98
- package/lib/time.js +8 -17
- package/lib/transitions.js +36 -57
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +57 -57
- package/lib/worktree.js +58 -58
- package/package.json +1 -1
package/lib/gitea.js
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Gitea HTTP API client.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Standalone module without external dependencies, communicating directly with Gitea v1 API.
|
|
4
|
+
// Network calls use injectable fetch (`fetchImpl`), and token resolution is provided via callback.
|
|
5
|
+
// The client can be unit tested without making real network requests.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Токен в настройках не хранится: там только ИМЯ учётной записи. Наружу
|
|
11
|
-
// значение не отдаётся никогда, включая сообщения об ошибках.
|
|
7
|
+
// Secrets are not stored in settings; only the credential name is stored.
|
|
8
|
+
// Token values are never emitted to logs or error messages.
|
|
12
9
|
|
|
13
|
-
/**
|
|
10
|
+
/** Strip trailing slashes from base URL. */
|
|
14
11
|
export function normalizeBaseUrl(url) {
|
|
15
12
|
return String(url || '').trim().replace(/\/+$/, '')
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
|
-
*
|
|
16
|
+
* Path segment sanitized for URL interpolation.
|
|
20
17
|
*
|
|
21
|
-
* `owner`, `repo
|
|
22
|
-
*
|
|
23
|
-
* куда собирались.
|
|
18
|
+
* `owner`, `repo`, and issue index originate from external data or user input.
|
|
19
|
+
* Sanitization prevents directory traversal or path manipulation.
|
|
24
20
|
*
|
|
25
|
-
* @returns {string|undefined}
|
|
21
|
+
* @returns {string|undefined} encoded segment or undefined if invalid
|
|
26
22
|
*/
|
|
27
23
|
export function safeSegment(value) {
|
|
28
24
|
const s = String(value ?? '').trim()
|
|
@@ -31,27 +27,23 @@ export function safeSegment(value) {
|
|
|
31
27
|
return encodeURIComponent(s)
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
/**
|
|
30
|
+
/** Credential reference name (e.g. env var name), distinct from the raw secret. */
|
|
35
31
|
export function isCredentialName(value) {
|
|
36
32
|
const str = String(value || '')
|
|
37
33
|
if (looksLikeSecret(str)) return false
|
|
38
34
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str)
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
/**
|
|
37
|
+
/** Check if value looks like a raw token secret rather than a credential reference name. */
|
|
42
38
|
export function looksLikeSecret(value) {
|
|
43
39
|
return /^[A-Za-z0-9_-]{32,}$/.test(String(value || '').trim())
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* Сверху те, где работа идёт прямо сейчас. Признак «идёт» выбран самый честный
|
|
50
|
-
* из доступных без лишних запросов: есть открытые задачи И репозиторий недавно
|
|
51
|
-
* трогали. Архивные уходят вниз всегда — там работы не ведут по определению.
|
|
43
|
+
* Rank repositories by activity.
|
|
52
44
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
45
|
+
* Places active repositories with open tasks and recent updates at the top.
|
|
46
|
+
* Archived repositories are sorted to the bottom.
|
|
55
47
|
*/
|
|
56
48
|
export function rankRepos(repos) {
|
|
57
49
|
const rank = (r) => {
|
|
@@ -69,12 +61,12 @@ export function rankRepos(repos) {
|
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
/**
|
|
72
|
-
*
|
|
64
|
+
* Gitea client factory.
|
|
73
65
|
*
|
|
74
66
|
* @param {object} options
|
|
75
|
-
* @param {() => object} options.getConfig
|
|
76
|
-
* @param {(name: string) => Promise<string>} options.resolveToken
|
|
77
|
-
* @param {Function} [options.fetchImpl]
|
|
67
|
+
* @param {() => object} options.getConfig current plugin configuration
|
|
68
|
+
* @param {(name: string) => Promise<string>} options.resolveToken credential resolver callback
|
|
69
|
+
* @param {Function} [options.fetchImpl] fetch implementation
|
|
78
70
|
* @param {number} [options.timeoutMs]
|
|
79
71
|
*/
|
|
80
72
|
export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch, timeoutMs = 30_000 }) {
|
|
@@ -92,8 +84,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
92
84
|
const res = await fetchImpl(`${baseUrl}/api/v1${path}`, {
|
|
93
85
|
method,
|
|
94
86
|
headers: {
|
|
95
|
-
//
|
|
96
|
-
// отдаёт «Missing API key».
|
|
87
|
+
// Use header authorization rather than basic auth to avoid 'Missing API key' on private instances.
|
|
97
88
|
'Authorization': `token ${token}`,
|
|
98
89
|
'Content-Type': 'application/json',
|
|
99
90
|
'Accept': 'application/json',
|
|
@@ -105,7 +96,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
105
96
|
let payload
|
|
106
97
|
try { payload = text.trim() === '' ? null : JSON.parse(text) } catch { payload = null }
|
|
107
98
|
if (!res.ok) {
|
|
108
|
-
//
|
|
99
|
+
// Do not leak raw response body in error messages to prevent echoing sensitive headers.
|
|
109
100
|
throw Object.assign(new Error(`Gitea HTTP error ${res.status}`), { code: 'http', status: res.status })
|
|
110
101
|
}
|
|
111
102
|
return payload
|
|
@@ -124,18 +115,13 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
124
115
|
}
|
|
125
116
|
|
|
126
117
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* Gitea отдаёт списки порциями, и запрос без перелистывания молча теряет
|
|
130
|
-
* хвост. Признак конца — неполная страница: инстанс отдал меньше, чем
|
|
131
|
-
* просили, значит дальше пусто. Верхний предел стоит, чтобы очень большой
|
|
132
|
-
* ответ не превратил сверку в бесконечный поход.
|
|
118
|
+
* Paginate and collect all items from Gitea endpoints up to maxPages.
|
|
133
119
|
*
|
|
134
120
|
* @param {object} options
|
|
135
|
-
* @param {(page: number, size: number) => string} options.buildPath
|
|
136
|
-
* @param {number} [options.limit]
|
|
137
|
-
* @param {number} [options.maxPages]
|
|
138
|
-
* @returns {Promise<Array>}
|
|
121
|
+
* @param {(page: number, size: number) => string} options.buildPath page path builder
|
|
122
|
+
* @param {number} [options.limit] page size
|
|
123
|
+
* @param {number} [options.maxPages] maximum pages to retrieve
|
|
124
|
+
* @returns {Promise<Array>} concatenated results from all pages
|
|
139
125
|
*/
|
|
140
126
|
async function collectPages({ buildPath, limit, maxPages = 10 }) {
|
|
141
127
|
const size = Math.min(Math.max(Number(limit) || 50, 1), 50)
|
|
@@ -149,10 +135,8 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
149
135
|
return out
|
|
150
136
|
}
|
|
151
137
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// репозиториев, — а резолв токена это поход в хранилище учётных данных.
|
|
155
|
-
// Пока адрес и имя учётной записи не менялись, ответ не мог измениться.
|
|
138
|
+
// Cache configuration check result keyed on URL and token reference to avoid
|
|
139
|
+
// repeated credential lookups on hot paths.
|
|
156
140
|
let configuredCache
|
|
157
141
|
const configuredKey = () => {
|
|
158
142
|
const config = getConfig()
|
|
@@ -160,7 +144,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
160
144
|
}
|
|
161
145
|
|
|
162
146
|
return {
|
|
163
|
-
/**
|
|
147
|
+
/** Check whether access is configured. Token value is never exposed. */
|
|
164
148
|
async isConfigured() {
|
|
165
149
|
const key = configuredKey()
|
|
166
150
|
if (configuredCache?.key === key) return configuredCache.value
|
|
@@ -173,46 +157,38 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
173
157
|
},
|
|
174
158
|
|
|
175
159
|
async listIssues({ owner, repo, state = 'open', limit = 50 }) {
|
|
176
|
-
// API
|
|
177
|
-
// Перелистываем до конца: при 50+ открытых issue хвост иначе теряется
|
|
178
|
-
// молча, и подхват задач смотрит на неполную картину.
|
|
160
|
+
// API returns issues and PRs together; filter out pull requests and paginate completely.
|
|
179
161
|
const rows = await collectPages({
|
|
180
162
|
limit,
|
|
181
163
|
buildPath: (page, size) => repoPath(owner, repo,
|
|
182
|
-
`/issues?state=${encodeURIComponent(state)}&page=${page}&limit=${size}
|
|
164
|
+
`/issues?state=${encodeURIComponent(state)}&page=${page}&limit=${size}`),
|
|
183
165
|
})
|
|
184
|
-
return rows.filter((
|
|
166
|
+
return rows.filter((i) => i.pull_request === undefined)
|
|
185
167
|
},
|
|
186
168
|
|
|
187
169
|
async getIssue({ owner, repo, index }) {
|
|
188
170
|
const n = safeSegment(index)
|
|
189
|
-
if (n === undefined) throw Object.assign(new Error('
|
|
171
|
+
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
190
172
|
return request('GET', repoPath(owner, repo, `/issues/${n}`))
|
|
191
173
|
},
|
|
192
174
|
|
|
193
175
|
async comment({ owner, repo, index, body }) {
|
|
194
176
|
const n = safeSegment(index)
|
|
195
|
-
if (n === undefined) throw Object.assign(new Error('
|
|
177
|
+
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
196
178
|
return request('POST', repoPath(owner, repo, `/issues/${n}/comments`), { body })
|
|
197
179
|
},
|
|
198
180
|
|
|
199
|
-
/**
|
|
181
|
+
/** Create issue. Labels are omitted here to respect repository conventions. */
|
|
200
182
|
async createIssue({ owner, repo, title, body }) {
|
|
201
183
|
const name = String(title ?? '').trim()
|
|
202
|
-
if (name === '') throw Object.assign(new Error('
|
|
184
|
+
if (name === '') throw Object.assign(new Error('empty title'), { code: 'bad-title' })
|
|
203
185
|
return request('POST', repoPath(owner, repo, '/issues'), { title: name, body: String(body ?? '') })
|
|
204
186
|
},
|
|
205
187
|
|
|
206
|
-
/**
|
|
207
|
-
* Завести репозиторий в организации.
|
|
208
|
-
*
|
|
209
|
-
* Приватный и пустой — так решил владелец: расширить права потом дешевле,
|
|
210
|
-
* чем убирать лишнее из публичного, а пустой репозиторий честнее чужих
|
|
211
|
-
* заготовок.
|
|
212
|
-
*/
|
|
188
|
+
/** Create private, empty repository in organization. */
|
|
213
189
|
async createRepo({ owner, name, description = '' }) {
|
|
214
190
|
const safe = safeSegment(name)
|
|
215
|
-
if (safe === undefined) throw Object.assign(new Error('
|
|
191
|
+
if (safe === undefined) throw Object.assign(new Error('invalid name'), { code: 'bad-segment' })
|
|
216
192
|
return request('POST', `/orgs/${safeSegment(owner)}/repos`, {
|
|
217
193
|
name: safe, description, private: true, auto_init: false,
|
|
218
194
|
})
|
|
@@ -220,39 +196,27 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
220
196
|
|
|
221
197
|
async closeIssue({ owner, repo, index }) {
|
|
222
198
|
const n = safeSegment(index)
|
|
223
|
-
if (n === undefined) throw Object.assign(new Error('
|
|
199
|
+
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
224
200
|
return request('PATCH', repoPath(owner, repo, `/issues/${n}`), { state: 'closed' })
|
|
225
201
|
},
|
|
226
202
|
|
|
227
|
-
/**
|
|
228
|
-
* Назначить ответственного за issue.
|
|
229
|
-
*
|
|
230
|
-
* Пустой список снимает назначение: Gitea понимает это как «никто», и
|
|
231
|
-
* доска обязана уметь то же самое — иначе снять с себя задачу можно было
|
|
232
|
-
* бы только руками в Gitea.
|
|
233
|
-
*/
|
|
203
|
+
/** Set issue assignees. Empty array clears assignees. */
|
|
234
204
|
async setAssignees({ owner, repo, index, logins }) {
|
|
235
205
|
const n = safeSegment(index)
|
|
236
|
-
if (n === undefined) throw Object.assign(new Error('
|
|
206
|
+
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
237
207
|
const list = (Array.isArray(logins) ? logins : [])
|
|
238
208
|
.filter((one) => typeof one === 'string' && one !== '')
|
|
239
209
|
return request('PATCH', repoPath(owner, repo, `/issues/${n}`), { assignees: list })
|
|
240
210
|
},
|
|
241
211
|
|
|
242
|
-
/**
|
|
243
|
-
* Кто мы для Gitea.
|
|
244
|
-
*
|
|
245
|
-
* «Взять себе» на доске значит «назначить того, чьим токеном доска ходит»:
|
|
246
|
-
* своего понятия пользователя у харнесса нет, и выдумывать его вместо
|
|
247
|
-
* ответа сервера нельзя.
|
|
248
|
-
*/
|
|
212
|
+
/** Authenticated user identity for the token. */
|
|
249
213
|
async me() {
|
|
250
214
|
const row = await request('GET', '/user')
|
|
251
215
|
const login = row?.login
|
|
252
216
|
return typeof login === 'string' ? login : ''
|
|
253
217
|
},
|
|
254
218
|
|
|
255
|
-
/**
|
|
219
|
+
/** List organizations accessible to the token. */
|
|
256
220
|
async listOrgs() {
|
|
257
221
|
const rows = await request('GET', '/user/orgs?limit=50')
|
|
258
222
|
return (Array.isArray(rows) ? rows : [])
|
|
@@ -260,14 +224,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
260
224
|
.filter((name) => name !== '')
|
|
261
225
|
},
|
|
262
226
|
|
|
263
|
-
/**
|
|
264
|
-
* Репозитории организации с числом открытых задач.
|
|
265
|
-
*
|
|
266
|
-
* Число приходит в том же ответе, и по нему отсеиваются пустые: ходить за
|
|
267
|
-
* задачами в репозиторий, где их нет, — полсотни запросов впустую каждые
|
|
268
|
-
* две минуты. Перелистываем: при 100+ репозиториях одна страница теряла бы
|
|
269
|
-
* хвост, и доска смотрела бы на неполную организацию.
|
|
270
|
-
*/
|
|
227
|
+
/** List organization repositories with open issue counts, paginating to handle large orgs. */
|
|
271
228
|
async listOrgRepos({ owner, limit = 100 }) {
|
|
272
229
|
const rows = await collectPages({
|
|
273
230
|
limit,
|
|
@@ -280,7 +237,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
280
237
|
})).filter((r) => r.name !== '')
|
|
281
238
|
},
|
|
282
239
|
|
|
283
|
-
/**
|
|
240
|
+
/** List pull requests for repository. */
|
|
284
241
|
async listPulls({ owner, repo, state = 'all', limit = 50 }) {
|
|
285
242
|
return collectPages({
|
|
286
243
|
limit,
|
|
@@ -289,13 +246,13 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
289
246
|
})
|
|
290
247
|
},
|
|
291
248
|
|
|
292
|
-
/**
|
|
249
|
+
/** Create pull request in repository. */
|
|
293
250
|
async createPullRequest({ owner, repo, title, body, head, base = 'main' }) {
|
|
294
251
|
const name = String(title ?? '').trim()
|
|
295
|
-
if (name === '') throw Object.assign(new Error('
|
|
252
|
+
if (name === '') throw Object.assign(new Error('empty title'), { code: 'bad-title' })
|
|
296
253
|
const h = String(head ?? '').trim()
|
|
297
254
|
const b = String(base ?? 'main').trim()
|
|
298
|
-
if (!h) throw Object.assign(new Error('
|
|
255
|
+
if (!h) throw Object.assign(new Error('head branch required'), { code: 'bad-head' })
|
|
299
256
|
return request('POST', repoPath(owner, repo, '/pulls'), {
|
|
300
257
|
title: name,
|
|
301
258
|
body: String(body ?? ''),
|
|
@@ -304,13 +261,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
304
261
|
})
|
|
305
262
|
},
|
|
306
263
|
|
|
307
|
-
/**
|
|
308
|
-
* Ветки репозитория: по ним видно, начата ли работа и сделан ли cleanup.
|
|
309
|
-
*
|
|
310
|
-
* Ветки перелистываются не ради полноты списка, а ради правды наблюдения:
|
|
311
|
-
* `branchOfTask` не найдёт ветку задачи на усечённой странице и сочтёт её
|
|
312
|
-
* отсутствующей — карточка уедет не туда на пустом месте.
|
|
313
|
-
*/
|
|
264
|
+
/** List repository branches with pagination to avoid false negatives when detecting active branches. */
|
|
314
265
|
async listBranches({ owner, repo, limit = 100 }) {
|
|
315
266
|
const rows = await collectPages({
|
|
316
267
|
limit,
|
|
@@ -319,16 +270,7 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
319
270
|
return rows.map((b) => ({ name: b?.name ?? '' })).filter((b) => b.name !== '')
|
|
320
271
|
},
|
|
321
272
|
|
|
322
|
-
/**
|
|
323
|
-
* Репозитории инстанса, все страницы.
|
|
324
|
-
*
|
|
325
|
-
* Одной страницы не хватает: Gitea отдаёт их порциями, и запрос без
|
|
326
|
-
* перелистывания молча теряет хвост. Верхний предел стоит, чтобы очень
|
|
327
|
-
* большой инстанс не превратил открытие диалога в долгую загрузку.
|
|
328
|
-
*
|
|
329
|
-
* Число открытых задач и время правки приходят в том же ответе — считать
|
|
330
|
-
* их отдельными запросами не нужно.
|
|
331
|
-
*/
|
|
273
|
+
/** Search repositories across instance with pagination up to maxPages. */
|
|
332
274
|
async searchRepos({ query, limit = 50, maxPages = 10 }) {
|
|
333
275
|
const q = encodeURIComponent(String(query || ''))
|
|
334
276
|
const size = Math.min(Math.max(Number(limit) || 50, 1), 50)
|
package/lib/github.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// от исчерпания часового лимита запросов GitHub (5 000 req/hr).
|
|
7
|
-
//
|
|
8
|
-
// Модуль изолирован от файловой системы и DSH: токен и конфигурация
|
|
9
|
-
// передаются фабрикой createGitHubClient.
|
|
1
|
+
// GitHub REST API v3 client.
|
|
2
|
+
// Equivalent functionality to GiteaClient: reading issues, pull requests,
|
|
3
|
+
// branches, repositories, creating PRs and comments.
|
|
4
|
+
// Supports conditional requests with ETag (If-None-Match) to avoid rate limits.
|
|
5
|
+
// Isolated from filesystem and DSH: token and configuration injected via factory.
|
|
10
6
|
|
|
11
7
|
const DEFAULT_BASE_URL = 'https://api.github.com'
|
|
12
8
|
|
|
@@ -32,16 +28,16 @@ export function isCredentialName(value) {
|
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
|
-
*
|
|
31
|
+
* Create GitHub API client.
|
|
36
32
|
*
|
|
37
33
|
* @param {object} options
|
|
38
|
-
* @param {() => object} options.getConfig
|
|
39
|
-
* @param {(name: string) => Promise<string>} options.resolveToken
|
|
40
|
-
* @param {typeof fetch} [options.fetchImpl]
|
|
41
|
-
* @param {number} [options.timeoutMs]
|
|
34
|
+
* @param {() => object} options.getConfig config getter
|
|
35
|
+
* @param {(name: string) => Promise<string>} options.resolveToken token resolver
|
|
36
|
+
* @param {typeof fetch} [options.fetchImpl] fetch implementation
|
|
37
|
+
* @param {number} [options.timeoutMs] request timeout in ms
|
|
42
38
|
*/
|
|
43
39
|
export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch, timeoutMs = 30_000 }) {
|
|
44
|
-
//
|
|
40
|
+
// ETag conditional request cache: url -> { etag, data }
|
|
45
41
|
const etagCache = new Map()
|
|
46
42
|
|
|
47
43
|
async function request(method, path, body, { useEtag = false } = {}) {
|
|
@@ -144,7 +140,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
144
140
|
return {
|
|
145
141
|
provider: 'github',
|
|
146
142
|
|
|
147
|
-
/**
|
|
143
|
+
/** Whether access token is valid. */
|
|
148
144
|
async isConfigured() {
|
|
149
145
|
const key = configuredKey()
|
|
150
146
|
if (configuredCache?.key === key) return configuredCache.value
|
|
@@ -156,12 +152,12 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
156
152
|
return value
|
|
157
153
|
},
|
|
158
154
|
|
|
159
|
-
/**
|
|
155
|
+
/** Clear ETag cache (e.g. on manual refresh). */
|
|
160
156
|
clearCache() {
|
|
161
157
|
etagCache.clear()
|
|
162
158
|
},
|
|
163
159
|
|
|
164
|
-
/**
|
|
160
|
+
/** List repository issues (excluding PRs). */
|
|
165
161
|
async listIssues({ owner, repo, state = 'open', limit = 50 }) {
|
|
166
162
|
const rows = await collectPages({
|
|
167
163
|
limit,
|
|
@@ -172,28 +168,28 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
172
168
|
return rows.filter((r) => !r.pull_request)
|
|
173
169
|
},
|
|
174
170
|
|
|
175
|
-
/**
|
|
171
|
+
/** Get single issue by number. */
|
|
176
172
|
async getIssue({ owner, repo, index }) {
|
|
177
173
|
const n = safeSegment(index)
|
|
178
174
|
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
179
175
|
return request('GET', repoPath(owner, repo, `/issues/${n}`))
|
|
180
176
|
},
|
|
181
177
|
|
|
182
|
-
/**
|
|
178
|
+
/** Post comment on issue. */
|
|
183
179
|
async comment({ owner, repo, index, body }) {
|
|
184
180
|
const n = safeSegment(index)
|
|
185
181
|
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
186
182
|
return request('POST', repoPath(owner, repo, `/issues/${n}/comments`), { body })
|
|
187
183
|
},
|
|
188
184
|
|
|
189
|
-
/**
|
|
185
|
+
/** Create new issue. */
|
|
190
186
|
async createIssue({ owner, repo, title, body }) {
|
|
191
187
|
const name = String(title ?? '').trim()
|
|
192
188
|
if (name === '') throw Object.assign(new Error('empty issue title'), { code: 'bad-title' })
|
|
193
189
|
return request('POST', repoPath(owner, repo, '/issues'), { title: name, body: String(body ?? '') })
|
|
194
190
|
},
|
|
195
191
|
|
|
196
|
-
/**
|
|
192
|
+
/** Close issue. */
|
|
197
193
|
async closeIssue({ owner, repo, index }) {
|
|
198
194
|
const n = safeSegment(index)
|
|
199
195
|
if (n === undefined) throw Object.assign(new Error('invalid issue number'), { code: 'bad-segment' })
|
|
@@ -201,8 +197,8 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
201
197
|
},
|
|
202
198
|
|
|
203
199
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
200
|
+
* Set assignees for issue.
|
|
201
|
+
* In GitHub REST API PATCH /issues/{n} with assignees replaces the entire list.
|
|
206
202
|
*/
|
|
207
203
|
async setAssignees({ owner, repo, index, logins }) {
|
|
208
204
|
const n = safeSegment(index)
|
|
@@ -212,14 +208,14 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
212
208
|
return request('PATCH', repoPath(owner, repo, `/issues/${n}`), { assignees: list })
|
|
213
209
|
},
|
|
214
210
|
|
|
215
|
-
/**
|
|
211
|
+
/** Login of current token owner. */
|
|
216
212
|
async me() {
|
|
217
213
|
const row = await request('GET', '/user')
|
|
218
214
|
const login = row?.login
|
|
219
215
|
return typeof login === 'string' ? login : ''
|
|
220
216
|
},
|
|
221
217
|
|
|
222
|
-
/**
|
|
218
|
+
/** Organizations available to token. */
|
|
223
219
|
async listOrgs() {
|
|
224
220
|
const rows = await request('GET', '/user/orgs?per_page=50')
|
|
225
221
|
return (Array.isArray(rows) ? rows : [])
|
|
@@ -227,7 +223,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
227
223
|
.filter((name) => name !== '')
|
|
228
224
|
},
|
|
229
225
|
|
|
230
|
-
/**
|
|
226
|
+
/** Organization repositories with open task count. */
|
|
231
227
|
async listOrgRepos({ owner, limit = 100 }) {
|
|
232
228
|
const rows = await collectPages({
|
|
233
229
|
limit,
|
|
@@ -241,7 +237,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
241
237
|
})).filter((r) => r.name !== '')
|
|
242
238
|
},
|
|
243
239
|
|
|
244
|
-
/**
|
|
240
|
+
/** Repository pull requests. */
|
|
245
241
|
async listPulls({ owner, repo, state = 'all', limit = 50 }) {
|
|
246
242
|
return collectPages({
|
|
247
243
|
limit,
|
|
@@ -251,7 +247,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
251
247
|
})
|
|
252
248
|
},
|
|
253
249
|
|
|
254
|
-
/**
|
|
250
|
+
/** Create Pull Request. */
|
|
255
251
|
async createPullRequest({ owner, repo, title, body, head, base = 'main' }) {
|
|
256
252
|
const name = String(title ?? '').trim()
|
|
257
253
|
if (name === '') throw Object.assign(new Error('empty PR title'), { code: 'bad-title' })
|
|
@@ -266,7 +262,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
266
262
|
})
|
|
267
263
|
},
|
|
268
264
|
|
|
269
|
-
/**
|
|
265
|
+
/** List repository branches. */
|
|
270
266
|
async listBranches({ owner, repo, limit = 100 }) {
|
|
271
267
|
const rows = await collectPages({
|
|
272
268
|
limit,
|
|
@@ -275,7 +271,7 @@ export function createGitHubClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
275
271
|
return rows.map((b) => ({ name: String(b?.name ?? '') })).filter((b) => b.name !== '')
|
|
276
272
|
},
|
|
277
273
|
|
|
278
|
-
/**
|
|
274
|
+
/** Search repositories. */
|
|
279
275
|
async searchRepos({ query, limit = 50, maxPages = 5 }) {
|
|
280
276
|
const q = encodeURIComponent(String(query || ''))
|
|
281
277
|
const size = Math.min(Math.max(Number(limit) || 50, 1), 50)
|