@goodandready/dsh-github-ops 0.1.1 → 0.2.0
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 +47 -0
- package/README.md +95 -5
- package/README.ru.md +82 -5
- package/README.zh.md +64 -4
- package/lib/auth-device.js +150 -0
- package/lib/auth.js +17 -5
- package/lib/client.js +604 -16
- package/lib/github.js +105 -9
- package/lib/guidance.js +26 -0
- package/lib/index.js +181 -27
- package/lib/routes.js +222 -0
- package/lib/scm.js +115 -0
- package/lib/status.js +66 -0
- package/lib/tools/actions.js +1 -1
- package/lib/tools/insights.js +246 -0
- package/lib/tools/issues.js +2 -1
- package/lib/tools/panel-data.js +45 -0
- package/lib/tools/register-insights.js +126 -0
- package/lib/tools/register-write.js +245 -0
- package/lib/tools/releases.js +3 -1
- package/lib/tools/repos.js +1 -1
- package/lib/tools/write-files.js +207 -0
- package/media/visual-verification.png +0 -0
- package/package.json +2 -1
- package/lib/approval.js +0 -193
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"lib",
|
|
14
|
+
"media/visual-verification.png",
|
|
14
15
|
"cordis.patch.yml",
|
|
15
16
|
"LICENSE",
|
|
16
17
|
"CHANGELOG.md",
|
package/lib/approval.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
// Who approves a GitHub write, and why.
|
|
2
|
-
//
|
|
3
|
-
// Every state-changing tool maps to a named action. The action must be in `allowedActions`
|
|
4
|
-
// (otherwise it is denied outright), a destructive action is always asked even in an
|
|
5
|
-
// unattended run, and anything else asks the human with a sentence that says what is about
|
|
6
|
-
// to happen — not just "perform a GitHub write".
|
|
7
|
-
//
|
|
8
|
-
// `decide()` is pure: the policy is a function of the call and the settings, so it is fully
|
|
9
|
-
// testable and the listener stays trivial.
|
|
10
|
-
|
|
11
|
-
/** Tool → action name. A tool absent from this map is not a write. */
|
|
12
|
-
export const ACTION_BY_TOOL = {
|
|
13
|
-
gh_release_create: 'release-create',
|
|
14
|
-
gh_release_edit: 'release-edit',
|
|
15
|
-
gh_release_delete: 'release-delete',
|
|
16
|
-
gh_tag_create: 'tag-create',
|
|
17
|
-
gh_tag_delete: 'tag-delete',
|
|
18
|
-
gh_repo_create: 'repo-create',
|
|
19
|
-
gh_repo_edit: 'repo-edit',
|
|
20
|
-
issue_open: 'issue-open',
|
|
21
|
-
issue_comment: 'issue-comment',
|
|
22
|
-
issue_close: 'issue-close',
|
|
23
|
-
pr_create: 'pr-create',
|
|
24
|
-
pr_update: 'pr-update',
|
|
25
|
-
pr_merge: 'pr-merge',
|
|
26
|
-
review_post: 'review-post',
|
|
27
|
-
gh_variable_set: 'variable-set',
|
|
28
|
-
gh_variable_delete: 'variable-delete',
|
|
29
|
-
gh_secret_set: 'secret-set',
|
|
30
|
-
gh_secret_delete: 'secret-delete',
|
|
31
|
-
gh_ruleset_apply: 'ruleset-apply',
|
|
32
|
-
gh_ruleset_delete: 'ruleset-delete',
|
|
33
|
-
gh_branch_protection_set: 'branch-protection-set',
|
|
34
|
-
gh_branch_protection_delete: 'branch-protection-delete',
|
|
35
|
-
gh_run_rerun: 'run-rerun',
|
|
36
|
-
gh_run_cancel: 'run-cancel',
|
|
37
|
-
gh_mirror_publish: 'mirror-publish',
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Actions a single confirmation cannot make safe: always asked, never auto-approved. */
|
|
41
|
-
export const DESTRUCTIVE_ACTIONS = new Set([
|
|
42
|
-
'release-delete', 'tag-delete', 'variable-delete', 'secret-delete',
|
|
43
|
-
'ruleset-delete', 'branch-protection-delete', 'run-cancel',
|
|
44
|
-
])
|
|
45
|
-
|
|
46
|
-
export const ALL_ACTIONS = Object.freeze([...new Set(Object.values(ACTION_BY_TOOL))])
|
|
47
|
-
|
|
48
|
-
const MUTATING_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE'])
|
|
49
|
-
|
|
50
|
-
/** Which action a call is, or null when it changes nothing. */
|
|
51
|
-
export function actionFor(exec) {
|
|
52
|
-
const name = exec && exec.name
|
|
53
|
-
if (!name) return null
|
|
54
|
-
if (name === 'gh_api') {
|
|
55
|
-
const args = (exec && exec.arguments) || {}
|
|
56
|
-
if (args.graphql) return null // a query may still mutate; the GraphQL text cannot be judged here
|
|
57
|
-
const method = String(args.method || 'GET').toUpperCase()
|
|
58
|
-
return MUTATING_METHODS.has(method) ? 'api-call' : null
|
|
59
|
-
}
|
|
60
|
-
return ACTION_BY_TOOL[name] || null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** A sentence a human can decide on. */
|
|
64
|
-
export function askReason(exec) {
|
|
65
|
-
const args = (exec && exec.arguments) || {}
|
|
66
|
-
const repo = args.repository || 'the default repository'
|
|
67
|
-
const name = exec && exec.name
|
|
68
|
-
switch (name) {
|
|
69
|
-
case 'gh_release_create':
|
|
70
|
-
return `create GitHub release ${args.tag}${args.draft ? ' (draft)' : ''} in ${repo}`
|
|
71
|
-
case 'gh_release_edit':
|
|
72
|
-
return `edit GitHub release ${args.tag || args.releaseId} in ${repo}${args.makeLatest ? ' and mark it latest' : ''}`
|
|
73
|
-
case 'gh_release_delete':
|
|
74
|
-
return `delete GitHub release ${args.tag || args.releaseId} in ${repo} (the git tag stays)`
|
|
75
|
-
case 'gh_tag_create':
|
|
76
|
-
return `create tag ${args.tag} in ${repo}`
|
|
77
|
-
case 'gh_tag_delete':
|
|
78
|
-
return `delete tag ${args.tag} in ${repo}`
|
|
79
|
-
case 'gh_repo_create':
|
|
80
|
-
return `create repository ${args.name}${args.org ? ` in ${args.org}` : ''}${args.private === false ? ' as PUBLIC' : ' (private)'}`
|
|
81
|
-
case 'gh_repo_edit':
|
|
82
|
-
return `change settings of ${repo}${args.private !== undefined ? ` (visibility: ${args.private ? 'private' : 'PUBLIC'})` : ''}`
|
|
83
|
-
case 'issue_open':
|
|
84
|
-
return `open issue "${args.title}" in ${repo}`
|
|
85
|
-
case 'issue_comment':
|
|
86
|
-
return `comment on #${args.number} in ${repo}`
|
|
87
|
-
case 'issue_close':
|
|
88
|
-
return `close #${args.number} in ${repo}`
|
|
89
|
-
case 'pr_create':
|
|
90
|
-
return `open pull request "${args.title}" (${args.head} → ${args.base || 'main'}) in ${repo}`
|
|
91
|
-
case 'pr_update':
|
|
92
|
-
return `update pull request #${args.number} in ${repo}`
|
|
93
|
-
case 'pr_merge':
|
|
94
|
-
return `MERGE pull request #${args.number} in ${repo} (${args.method || 'merge'})`
|
|
95
|
-
case 'review_post':
|
|
96
|
-
return `post a review on #${args.number} in ${repo}`
|
|
97
|
-
case 'gh_variable_set':
|
|
98
|
-
return `set variable ${args.name} in ${repo}`
|
|
99
|
-
case 'gh_variable_delete':
|
|
100
|
-
return `delete variable ${args.name} in ${repo}`
|
|
101
|
-
case 'gh_secret_set':
|
|
102
|
-
return `set secret ${args.name} in ${repo}`
|
|
103
|
-
case 'gh_secret_delete':
|
|
104
|
-
return `delete secret ${args.name} in ${repo}`
|
|
105
|
-
case 'gh_ruleset_apply':
|
|
106
|
-
return `apply ruleset ${args.name || args.rulesetId} to ${repo}`
|
|
107
|
-
case 'gh_ruleset_delete':
|
|
108
|
-
return `delete ruleset ${args.rulesetId} from ${repo}`
|
|
109
|
-
case 'gh_branch_protection_set':
|
|
110
|
-
return `replace branch protection on ${args.branch} in ${repo}`
|
|
111
|
-
case 'gh_branch_protection_delete':
|
|
112
|
-
return `remove branch protection from ${args.branch} in ${repo}`
|
|
113
|
-
case 'gh_run_rerun':
|
|
114
|
-
return `re-run workflow run ${args.runId} in ${repo}`
|
|
115
|
-
case 'gh_run_cancel':
|
|
116
|
-
return `cancel workflow run ${args.runId} in ${repo}`
|
|
117
|
-
case 'gh_mirror_publish':
|
|
118
|
-
return `publish a sanitized tree to the mirror of ${args.cwd || 'the checkout'} (${args.mirror || 'github'}/${args.branch || 'main'})`
|
|
119
|
-
case 'gh_api':
|
|
120
|
-
return `call the GitHub API: ${String(args.method || 'GET').toUpperCase()} ${args.path}`
|
|
121
|
-
default:
|
|
122
|
-
return `perform a GitHub write action (${name})`
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Whether an unattended run may proceed without a human. */
|
|
127
|
-
export function isUnattended(env = process.env) {
|
|
128
|
-
return String((env && env.DSH_GITHUB_OPS_UNATTENDED) || '') === '1'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** How the plugin treats a write. */
|
|
132
|
-
export const APPROVAL_MODES = ['auto', 'ask', 'off']
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* The policy, as a pure function.
|
|
136
|
-
*
|
|
137
|
-
* - `auto` (default): an agreed, non-destructive write goes through without a prompt, so a
|
|
138
|
-
* session whose host has approvals disabled is not blocked by a question it cannot answer.
|
|
139
|
-
* A destructive action still asks — and where prompts are unavailable that means it is
|
|
140
|
-
* refused, which is the intent: deleting a release, a tag, a secret, a ruleset or branch
|
|
141
|
-
* protection is never automatic.
|
|
142
|
-
* - `ask`: every write asks.
|
|
143
|
-
* - `off`: the plugin has no opinion; the host's own approval contour decides everything.
|
|
144
|
-
*
|
|
145
|
-
* `allowedActions` is the fence in every mode: an action outside it is denied outright.
|
|
146
|
-
*
|
|
147
|
-
* @returns {{kind: 'allow'|'ask'|'deny'|'continue', reason?: string, action?: string|null}}
|
|
148
|
-
*/
|
|
149
|
-
export function decide({ exec, allowedActions, autoApprove, unattended = false, mode = 'auto' } = {}) {
|
|
150
|
-
const action = actionFor(exec)
|
|
151
|
-
if (!action) return { kind: 'continue', action: null }
|
|
152
|
-
if (mode === 'off') return { kind: 'continue', action }
|
|
153
|
-
|
|
154
|
-
const allowed = Array.isArray(allowedActions) && allowedActions.length ? allowedActions : ALL_ACTIONS
|
|
155
|
-
if (!allowed.includes(action)) {
|
|
156
|
-
return { kind: 'deny', action, reason: `dsh-github-ops: action "${action}" is not in allowedActions` }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const destructive = DESTRUCTIVE_ACTIONS.has(action)
|
|
160
|
-
if (destructive) {
|
|
161
|
-
return {
|
|
162
|
-
kind: 'ask',
|
|
163
|
-
action,
|
|
164
|
-
reason: `dsh-github-ops: ${askReason(exec)}. This cannot be undone here, so it is never automatic.`,
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (mode === 'auto') return { kind: 'allow', action }
|
|
169
|
-
if (unattended && Array.isArray(autoApprove) && autoApprove.includes(action)) return { kind: 'allow', action }
|
|
170
|
-
return { kind: 'ask', action, reason: `dsh-github-ops: ${askReason(exec)}.` }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Register the gate. Registration is an effect: disposing the plugin removes the listener.
|
|
175
|
-
*/
|
|
176
|
-
export function registerApprovalGate(ctx, getConfig) {
|
|
177
|
-
const first = (typeof getConfig === 'function' ? getConfig() : getConfig) || {}
|
|
178
|
-
if (first.approvalMode === 'off') return () => {}
|
|
179
|
-
return ctx.on('tools/pre-execute', async (exec, next) => {
|
|
180
|
-
const cfg = (typeof getConfig === 'function' ? getConfig() : getConfig) || {}
|
|
181
|
-
const decision = decide({
|
|
182
|
-
exec,
|
|
183
|
-
allowedActions: cfg.allowedActions,
|
|
184
|
-
autoApprove: cfg.autoApprove,
|
|
185
|
-
mode: cfg.approvalMode || 'auto',
|
|
186
|
-
unattended: cfg.unattended !== undefined ? cfg.unattended : isUnattended(),
|
|
187
|
-
})
|
|
188
|
-
if (decision.kind === 'continue') return next()
|
|
189
|
-
if (decision.kind === 'allow') return { kind: 'allow' }
|
|
190
|
-
if (decision.kind === 'deny') return { kind: 'deny', reason: decision.reason }
|
|
191
|
-
return { kind: 'ask', reason: decision.reason }
|
|
192
|
-
})
|
|
193
|
-
}
|