@goodandready/dsh-github-ops 0.1.0 → 0.1.2
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 +20 -20
- package/README.md +40 -5
- package/README.ru.md +37 -5
- package/README.zh.md +37 -4
- package/lib/index.js +1 -16
- package/package.json +1 -1
- package/lib/approval.js +0 -168
package/CHANGELOG.md
CHANGED
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@goodandready/dsh-github-ops`.
|
|
4
4
|
|
|
5
|
-
## 0.1.
|
|
5
|
+
## 0.1.2
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
7
|
+
- Removed the write-approval contour the plugin never should have had: no `tools/pre-execute`
|
|
8
|
+
listener, no `allowedActions`, `autoApprove` or `approvalMode`, no action map. Approvals are
|
|
9
|
+
the host's business (the permission policy and dsh-approval-gate); a GitHub tool plugin that
|
|
10
|
+
registers its own gate duplicates the contour and, in a session where prompts are disabled,
|
|
11
|
+
turns every agreed write into a rejection — which is exactly what happened on 20.09.2026.
|
|
12
|
+
The `confirm` flags and the outright refusals (deleting or transferring a repository,
|
|
13
|
+
deleting an organization) stay: those are this plugin's own contract.
|
|
14
|
+
|
|
15
|
+
## 0.1.1 — released
|
|
16
|
+
|
|
17
|
+
- `approvalMode` with "auto" as the default: an agreed, non-destructive write goes through
|
|
18
|
+
without a prompt, so a session whose host has approval prompts disabled is not blocked by a
|
|
19
|
+
question it cannot answer. Deleting a release, a tag, a secret, a variable, a ruleset,
|
|
20
|
+
cancelling a run or removing branch protection still asks, and where prompts are
|
|
21
|
+
unavailable that means it is refused — never automatic. "ask" prompts on every write;
|
|
22
|
+
"off" leaves the decision entirely to the host approval contour.
|
|
23
|
+
- `allowedActions` is the fence in every mode: an action outside it is denied outright.
|
|
24
|
+
- The setting is editable in the card (a three-option select) and documented in all three
|
|
25
|
+
READMEs.
|
|
26
26
|
|
|
27
27
|
## 0.1.0 — first release
|
|
28
28
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
# dsh-github-ops
|
|
1
|
+
# 📦 @goodandready/dsh-github-ops
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Advanced GitHub Operations, Releases, CI Runs, Secrets & Sanitized Mirrors for DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-github-ops"><img src="https://img.shields.io/npm/v/@goodandready/dsh-github-ops.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-github-ops.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="All Author Projects"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
2
37
|
|
|
3
38
|
GitHub operations for [DeepSeek Harness](https://github.com/topics/dsh-plugin): releases,
|
|
4
39
|
tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror
|
|
@@ -31,8 +66,6 @@ Requires a GitHub token in the DSH credential service. Store the token under a n
|
|
|
31
66
|
| `timeoutMs` | `30000` | Per-request timeout. |
|
|
32
67
|
| `maxRetries` | `2` | Retries for a failed read (never for a write). |
|
|
33
68
|
| `reviewRulesJson` | *(empty)* | Review-rule overrides as JSON: `sensitivePaths`, `sensitiveSeverity`, `attentionPaths`, `migrationPaths`, `testsRequired`, `sourcePatterns`, `testPatterns`, `largeDiffLines`. |
|
|
34
|
-
| `allowedActions` | *(all)* | Write actions the plugin may perform; anything else is denied. Each one still asks. |
|
|
35
|
-
| `autoApprove` | *(empty)* | Actions an unattended run (`DSH_GITHUB_OPS_UNATTENDED=1`) may perform without asking. Destructive actions are never auto-approved. |
|
|
36
69
|
| `reviewJobTimeoutMs` | `120000` | How long a background review job may run. |
|
|
37
70
|
|
|
38
71
|
## Tools
|
|
@@ -130,8 +163,10 @@ still passes through the approval gate.
|
|
|
130
163
|
## Safety
|
|
131
164
|
|
|
132
165
|
- The token lives in the DSH credential service; settings hold only its name.
|
|
133
|
-
- Read operations never change state
|
|
134
|
-
repository, transferring one or deleting an organization is refused
|
|
166
|
+
- Read operations never change state. A state-changing tool needs an explicit `confirm`
|
|
167
|
+
flag, deleting a repository, transferring one or deleting an organization is refused
|
|
168
|
+
outright, and whether a prompt is shown is the host approval contour — the plugin does
|
|
169
|
+
not run an approval gate of its own.
|
|
135
170
|
- The client never logs the token, and every failure is reported as a value
|
|
136
171
|
(`ok: false` with `status`, `code`, `rateLimit`) instead of an exception.
|
|
137
172
|
- Findings from `gh_review` are deterministic rules: they point at what needs attention
|
package/README.ru.md
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
# dsh-github-ops
|
|
1
|
+
# 📦 @goodandready/dsh-github-ops
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Комплексные операции GitHub, релизы, CI-запуски, секреты и санитизированные зеркала для DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-github-ops"><img src="https://img.shields.io/npm/v/@goodandready/dsh-github-ops.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-github-ops.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему Star на GitHub</strong> — это покажет мне, что плагин полезен, и добавит мотивации продолжать его развитие.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>Если вы нашли ошибку или хотите предложить новую функцию</strong>, откройте issue на GitHub на любом удобном языке — я регулярно просматриваю предложения и реализую полезные идеи в будущих версиях плагина.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
2
37
|
|
|
3
38
|
Операции с GitHub для [DeepSeek Harness](https://github.com/topics/dsh-plugin): релизы,
|
|
4
39
|
теги, защищённый универсальный доступ к API, запуски workflow, секреты и публикация
|
|
@@ -31,8 +66,6 @@ dsh plugin --profile web add @goodandready/dsh-github-ops
|
|
|
31
66
|
| `timeoutMs` | `30000` | Таймаут одного запроса. |
|
|
32
67
|
| `maxRetries` | `2` | Повторы для неудачного чтения (запись не повторяется). |
|
|
33
68
|
| `reviewRulesJson` | пусто | Переопределение правил ревью в JSON: `sensitivePaths`, `sensitiveSeverity`, `attentionPaths`, `migrationPaths`, `testsRequired`, `sourcePatterns`, `testPatterns`, `largeDiffLines`. |
|
|
34
|
-
| `allowedActions` | все | Разрешённые записи; всё остальное отклоняется, и каждая всё равно спрашивает подтверждение. |
|
|
35
|
-
| `autoApprove` | пусто | Действия, которые неавтономный прогон (`DSH_GITHUB_OPS_UNATTENDED=1`) может выполнить без вопроса; разрушительные не автоутверждаются никогда. |
|
|
36
69
|
| `reviewJobTimeoutMs` | `120000` | Сколько может работать фоновая задача ревью. |
|
|
37
70
|
|
|
38
71
|
## Инструменты
|
|
@@ -130,8 +163,7 @@ Allowlist — состав `package.json → files` плюс `.gitignore`, `LICE
|
|
|
130
163
|
## Безопасность
|
|
131
164
|
|
|
132
165
|
- Токен живёт в сервисе учётных данных DSH; в настройках только его имя.
|
|
133
|
-
- Чтение не меняет
|
|
134
|
-
перенос и удаление организации отклоняются сразу.
|
|
166
|
+
- Чтение не меняет состояние. Изменяющий инструмент требует явного флага `confirm`, удаление репозитория, его перенос и удаление организации отклоняются сразу, а показывать ли подтверждение — решает штатный контур развёртывания: своего гейта подтверждений плагин не запускает.
|
|
135
167
|
- Клиент никогда не печатает токен, а любая ошибка возвращается значением
|
|
136
168
|
(`ok: false` с `status`, `code`, `rateLimit`), а не исключением.
|
|
137
169
|
|
package/README.zh.md
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
# dsh-github-ops
|
|
1
|
+
# 📦 @goodandready/dsh-github-ops
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>面向 DeepSeek Harness 的高级 GitHub 操作、发布、CI 运行、密钥与净化镜像套件</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-github-ops"><img src="https://img.shields.io/npm/v/@goodandready/dsh-github-ops.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-github-ops.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
20
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
2
37
|
|
|
3
38
|
面向 [DeepSeek Harness](https://github.com/topics/dsh-plugin) 的 GitHub 操作插件:发布(release)、
|
|
4
39
|
标签(tag)、受保护的通用 API 直通、工作流运行、密钥,以及经过净化的镜像发布。
|
|
@@ -28,8 +63,6 @@ dsh plugin --profile web add @goodandready/dsh-github-ops
|
|
|
28
63
|
| `timeoutMs` | `30000` | 单次请求超时。 |
|
|
29
64
|
| `maxRetries` | `2` | 读取失败时的重试次数(写入不重试)。 |
|
|
30
65
|
| `reviewRulesJson` | 空 | 以 JSON 覆盖评审规则:`sensitivePaths`、`sensitiveSeverity`、`attentionPaths`、`migrationPaths`、`testsRequired`、`sourcePatterns`、`testPatterns`、`largeDiffLines`。 |
|
|
31
|
-
| `allowedActions` | 全部 | 允许执行的写操作;其余一律拒绝,且每个都会询问确认。 |
|
|
32
|
-
| `autoApprove` | 空 | 非交互运行(`DSH_GITHUB_OPS_UNATTENDED=1`)可免除询问的操作;破坏性操作永远不会被自动批准。 |
|
|
33
66
|
| `reviewJobTimeoutMs` | `120000` | 后台评审任务的最长运行时间。 |
|
|
34
67
|
|
|
35
68
|
## 工具
|
|
@@ -122,7 +155,7 @@ dsh plugin --profile web add @goodandready/dsh-github-ops
|
|
|
122
155
|
## 安全边界
|
|
123
156
|
|
|
124
157
|
- token 只存在于 DSH 凭据服务,设置中只有名称;
|
|
125
|
-
-
|
|
158
|
+
- 读取操作不改变状态;写入需要显式 `confirm` 参数,删除仓库、转移仓库、删除组织会被直接拒绝。是否显示确认提示由宿主的审批机制决定——本插件不自行运行审批关卡;
|
|
126
159
|
- 客户端不会输出 token,失败以返回值形式报告(`ok: false` 及 `status`、`code`、`rateLimit`),
|
|
127
160
|
不会中断整个回合;
|
|
128
161
|
- `gh_review` 的发现是确定性规则:指出需要关注的地方,绝不冒充正确性结论。
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,6 @@ import { presentationFor } from './present.js'
|
|
|
21
21
|
import { createRecords, startReviewJob } from './jobs.js'
|
|
22
22
|
import { registerCommands } from './commands.js'
|
|
23
23
|
import { registerJobTools } from './tools/register-jobs.js'
|
|
24
|
-
import { registerApprovalGate, ALL_ACTIONS } from './approval.js'
|
|
25
24
|
import { registerCoreTools } from './tools/register-core.js'
|
|
26
25
|
import { registerCollabTools } from './tools/register-collab.js'
|
|
27
26
|
import { registerMirrorTools } from './tools/register-mirror.js'
|
|
@@ -67,9 +66,6 @@ export const OUTPUT_SCHEMA = {
|
|
|
67
66
|
|
|
68
67
|
const MAX_RENDER_CHARS = 20000
|
|
69
68
|
|
|
70
|
-
/** Actions this plugin can be asked to perform; exposed for settings validation. */
|
|
71
|
-
export const KNOWN_ACTIONS = ALL_ACTIONS
|
|
72
|
-
|
|
73
69
|
/** Render a tool result for the model: readable text, bounded so one call cannot flood the turn. */
|
|
74
70
|
export function renderToolResult(toolName, value) {
|
|
75
71
|
if (!value || value.ok !== true) {
|
|
@@ -108,10 +104,6 @@ export const Config = Schema.object({
|
|
|
108
104
|
.description('Retries for a read (GET/HEAD) on a network failure, a timeout or a 5xx. Writes are never retried automatically.'),
|
|
109
105
|
reviewRulesJson: Schema.string().default('')
|
|
110
106
|
.description('Optional review-rule overrides as JSON: sensitivePaths, sensitiveSeverity, attentionPaths, migrationPaths, testsRequired, sourcePatterns, testPatterns, largeDiffLines.'),
|
|
111
|
-
allowedActions: Schema.array(Schema.string()).default([])
|
|
112
|
-
.description('Write actions this plugin may perform; anything else is denied. Empty means every known action is allowed, and each one still asks for confirmation.'),
|
|
113
|
-
autoApprove: Schema.array(Schema.string()).default([])
|
|
114
|
-
.description('Actions an unattended run (DSH_GITHUB_OPS_UNATTENDED=1) may perform without asking. Destructive actions are never auto-approved.'),
|
|
115
107
|
reviewJobTimeoutMs: Schema.number().default(120000)
|
|
116
108
|
.description('How long a background review job may run before it is cancelled.'),
|
|
117
109
|
})
|
|
@@ -292,7 +284,7 @@ export function apply(ctx, rawConfig) {
|
|
|
292
284
|
})
|
|
293
285
|
|
|
294
286
|
// Slash commands are a fast path for a human: they only hand the model an instruction, so
|
|
295
|
-
// the write still
|
|
287
|
+
// the write still passes through the host approval contour.
|
|
296
288
|
ctx.inject(['commands'], (cctx) => {
|
|
297
289
|
try {
|
|
298
290
|
const git = makeGitRunner()
|
|
@@ -315,13 +307,6 @@ export function apply(ctx, rawConfig) {
|
|
|
315
307
|
}
|
|
316
308
|
})
|
|
317
309
|
|
|
318
|
-
// Writes go through the approval contour: a named action, a reason a human can decide
|
|
319
|
-
// on, and no auto-approval for anything destructive.
|
|
320
|
-
try {
|
|
321
|
-
registerApprovalGate(ctx, () => liveConfig())
|
|
322
|
-
} catch (err) {
|
|
323
|
-
ctx.logger?.warn?.('[dsh-github-ops] approval gate not registered:', err && err.message)
|
|
324
|
-
}
|
|
325
310
|
|
|
326
311
|
if (skippedTools.length) {
|
|
327
312
|
ctx.logger?.warn?.(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
package/lib/approval.js
DELETED
|
@@ -1,168 +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
|
-
/**
|
|
132
|
-
* The policy, as a pure function.
|
|
133
|
-
*
|
|
134
|
-
* @returns {{kind: 'allow'|'ask'|'deny'|'continue', reason?: string, action?: string|null}}
|
|
135
|
-
*/
|
|
136
|
-
export function decide({ exec, allowedActions, autoApprove, unattended = false } = {}) {
|
|
137
|
-
const action = actionFor(exec)
|
|
138
|
-
if (!action) return { kind: 'continue', action: null }
|
|
139
|
-
|
|
140
|
-
const allowed = Array.isArray(allowedActions) && allowedActions.length ? allowedActions : ALL_ACTIONS
|
|
141
|
-
if (!allowed.includes(action)) {
|
|
142
|
-
return { kind: 'deny', action, reason: `dsh-github-ops: action "${action}" is not in allowedActions` }
|
|
143
|
-
}
|
|
144
|
-
if (unattended && !DESTRUCTIVE_ACTIONS.has(action) && Array.isArray(autoApprove) && autoApprove.includes(action)) {
|
|
145
|
-
return { kind: 'allow', action }
|
|
146
|
-
}
|
|
147
|
-
const destructive = DESTRUCTIVE_ACTIONS.has(action) ? ' This cannot be undone here.' : ''
|
|
148
|
-
return { kind: 'ask', action, reason: `dsh-github-ops: ${askReason(exec)}.${destructive}` }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Register the gate. Registration is an effect: disposing the plugin removes the listener.
|
|
153
|
-
*/
|
|
154
|
-
export function registerApprovalGate(ctx, getConfig) {
|
|
155
|
-
return ctx.on('tools/pre-execute', async (exec, next) => {
|
|
156
|
-
const cfg = (typeof getConfig === 'function' ? getConfig() : getConfig) || {}
|
|
157
|
-
const decision = decide({
|
|
158
|
-
exec,
|
|
159
|
-
allowedActions: cfg.allowedActions,
|
|
160
|
-
autoApprove: cfg.autoApprove,
|
|
161
|
-
unattended: cfg.unattended !== undefined ? cfg.unattended : isUnattended(),
|
|
162
|
-
})
|
|
163
|
-
if (decision.kind === 'continue') return next()
|
|
164
|
-
if (decision.kind === 'allow') return { kind: 'allow' }
|
|
165
|
-
if (decision.kind === 'deny') return { kind: 'deny', reason: decision.reason }
|
|
166
|
-
return { kind: 'ask', reason: decision.reason }
|
|
167
|
-
})
|
|
168
|
-
}
|