@goodandready/dsh-github-ops 0.2.8 → 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 +40 -21
- 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 +3 -3
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 }
|
|
@@ -126,48 +127,66 @@ export function renderToolResult(toolName, value) {
|
|
|
126
127
|
return [{ type: 'text', text }]
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// Unwrap Volatile boxes before any caller reads a value. A volatile field holds a box
|
|
131
|
+
// rather than its value, and the Loader mutates those boxes in place.
|
|
132
|
+
export function plainConfig(value) {
|
|
133
|
+
if (value === null || typeof value !== 'object') return value
|
|
134
|
+
if (Array.isArray(value)) return value.map(plainConfig)
|
|
135
|
+
if (typeof value.get === 'function') return plainConfig(value.get())
|
|
136
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]))
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
export const Config = Schema.object({
|
|
130
|
-
tokenEnv: Schema.string().role('credential-ref').default('GITHUB_TOKEN')
|
|
140
|
+
tokenEnv: Schema.string().volatile().role('credential-ref').default('GITHUB_TOKEN')
|
|
131
141
|
.description('Name used to look up access: the DSH credential, then the environment variable, then the gh CLI. The value itself is never stored in settings.'),
|
|
132
|
-
tokenSource: Schema.string().default('auto')
|
|
142
|
+
tokenSource: Schema.string().volatile().default('auto')
|
|
133
143
|
.description('Where access comes from: "auto" tries the DSH credential, then the environment variable, then the gh CLI; pin it to "credentials", "env" or "gh" to use one source only.'),
|
|
134
|
-
defaultRepository: Schema.string().default('')
|
|
144
|
+
defaultRepository: Schema.string().volatile().default('')
|
|
135
145
|
.description('Default repository as "owner/repo"; used when a tool call omits the repository.'),
|
|
136
|
-
baseUrl: Schema.string().default('https://api.github.com')
|
|
146
|
+
baseUrl: Schema.string().volatile().default('https://api.github.com')
|
|
137
147
|
.description('GitHub API base URL; change it for GitHub Enterprise.'),
|
|
138
|
-
timeoutMs: Schema.number().default(30000)
|
|
148
|
+
timeoutMs: Schema.number().volatile().default(30000)
|
|
139
149
|
.description('Per-request timeout in milliseconds.'),
|
|
140
|
-
maxRetries: Schema.number().default(2)
|
|
150
|
+
maxRetries: Schema.number().volatile().default(2)
|
|
141
151
|
.description('Retries for a read (GET/HEAD) on a network failure, a timeout or a 5xx. Writes are never retried automatically.'),
|
|
142
|
-
reviewRulesJson: Schema.string().default('')
|
|
152
|
+
reviewRulesJson: Schema.string().volatile().default('')
|
|
143
153
|
.description('Optional review-rule overrides as JSON: sensitivePaths, sensitiveSeverity, attentionPaths, migrationPaths, testsRequired, sourcePatterns, testPatterns, largeDiffLines.'),
|
|
144
|
-
oauthClientId: Schema.string().default(DEFAULT_CLIENT_ID)
|
|
154
|
+
oauthClientId: Schema.string().volatile().default(DEFAULT_CLIENT_ID)
|
|
145
155
|
.description('OAuth application id used by gh_auth_login. The default is the public GitHub CLI application.'),
|
|
146
|
-
oauthScope: Schema.string().default(DEFAULT_SCOPE)
|
|
156
|
+
oauthScope: Schema.string().volatile().default(DEFAULT_SCOPE)
|
|
147
157
|
.description('OAuth scope requested by gh_auth_login.'),
|
|
148
|
-
oauthClientSecretRef: Schema.string().role('credential-ref').default('')
|
|
158
|
+
oauthClientSecretRef: Schema.string().volatile().role('credential-ref').default('')
|
|
149
159
|
.description('Optional credential holding the OAuth client secret, for apps that require one.'),
|
|
150
|
-
accessFile: Schema.string().default('')
|
|
160
|
+
accessFile: Schema.string().volatile().default('')
|
|
151
161
|
.description('Where gh_auth_login stores its sign-in (default: $DSH_HOME/github-ops-auth.json, mode 0600).'),
|
|
152
|
-
interceptLinks: Schema.boolean().default(false)
|
|
162
|
+
interceptLinks: Schema.boolean().volatile().default(false)
|
|
153
163
|
.description('Let a github.com link clicked in the conversation also select that repository for the Source control tab and the sidebar panel. Off by default: the link always opens in the browser either way.'),
|
|
154
|
-
guidance: Schema.boolean().default(true)
|
|
164
|
+
guidance: Schema.boolean().volatile().default(true)
|
|
155
165
|
.description('Add a short paragraph about this plugin to the system prompt: which tool for which question, when the repository argument can be omitted, and that approvals belong to the host.'),
|
|
156
|
-
guidanceText: Schema.string().default('')
|
|
166
|
+
guidanceText: Schema.string().volatile().default('')
|
|
157
167
|
.description('Replace the default guidance paragraph with your own text.'),
|
|
158
|
-
cacheTtlMs: Schema.number().default(60000)
|
|
168
|
+
cacheTtlMs: Schema.number().volatile().default(60000)
|
|
159
169
|
.description('How long a read stays cached, in milliseconds. A composed report then costs one request set instead of many; writes always go to the network.'),
|
|
160
|
-
reviewJobTimeoutMs: Schema.number().default(120000)
|
|
170
|
+
reviewJobTimeoutMs: Schema.number().volatile().default(120000)
|
|
161
171
|
.description('How long a background review job may run before it is cancelled.'),
|
|
162
172
|
})
|
|
163
173
|
|
|
164
174
|
export function apply(ctx, rawConfig) {
|
|
165
|
-
const config = { ...Config(), ...(rawConfig || {}) }
|
|
175
|
+
const config = plainConfig({ ...Config(), ...(rawConfig || {}) })
|
|
166
176
|
let settingsScope = null
|
|
167
177
|
|
|
168
178
|
ctx.inject(['settings'], (sctx) => {
|
|
169
179
|
try {
|
|
170
|
-
settingsScope =
|
|
180
|
+
settingsScope = {
|
|
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
|
+
}
|
|
171
190
|
} catch (err) {
|
|
172
191
|
ctx.logger?.warn?.('[dsh-github-ops] settings namespace unavailable:', err && err.message)
|
|
173
192
|
}
|
|
@@ -189,9 +208,9 @@ export function apply(ctx, rawConfig) {
|
|
|
189
208
|
function liveConfig() {
|
|
190
209
|
try {
|
|
191
210
|
const fromScope = settingsScope && settingsScope.get && settingsScope.get()
|
|
192
|
-
return { ...config, ...(fromScope || {}) }
|
|
211
|
+
return plainConfig({ ...config, ...(fromScope || {}) })
|
|
193
212
|
} catch {
|
|
194
|
-
return config
|
|
213
|
+
return plainConfig(config)
|
|
195
214
|
}
|
|
196
215
|
}
|
|
197
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",
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
42
42
|
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
43
43
|
"@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
44
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
44
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
48
|
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
|
|
49
49
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
|
|
50
50
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
|
|
51
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
51
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
52
52
|
},
|
|
53
53
|
"repository": {
|
|
54
54
|
"type": "git",
|