@goodandready/dsh-github-ops 0.2.9 → 0.2.10
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/CHANGELOG.md +18 -0
- package/lib/client.js +14 -7
- package/lib/index.js +14 -17
- package/lib/present.js +2 -2
- package/lib/tools/insights.js +1 -1
- package/lib/tools/issues.js +1 -0
- package/lib/tools/pulls.js +5 -1
- package/lib/tools/register-write.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Settings Surface & Volatile Fields on DSH 0.2** (#76, #85): Config fields declare `.volatile()` wrappers and live settings reads are unwrapped via `plainConfig()` to comply with the DSH 0.2 live-value contract and prevent settings from being marked unavailable.
|
|
7
|
+
- **Dead settingsScope.replace Removed** (#100, #86): Removed unused dead `settingsScope.replace` method that swallowed errors.
|
|
8
|
+
- **Client Fetch AbortSignal Timeout** (#101): Attached 15s/120s `AbortSignal` timeouts to all `fetch` calls in `lib/client.js` to prevent the UI from hanging indefinitely on stalled requests.
|
|
9
|
+
- **Git Runner Timeout** (#90): SCM route passes `timeoutMs` (120000ms) to `execFileAsync` git runner options, properly enforcing timeouts on write operations.
|
|
10
|
+
- **Presenter Interpolation Format** (#94): Fixed entity number interpolation in `issue_comment` (`#${number}`) and `pr_merge` (`Merged pull request #${number}`) cards.
|
|
11
|
+
- **GraphQL Thread Mutation Validation** (#88): `resolveReviewThread` validates return thread existence and throws on empty/null responses instead of reporting false success.
|
|
12
|
+
- **Merged PRs Filter** (#89): `mergedPulls` in insights now verifies `mergedAt` presence instead of counting closed-unmerged pull requests.
|
|
13
|
+
- **Auth Device Secret Await** (#84): Fixed missing `await auth.secret()` in `register-write.js` that was passing a Promise instance as OAuth `client_secret`.
|
|
14
|
+
|
|
15
|
+
## 0.2.9
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Audit Findings & Stability**: Pre-release audit bundle addressing review thread mutations, auth credentials and input validation.
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
## 0.2.8
|
|
4
22
|
|
|
5
23
|
### Fixed
|
package/lib/client.js
CHANGED
|
@@ -20,6 +20,13 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
const SCM_PATH = '/dsh-github-ops/scm'
|
|
21
21
|
const PANEL_PATH = '/dsh-github-ops/panel'
|
|
22
22
|
|
|
23
|
+
const fetchWithTimeout = (url, options = {}, timeoutMs = 15000) => {
|
|
24
|
+
const signal = (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function')
|
|
25
|
+
? AbortSignal.timeout(timeoutMs)
|
|
26
|
+
: undefined
|
|
27
|
+
return fetch(url, signal ? { signal, ...options } : options)
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
// The repository a github.com link in the conversation pointed at, so the tab and the
|
|
24
31
|
// panel can follow the human instead of asking again.
|
|
25
32
|
const linkState = { repo: '', enabled: false }
|
|
@@ -283,7 +290,7 @@ window.__ModuleLoader__.load({
|
|
|
283
290
|
setState({ phase: 'error', message: 'fetch is unavailable in this client' })
|
|
284
291
|
return
|
|
285
292
|
}
|
|
286
|
-
|
|
293
|
+
fetchWithTimeout(cwdQuery ? `${SCM_PATH}?${cwdQuery}` : SCM_PATH, { headers: { accept: 'application/json' } })
|
|
287
294
|
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
288
295
|
.then((payload) => setState({ phase: 'ready', payload }))
|
|
289
296
|
.catch((err) => setState({ phase: 'error', message: String((err && err.message) || err) }))
|
|
@@ -296,11 +303,11 @@ window.__ModuleLoader__.load({
|
|
|
296
303
|
if (typeof fetch !== 'function') return
|
|
297
304
|
setBusy(action)
|
|
298
305
|
setActionError('')
|
|
299
|
-
|
|
306
|
+
fetchWithTimeout(SCM_PATH, {
|
|
300
307
|
method: 'POST',
|
|
301
308
|
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
302
309
|
body: JSON.stringify(repoPath ? { action, args: args || {}, cwd: repoPath } : { action, args: args || {} }),
|
|
303
|
-
})
|
|
310
|
+
}, 120000)
|
|
304
311
|
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
305
312
|
.then((payload) => {
|
|
306
313
|
setBusy('')
|
|
@@ -322,7 +329,7 @@ window.__ModuleLoader__.load({
|
|
|
322
329
|
setSelected({ path: file.path, staged })
|
|
323
330
|
setDiff({ phase: 'loading', text: '' })
|
|
324
331
|
const query = `?file=${encodeURIComponent(file.path)}${staged ? '&staged=1' : ''}`
|
|
325
|
-
|
|
332
|
+
fetchWithTimeout(SCM_PATH + query, { headers: { accept: 'application/json' } })
|
|
326
333
|
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
327
334
|
.then((payload) => setDiff({ phase: 'ready', text: payload.diff || '' }))
|
|
328
335
|
.catch((err) => setDiff({ phase: 'error', text: String((err && err.message) || err) }))
|
|
@@ -518,7 +525,7 @@ window.__ModuleLoader__.load({
|
|
|
518
525
|
|
|
519
526
|
const load = React.useCallback(() => {
|
|
520
527
|
if (typeof fetch !== 'function') return
|
|
521
|
-
|
|
528
|
+
fetchWithTimeout(SCM_PATH, { headers: { accept: 'application/json' } })
|
|
522
529
|
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
523
530
|
.then((payload) => setState({ phase: 'ready', payload }))
|
|
524
531
|
.catch(() => setState({ phase: 'idle' }))
|
|
@@ -602,7 +609,7 @@ window.__ModuleLoader__.load({
|
|
|
602
609
|
const call = React.useCallback((params) => {
|
|
603
610
|
if (typeof fetch !== 'function') return Promise.reject(new Error('fetch is unavailable in this client'))
|
|
604
611
|
const query = new URLSearchParams(params).toString()
|
|
605
|
-
return
|
|
612
|
+
return fetchWithTimeout(`${PANEL_PATH}?${query}`, { headers: { accept: 'application/json' } })
|
|
606
613
|
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
607
614
|
}, [])
|
|
608
615
|
|
|
@@ -841,7 +848,7 @@ window.__ModuleLoader__.load({
|
|
|
841
848
|
setStatusError('fetch is unavailable in this client')
|
|
842
849
|
return
|
|
843
850
|
}
|
|
844
|
-
|
|
851
|
+
fetchWithTimeout(STATUS_PATH, { headers: { accept: 'application/json' } })
|
|
845
852
|
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
|
846
853
|
.then((payload) => setAccessStatus(payload))
|
|
847
854
|
.catch((err) => { setAccessStatus(null); setStatusError(String((err && err.message) || err)) })
|
package/lib/index.js
CHANGED
|
@@ -44,11 +44,12 @@ const callContext = new AsyncLocalStorage()
|
|
|
44
44
|
|
|
45
45
|
/** git runner: never throws, always answers with a code and both streams. */
|
|
46
46
|
function makeGitRunner() {
|
|
47
|
-
return async (args, { cwd, env } = {}) => {
|
|
47
|
+
return async (args, { cwd, env, timeoutMs, timeout } = {}) => {
|
|
48
48
|
try {
|
|
49
49
|
const { stdout, stderr } = await execFileAsync('git', args, {
|
|
50
50
|
cwd: cwd || process.cwd(),
|
|
51
51
|
env: { ...process.env, ...(env || {}) },
|
|
52
|
+
timeout: typeof timeoutMs === 'number' ? timeoutMs : (typeof timeout === 'number' ? timeout : undefined),
|
|
52
53
|
maxBuffer: 8 * 1024 * 1024,
|
|
53
54
|
})
|
|
54
55
|
return { code: 0, stdout, stderr }
|
|
@@ -171,25 +172,21 @@ export const Config = Schema.object({
|
|
|
171
172
|
})
|
|
172
173
|
|
|
173
174
|
export function apply(ctx, rawConfig) {
|
|
174
|
-
const config = { ...Config(), ...(rawConfig || {}) }
|
|
175
|
+
const config = plainConfig({ ...Config(), ...(rawConfig || {}) })
|
|
175
176
|
let settingsScope = null
|
|
176
177
|
|
|
177
178
|
ctx.inject(['settings'], (sctx) => {
|
|
178
179
|
try {
|
|
179
180
|
settingsScope = {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return await sctx.settings?.replace?.(name, next, row?.revision);
|
|
190
|
-
} catch {}
|
|
191
|
-
},
|
|
192
|
-
}
|
|
181
|
+
get: () => {
|
|
182
|
+
try {
|
|
183
|
+
const row = sctx.settings?.describe?.().find((r) => r && (r.ns === name || r.ns === '@goodandready/dsh-github-ops'))
|
|
184
|
+
return plainConfig(row?.value ?? config ?? {})
|
|
185
|
+
} catch {
|
|
186
|
+
return plainConfig(config ?? {})
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
193
190
|
} catch (err) {
|
|
194
191
|
ctx.logger?.warn?.('[dsh-github-ops] settings namespace unavailable:', err && err.message)
|
|
195
192
|
}
|
|
@@ -211,9 +208,9 @@ export function apply(ctx, rawConfig) {
|
|
|
211
208
|
function liveConfig() {
|
|
212
209
|
try {
|
|
213
210
|
const fromScope = settingsScope && settingsScope.get && settingsScope.get()
|
|
214
|
-
return { ...config, ...(fromScope || {}) }
|
|
211
|
+
return plainConfig({ ...config, ...(fromScope || {}) })
|
|
215
212
|
} catch {
|
|
216
|
-
return config
|
|
213
|
+
return plainConfig(config)
|
|
217
214
|
}
|
|
218
215
|
}
|
|
219
216
|
|
package/lib/present.js
CHANGED
|
@@ -102,7 +102,7 @@ const PRESENTATIONS = {
|
|
|
102
102
|
},
|
|
103
103
|
issue_comment: {
|
|
104
104
|
call: (args) => pending(`Comment on #${args.number}`, pick(args, ['repository', 'number'])),
|
|
105
|
-
result: (_args, r) => failure('Comment not posted', r) || text(`Commented on #${r.data.id
|
|
105
|
+
result: (_args, r) => failure('Comment not posted', r) || text(`Commented on #${_args?.number || (r.data && r.data.id) || ''}`, `comment id ${r.data.id}`),
|
|
106
106
|
},
|
|
107
107
|
issue_close: {
|
|
108
108
|
call: (args) => pending(`Close #${args.number}`, pick(args, ['repository', 'number', 'stateReason'])),
|
|
@@ -126,7 +126,7 @@ const PRESENTATIONS = {
|
|
|
126
126
|
pr_merge: {
|
|
127
127
|
call: (args) => pending(`Merge pull request #${args.number}`, pick(args, ['repository', 'number', 'method', 'deleteBranch'])),
|
|
128
128
|
result: (_args, r) => failure('Pull request not merged', r)
|
|
129
|
-
|| text(r.data.merged ? `Merged #${
|
|
129
|
+
|| text(r.data.merged ? `Merged pull request #${_args?.number || (r.data && r.data.number) || ''}` : 'Merge not performed', `${r.data.message || ''}${r.data.sha ? `\n${String(r.data.sha).slice(0, 7)}` : ''}${r.data.branchDeleted ? `\nhead branch ${r.data.branchDeleted} deleted` : ''}`),
|
|
130
130
|
},
|
|
131
131
|
gh_review: {
|
|
132
132
|
call: (args) => pending(`Review pull request #${args.number}`, pick(args, ['repository', 'number', 'maxDiffChars'])),
|
package/lib/tools/insights.js
CHANGED
|
@@ -102,7 +102,7 @@ export async function weeklyDigest(client, { owner, repo, days = 7, limit = 10 }
|
|
|
102
102
|
since,
|
|
103
103
|
releases: releases.releases.filter((r) => after(r.publishedAt || r.createdAt)),
|
|
104
104
|
newIssues: issues.issues.filter((i) => after(i.createdAt)),
|
|
105
|
-
mergedPulls: pulls.issues.filter((i) =>
|
|
105
|
+
mergedPulls: pulls.issues.filter((i) => Boolean(i.mergedAt) && after(i.mergedAt)),
|
|
106
106
|
commits: commits.commits,
|
|
107
107
|
}
|
|
108
108
|
}
|
package/lib/tools/issues.js
CHANGED
|
@@ -26,6 +26,7 @@ export function normalizeIssue(data, { mine = '' } = {}) {
|
|
|
26
26
|
createdAt: data.created_at,
|
|
27
27
|
updatedAt: data.updated_at,
|
|
28
28
|
closedAt: data.closed_at,
|
|
29
|
+
mergedAt: (data.pull_request && data.pull_request.merged_at) || data.merged_at || null,
|
|
29
30
|
url: data.html_url,
|
|
30
31
|
body: typeof data.body === 'string' ? data.body : '',
|
|
31
32
|
isMine: mine ? (data.user && data.user.login) === mine : undefined,
|
package/lib/tools/pulls.js
CHANGED
|
@@ -329,8 +329,12 @@ export async function resolveReviewThread(client, { owner, repo, threadId, resol
|
|
|
329
329
|
? data?.resolveReviewThread?.thread
|
|
330
330
|
: data?.unresolveReviewThread?.thread
|
|
331
331
|
|
|
332
|
+
if (!thread) {
|
|
333
|
+
throw new Error(`failed to ${shouldResolve ? 'resolve' : 'unresolve'} review thread: mutation returned no thread`)
|
|
334
|
+
}
|
|
335
|
+
|
|
332
336
|
return {
|
|
333
337
|
threadId,
|
|
334
|
-
isResolved:
|
|
338
|
+
isResolved: Boolean(thread.isResolved),
|
|
335
339
|
}
|
|
336
340
|
}
|
|
@@ -188,7 +188,7 @@ export function registerWriteTools({ tool, asRepo, client, liveConfig, auth }) {
|
|
|
188
188
|
const cfg = authConfig()
|
|
189
189
|
const outcome = await pollDeviceFlow({
|
|
190
190
|
clientId: cfg.clientId || DEFAULT_CLIENT_ID,
|
|
191
|
-
clientSecret: auth.secret(),
|
|
191
|
+
clientSecret: await auth.secret(),
|
|
192
192
|
deviceCode: pending.deviceCode,
|
|
193
193
|
intervalSeconds: pending.intervalSeconds,
|
|
194
194
|
maxWaitMs: args.maxWaitMs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "GitHub operations for DeepSeek Harness: releases, tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror publication.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|