dsh-jenkins-plugin 0.0.0-stage → 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -2
  3. package/README.zh.md +170 -0
  4. package/cordis.patch.yml +7 -0
  5. package/lib/analyze.d.ts +125 -0
  6. package/lib/analyze.js +112 -0
  7. package/lib/client/Builds.d.ts +37 -0
  8. package/lib/client/Builds.js +303 -0
  9. package/lib/client/Panel.d.ts +30 -0
  10. package/lib/client/Panel.js +347 -0
  11. package/lib/client/SettingsSection.d.ts +24 -0
  12. package/lib/client/SettingsSection.js +239 -0
  13. package/lib/client/WriteAction.d.ts +40 -0
  14. package/lib/client/WriteAction.js +54 -0
  15. package/lib/client/api.d.ts +351 -0
  16. package/lib/client/api.js +251 -0
  17. package/lib/client/index.d.ts +26 -0
  18. package/lib/client/index.js +65 -0
  19. package/lib/client/live.d.ts +155 -0
  20. package/lib/client/live.js +199 -0
  21. package/lib/client/locales.d.ts +152 -0
  22. package/lib/client/locales.js +288 -0
  23. package/lib/client/tab.d.ts +25 -0
  24. package/lib/client/tab.js +38 -0
  25. package/lib/client/ui.d.ts +128 -0
  26. package/lib/client/ui.js +288 -0
  27. package/lib/client.js +2273 -0
  28. package/lib/config.d.ts +88 -0
  29. package/lib/config.js +53 -0
  30. package/lib/connection.d.ts +166 -0
  31. package/lib/connection.js +262 -0
  32. package/lib/follow.d.ts +74 -0
  33. package/lib/follow.js +170 -0
  34. package/lib/index.d.ts +27 -0
  35. package/lib/index.js +140 -0
  36. package/lib/jenkins/client.d.ts +277 -0
  37. package/lib/jenkins/client.js +974 -0
  38. package/lib/jenkins/types.d.ts +236 -0
  39. package/lib/jenkins/types.js +113 -0
  40. package/lib/policy.d.ts +33 -0
  41. package/lib/policy.js +42 -0
  42. package/lib/routes.d.ts +74 -0
  43. package/lib/routes.js +951 -0
  44. package/lib/settings.d.ts +133 -0
  45. package/lib/settings.js +229 -0
  46. package/lib/tools/builds.d.ts +42 -0
  47. package/lib/tools/builds.js +701 -0
  48. package/lib/tools/jobs.d.ts +16 -0
  49. package/lib/tools/jobs.js +140 -0
  50. package/lib/tracker.d.ts +288 -0
  51. package/lib/tracker.js +479 -0
  52. package/lib/watch.d.ts +96 -0
  53. package/lib/watch.js +117 -0
  54. package/package.json +111 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 byzhao999
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # dsh-jenkins-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Jenkins integration for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): **a live build-progress panel in the Web GUI's right sidebar**, plus a set of model-facing Jenkins tools.
4
+
5
+ Once installed you stop switching to the Jenkins page to watch a build: the jobs you follow become cards in the sidebar with a live progress bar, a notice when a build finishes, and — when a build fails — one button that hands the failure to the model for a diagnosis.
6
+
7
+ ![The Jenkins panel: a running build's card with live stage progress, an unstable build, and failed builds offering rebuild and hand-to-AI](https://raw.githubusercontent.com/Endless-zby/dsh-jenkins-plugin/main/doc/panel-favorites-2.png)
8
+
9
+ ## Install
10
+
11
+ Installing from npm is recommended — the package ships prebuilt code and needs no build permission:
12
+
13
+ ```sh
14
+ dsh plugin --profile web add dsh-jenkins-plugin
15
+ ```
16
+
17
+ Restart `dsh web`, open **Settings → Jenkins**, add an instance (URL, account, password/API token) and save; the **Jenkins** panel then appears in the right sidebar.
18
+
19
+ ![The Jenkins entry on the Web sidebar's start page](https://raw.githubusercontent.com/Endless-zby/dsh-jenkins-plugin/main/doc/settings-instances-1.png)
20
+
21
+ Other sources:
22
+
23
+ ```sh
24
+ # tarball (offline or internal distribution)
25
+ dsh plugin --profile web add ./dsh-jenkins-plugin-0.1.0.tgz
26
+
27
+ # git sources: the profile must allow the prepare script to build (pnpm ≥10 blocks it by default)
28
+ dsh plugin --profile web add github:<owner>/dsh-jenkins-plugin
29
+ ```
30
+
31
+ A git install fetches **sources, not built artifacts**, so the profile's `pnpm-workspace.yaml` has to allow its prepare script:
32
+
33
+ ```yaml
34
+ allowBuilds:
35
+ dsh-jenkins-plugin: true
36
+ ```
37
+
38
+ That is permission for this package's code to run on your machine at install time, outside any sandbox the agent runs in — only allow sources you trust, and pin a commit (`github:<owner>/dsh-jenkins-plugin#<sha>`). Use npm or a tarball to avoid the step entirely.
39
+
40
+ ## What you get
41
+
42
+ **The panel** (the **Jenkins** tab in the right sidebar) has two modules:
43
+
44
+ - **Following** — one card per followed job: state dot, build number, outcome tag, **live progress bar**, the running stage's name, the change description of the current build, and the Maven project version when the controller reports one. Cards are fed by an SSE stream, not by a polling page.
45
+ - **All jobs** — collapsed by default; ☆/★ at the end of each row follows or unfollows it.
46
+
47
+ Clicking a job or a card drills down without ever leaving the panel: build history (paged) → build detail (stage bar, embedded console log, test summary, change set, artifacts). Downloading an artifact is the only action that opens a Jenkins page.
48
+
49
+ **Writes** happen in the panel as *button → inline confirmation → run → result in place*:
50
+
51
+ | Action | Notes |
52
+ |---|---|
53
+ | Trigger a build | parameterized jobs get a `KEY=VALUE` box |
54
+ | Rebuild | offered only for failed builds; **reuses the failed build's parameters** |
55
+ | Abort | `allowCancel` is off by default; enable it to use the button |
56
+ | Hand to AI | offered only for failed builds; sends the failed log tail, stages and change set to the current session's model, and the answer appears in the conversation |
57
+
58
+ Jobs on `denyJobs` are refused whatever the switches say, and **not a single request reaches Jenkins**.
59
+
60
+ **Tools the model can call on its own:**
61
+
62
+ | Tool | Purpose |
63
+ |---|---|
64
+ | `jenkins_jobs` | list jobs and their status, optionally with recent build history |
65
+ | `jenkins_build_status` | one build's result, duration, Pipeline stages, change set, test summary |
66
+ | `jenkins_log` | console log, tail by default, continue from `next_offset` |
67
+ | `jenkins_workspace` | a build's workspace directory or artifact list (workspace reads need the Jenkins Workspace API plugin) |
68
+ | `jenkins_build` | trigger or cancel a build — the only write tool; `denyJobs`/`allow.*` first, then operator approval |
69
+
70
+ **Multiple instances**: the settings page holds several Jenkins controllers and probes each one with "test connection" before saving (if any row fails, the whole save is rejected with zero writes). The panel footer switches instances and marks the current one in green. The instance list and the followed jobs live in `$DSH_HOME/jenkins.json`.
71
+
72
+ **Completion notices**: a followed job's finished build is announced once through the platform's own background-job mechanism; the same build is never announced twice.
73
+
74
+ ## Permissions and risks
75
+
76
+ Please take these as read before installing:
77
+
78
+ - **Network**: requests go only to the Jenkins addresses you configure. Nothing else is contacted.
79
+ - **Credentials**: Basic auth; the password/API token is stored and resolved through **DSH's own credentials service** (`tokenRef` names a credential reference). The plugin's own `jenkins.json` holds only URLs, accounts and credential names — **no passwords** — and credentials never appear in tool output, the panel, or logs.
80
+ - **Writes**: it can trigger and cancel builds. `allowTrigger` defaults to on, `allowCancel` to off, and `denyJobs` hard-blocks specific jobs (including whole folders). Model-side calls additionally require approval; a panel click uses inline confirmation plus the same policy check.
81
+ - **Build logs enter the model's context**: "hand to AI" and the model's own `jenkins_log` calls send **console log content to the model as part of the prompt**. Jenkins logs are **untrusted input** — they can contain arbitrary text, including instructions aimed at the model — so treat them that way. Only the last 16 KiB of a failed build's log is sent by default (`analyzeLogBytes`).
82
+ - **Local state**: `$DSH_HOME/jenkins.json` (instance list plus followed jobs) and DSH credentials (passwords).
83
+ - The plugin never triggers a build by itself: only your click or an explicit model call does.
84
+
85
+ ## Compatibility
86
+
87
+ - **Harness**: built and verified against `0.1.6-alpha.2` (both `engines.dsh` and the peer dependencies pin that version).
88
+ - **Node**: matches the harness, `^22.19.0 || >=24.0.0`.
89
+ - **Surface**: the panel is a Web GUI surface (`dsh web`), verified on the Web composition.
90
+ - **Jenkins**: verified against a real **Jenkins 2.176.2** (FreeStyle and Maven jobs, an instance with 777 jobs). Several honest degradations:
91
+ - **Pipeline stages** need Blue Ocean's `wfapi` endpoints; on a controller without Pipeline jobs the stage list is empty and the card falls back to an elapsed-time estimate — that is "the controller reported no stages", not a broken panel.
92
+ - **Workspace reads** need the Jenkins Workspace API plugin; without it `jenkins_workspace` says so instead of pretending.
93
+ - **Maven versions** come from `mavenArtifacts/api/json`, which only Maven jobs answer — and on a real 2.176.2 it is usually produced when the build *ends*, so a running Maven build's card may have no version. When there is none, nothing is shown; a version is never guessed.
94
+ - **Artifacts carry no size**: Jenkins' `tree` query cannot fetch artifact sizes.
95
+ - **Not finished yet**: `notifyWakeOnFailure` (wake the model when a build fails) and `uiAutoOpenOnTrigger` (open the panel on trigger) are accepted but **not active**; `maxTrackedBuilds` is advisory only — the followed list is the user's own choice, so nothing is hard-capped.
96
+
97
+ ## Configuration
98
+
99
+ Static configuration is only the **fallback layer**: instances configured in the settings page win, and static configuration applies when no panel settings exist (as an instance with id `config`). `baseUrl`/`username` may be left empty — that is exactly the "configure everything in the panel" setup.
100
+
101
+ ![The Jenkins settings page: several instances, test-connection result, save](https://raw.githubusercontent.com/Endless-zby/dsh-jenkins-plugin/main/doc/settings-instances.png)
102
+
103
+ ```yaml
104
+ - id: jenkins
105
+ name: dsh-jenkins-plugin
106
+ config:
107
+ baseUrl: https://ci.example.com/jenkins
108
+ username: ci-bot
109
+ tokenRef: JENKINS_TOKEN
110
+ denyJobs: ['infra/*']
111
+ ```
112
+
113
+ | Field | Default | Meaning |
114
+ |---|---|---|
115
+ | `baseUrl` | empty | Jenkins root URL; empty means panel settings only |
116
+ | `username` | empty | login name paired with the token |
117
+ | `tokenRef` | `JENKINS_TOKEN` | credential reference holding the API token; resolved per call, so a rotation applies without a restart |
118
+ | `settingsFile` | `jenkins.json` | panel settings file: an absolute path, or a name under the harness home |
119
+ | `tlsMode` | `verify` | `verify` validates the certificate; `allowSelfSigned` accepts an untrusted one |
120
+ | `tlsCaFile` | — | PEM bundle used to validate the certificate under `verify` |
121
+ | `timeoutMs` | `15000` | bound on one HTTP request |
122
+ | `crumbMode` | `auto` | `auto` fetches a CSRF crumb before mutating requests; `off` skips it |
123
+ | `denyJobs` | `[]` | job paths that may never be triggered or cancelled; `folder/*` matches a folder |
124
+ | `allowTrigger` | `true` | whether triggering builds is allowed |
125
+ | `allowCancel` | `false` | whether cancelling builds is allowed |
126
+ | `progressIntervalMs` | `3000` | stage polling interval for tracked builds |
127
+ | `logIntervalMs` | `1500` | console-log polling interval for the build the panel shows |
128
+ | `idleBackoffMaxMs` | `30000` | upper bound of the idle backoff |
129
+ | `queueTimeoutMs` | `300000` | how long a build may stay queued before it reports a timeout |
130
+ | `maxTrackedBuilds` | `5` | advisory cap on tracked builds (not enforced) |
131
+ | `retainMs` | `600000` | how long a finished build stays in the tracking table |
132
+ | `historyCount` | `10` | default build-history rows (max 50) |
133
+ | `maxLogBytes` | `262144` | byte cap on one console-log **response** |
134
+ | `logReadBytes` | `8388608` | byte window read from the controller per log call |
135
+ | `maxListingBytes` | `4194304` | byte cap on one response of the recursive job walk |
136
+ | `maxWorkspaceEntries` | `500` | entry cap on one workspace directory listing |
137
+ | `maxReadFileBytes` | `1048576` | byte cap on one workspace file read |
138
+ | `notifyOnComplete` | `true` | announce a followed build when it finishes |
139
+ | `notifyWakeOnFailure` | `false` | wake the model on failure (**not active yet**) |
140
+ | `allowAnalyze` | `true` | allow handing a failed build to the model |
141
+ | `analyzeLogBytes` | `16384` | log-tail bytes handed to the model |
142
+ | `uiAutoOpenOnTrigger` | `true` | open the panel on trigger (**not active yet**) |
143
+
144
+ `maxLogBytes` and `logReadBytes` are two different things because real Jenkins **ignores `?start=`**: the read window has to be far larger than a page, or the "tail" you get back is really the head of the log. That is why this plugin reads a window and slices locally.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ npm install # runs prepare (= build) once
150
+ npm run typecheck
151
+ npm test # 235 assertions, no test framework: every tests/*.ts runs on its own
152
+ npm run build # tsc (Host half) + esbuild (browser half → lib/client.js)
153
+ npm run check # test + secret scan + packed-payload audit
154
+ ```
155
+
156
+ The browser half is bundled into the client module system's lazy factory (`window.__ModuleLoader__.load({ id, factory })`): React, Cordis and the static UI libraries stay external and resolve through the injected `require`, everything else is inlined — so the package has no runtime dependency beyond the platform baseline.
157
+
158
+ `tests/stub-jenkins.ts` is a zero-dependency stub Jenkins (`node tests/stub-jenkins.ts`) with stages, a failing build, a parameterized Maven job, write endpoints and a queue delay; it exercises the panel's live path and write path without touching a real controller.
159
+
160
+ Three gates run before a release (and are what CI should run):
161
+
162
+ ```sh
163
+ npm run check:secrets # credential shapes in everything a commit would carry (private hostnames / user paths warn only)
164
+ npm run check:payload # reads npm pack's manifest: artifacts present, no src/ or internal notes shipped
165
+ npm pack # produce the tarball, install it into a clean profile, then publish
166
+ ```
167
+
168
+ ## License
169
+
170
+ [MIT](LICENSE)
package/README.zh.md ADDED
@@ -0,0 +1,170 @@
1
+ # dsh-jenkins-plugin
2
+
3
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 Jenkins 插件:**Web GUI 右侧栏里的实时构建面板**,加上一组面向模型的 Jenkins 工具。
4
+
5
+ 装完之后,你不再需要为了看一次构建进度而切到 Jenkins 页面:关注的 job 变成侧栏里的卡片,进度条实时走,构建结束发通知,失败了一键交给模型分析原因。
6
+
7
+ ![Jenkins 面板:正在跑的构建卡片带实时阶段进度、不稳定构建、失败构建的「重新构建 / 交给 AI 分析」](https://raw.githubusercontent.com/Endless-zby/dsh-jenkins-plugin/main/doc/panel-favorites-2.png)
8
+
9
+ ## 安装
10
+
11
+ 推荐从 npm 安装(预构建产物,不需要任何构建授权):
12
+
13
+ ```sh
14
+ dsh plugin --profile web add dsh-jenkins-plugin
15
+ ```
16
+
17
+ 装完重启 `dsh web`,打开**设置 → Jenkins** 填一个实例(地址 + 账号 + 密码/API token),Save 之后右栏就会出现 **Jenkins** 面板。
18
+
19
+ ![Web 侧栏开始页上的 Jenkins 入口](https://raw.githubusercontent.com/Endless-zby/dsh-jenkins-plugin/main/doc/settings-instances-1.png)
20
+
21
+ 其他安装来源:
22
+
23
+ ```sh
24
+ # tarball(离线/内部分发)
25
+ dsh plugin --profile web add ./dsh-jenkins-plugin-0.1.0.tgz
26
+
27
+ # git 源码:需要授权 allowBuilds 让 prepare 脚本构建(pnpm ≥10 默认拒绝)
28
+ dsh plugin --profile web add github:<owner>/dsh-jenkins-plugin
29
+ ```
30
+
31
+ git 安装拉的是**源码而不是产物**,所以必须在 profile 的 `pnpm-workspace.yaml` 里允许它的准备脚本:
32
+
33
+ ```yaml
34
+ allowBuilds:
35
+ dsh-jenkins-plugin: true
36
+ ```
37
+
38
+ 那等于允许本包代码在安装时于你机器上执行,请只对可信源码授权并锁定 commit(`github:<owner>/dsh-jenkins-plugin#<sha>`)。想省掉这一步就用 npm 或 tarball。
39
+
40
+ ## 你会得到什么
41
+
42
+ **右栏面板**(侧栏右侧的 **Jenkins** 页签)分两个模块:
43
+
44
+ - **我关注的** —— 每个关注的 job 一张卡片:状态点、构建号、状态标签、**实时进度条**、当前阶段名、本次构建的变更说明、Maven 项目的版本号(构建结束后可取到时)。卡片直接来自 SSE 推送,不是轮询页面。
45
+ - **所有 job** —— 默认折叠,展开后每行末尾的 ☆/★ 一键关注。
46
+
47
+ 点 job 或卡片继续下钻,全程不跳出面板:构建历史(可翻页)→ 构建详情(阶段条、内嵌控制台日志、测试摘要、变更集、产物列表)。产物下载是唯一会打开 Jenkins 页面的动作。
48
+
49
+ **写操作**都在面板内完成,并且是「按钮 → 行内二次确认 → 执行 → 就地显示结果」:
50
+
51
+ | 动作 | 说明 |
52
+ |---|---|
53
+ | 触发构建 | 参数化 job 有参数输入框(每行 `KEY=VALUE`) |
54
+ | 重新构建 | 只对失败的构建出现,**沿用上次构建的参数** |
55
+ | 中止构建 | `allowCancel` 默认关闭,开启后可用 |
56
+ | 交给 AI 分析 | 只对失败的构建出现,把失败日志尾部 + 阶段 + 变更集交给当前会话的模型,答案直接出现在对话里 |
57
+
58
+ `denyJobs` 里的 job 无论开关怎么设都拒绝,且**一个请求都不会发到 Jenkins**。
59
+
60
+ **给模型用的工具**(模型可以自己调用,不必你手点):
61
+
62
+ | 工具 | 作用 |
63
+ |---|---|
64
+ | `jenkins_jobs` | 列 job 与状态,可选带最近构建历史 |
65
+ | `jenkins_build_status` | 一个构建的结果、耗时、Pipeline 阶段、变更集、测试摘要 |
66
+ | `jenkins_log` | 控制台日志,默认取尾部,可按 `next_offset` 续读 |
67
+ | `jenkins_workspace` | 构建的工作空间目录或产物列表(读工作空间需要 Jenkins Workspace API 插件) |
68
+ | `jenkins_build` | 触发或中止构建(唯一的写工具;先过 `denyJobs`/`allow.*`,再要求操作者审批) |
69
+
70
+ **多实例**:设置页里可以配多个 Jenkins,逐个「测试连接」通过才允许保存(任何一行连不上则整次保存被拒、零写入);面板底部可切换实例,当前实例用绿色标出。多实例的实例清单和关注列表存在 `$DSH_HOME/jenkins.json`。
71
+
72
+ **完成通知**:关注的 job 一有构建结束就用平台的后台作业机制发一次通知,同一次构建只通知一次。
73
+
74
+ ## 权限与风险
75
+
76
+ 装之前请如实了解这个插件会做什么:
77
+
78
+ - **网络**:只向你配置的 Jenkins 地址发请求,没有别的外联。
79
+ - **凭据**:Basic 认证,密码/API token 通过 **DSH 自己的 credentials 服务**存取(`tokenRef` 指向一个凭据引用)。**插件自己的 `jenkins.json` 里只有地址、账号和凭据名,没有密码**,密码也不会出现在工具输出、面板或日志里。
80
+ - **写操作**:可以触发和中止构建。`allowTrigger` 默认开、`allowCancel` 默认关,`denyJobs` 可以硬封指定 job(含整个目录)。模型侧调用还要额外经过审批;面板上的点击是行内二次确认 + 同一份策略判定。
81
+ - **日志会进入模型上下文**:「交给 AI 分析」和模型自己调 `jenkins_log` 时,**构建日志的内容会作为提示词的一部分送给模型**。Jenkins 日志属于**不可信输入**——它可能包含任意文本(包括试图操纵模型的指令),请按此对待;默认只送失败日志的最后 16 KiB(`analyzeLogBytes`)。
82
+ - **本地状态**:`$DSH_HOME/jenkins.json`(实例清单 + 关注列表)、DSH credentials(密码)。
83
+ - 插件自己不会触发任何构建:只有你点按钮或模型显式调用才会。
84
+
85
+ ## 兼容性
86
+
87
+ - **Harness**:针对 `0.1.6-alpha.2` 构建并验证(`package.json` 的 `engines.dsh` 与 peerDependencies 都钉在这个版本)。
88
+ - **Node**:与 harness 一致,`^22.19.0 || >=24.0.0`。
89
+ - **载体**:面板是 Web GUI 的界面(`dsh web`),在 Web 组合上验证过。
90
+ - **Jenkins**:在 **Jenkins 2.176.2** 上实测(FreeStyle 与 Maven job,777 个 job 的实例)。几条如实降级:
91
+ - **Pipeline 阶段**需要 Blue Ocean 的 `wfapi` 端点;2.176.2 且没有 Pipeline job 时阶段列表为空,面板会退化成按耗时估算的进度条——这是"控制器没给阶段",不是面板坏了。
92
+ - **读工作空间**需要 Jenkins Workspace API 插件;缺失时 `jenkins_workspace` 会如实说明该 Jenkins 不支持。
93
+ - **Maven 版本号**取自 `mavenArtifacts/api/json`,只有 Maven job 有,且真实 2.176.2 上通常在构建**结束后**才产生,所以正在跑的 Maven 构建卡片上可能没有版本号(拿不到就不显示,绝不猜)。
94
+ - **产物没有大小**:Jenkins 的 `tree` 查询取不到 artifact size。
95
+ - **已知未完成**:`notifyWakeOnFailure`(失败时唤醒模型)与 `uiAutoOpenOnTrigger`(触发后自动展开面板)两个配置项已接受但**尚未生效**;`maxTrackedBuilds` 只是建议值,不硬性限制跟踪数量(关注列表是用户自己挑的)。
96
+
97
+ ## 配置
98
+
99
+ 静态配置只是**回退层**:面板设置页里配的实例优先,静态配置在没有面板设置时生效(实例 id 为 `config`)。`baseUrl`/`username` 可以留空——那正是"全部在面板里配"的用法。
100
+
101
+ ![Jenkins 设置页:多个实例、测试连接结果、保存](https://raw.githubusercontent.com/Endless-zby/dsh-jenkins-plugin/main/doc/settings-instances.png)
102
+
103
+ ```yaml
104
+ - id: jenkins
105
+ name: dsh-jenkins-plugin
106
+ config:
107
+ baseUrl: https://ci.example.com/jenkins
108
+ username: ci-bot
109
+ tokenRef: JENKINS_TOKEN
110
+ denyJobs: ['infra/*']
111
+ ```
112
+
113
+ | 字段 | 默认 | 含义 |
114
+ |---|---|---|
115
+ | `baseUrl` | 空 | Jenkins 根地址;留空表示只用面板设置 |
116
+ | `username` | 空 | 与 token 配对的登录名 |
117
+ | `tokenRef` | `JENKINS_TOKEN` | 存放 API token 的凭据引用;每次调用现取,支持热轮换 |
118
+ | `settingsFile` | `jenkins.json` | 面板设置文件:绝对路径,或 harness home 下的文件名 |
119
+ | `tlsMode` | `verify` | `verify` 校验证书;`allowSelfSigned` 接受不受信任的证书 |
120
+ | `tlsCaFile` | — | `verify` 模式下用于校验的自签 CA PEM |
121
+ | `timeoutMs` | `15000` | 单个 HTTP 请求的上限 |
122
+ | `crumbMode` | `auto` | `auto` 在写请求前取 CSRF crumb;`off` 跳过 |
123
+ | `denyJobs` | `[]` | 永不允许触发/中止的 job 路径;`folder/*` 匹配整个目录 |
124
+ | `allowTrigger` | `true` | 是否允许触发构建 |
125
+ | `allowCancel` | `false` | 是否允许中止构建 |
126
+ | `progressIntervalMs` | `3000` | 跟踪构建的阶段轮询间隔 |
127
+ | `logIntervalMs` | `1500` | 面板展示的构建的日志轮询间隔 |
128
+ | `idleBackoffMaxMs` | `30000` | 没有进展时的退避上限 |
129
+ | `queueTimeoutMs` | `300000` | 排队超过多久就报超时 |
130
+ | `maxTrackedBuilds` | `5` | 跟踪表的建议上限(不强制) |
131
+ | `retainMs` | `600000` | 已结束构建留在跟踪表里的时长 |
132
+ | `historyCount` | `10` | 构建历史默认条数(最大 50) |
133
+ | `maxLogBytes` | `262144` | 单次控制台日志**返回**的字节上限 |
134
+ | `logReadBytes` | `8388608` | 单次从控制器**读取**的日志字节窗口 |
135
+ | `maxListingBytes` | `4194304` | 递归列 job 时单次响应的字节上限 |
136
+ | `maxWorkspaceEntries` | `500` | 单次工作空间目录列出的条目上限 |
137
+ | `maxReadFileBytes` | `1048576` | 单个工作空间文件的读取上限 |
138
+ | `notifyOnComplete` | `true` | 关注的构建结束时是否发通知 |
139
+ | `notifyWakeOnFailure` | `false` | 失败时唤醒模型(**尚未生效**) |
140
+ | `allowAnalyze` | `true` | 是否允许把失败构建交给模型分析 |
141
+ | `analyzeLogBytes` | `16384` | 交给模型分析的日志尾部字节数 |
142
+ | `uiAutoOpenOnTrigger` | `true` | 触发后自动展开面板(**尚未生效**) |
143
+
144
+ `maxLogBytes` 和 `logReadBytes` 是两件事,因为真实 Jenkins 会**忽略 `?start=`**:读取窗口必须显著大于页大小,否则拿到的"尾巴"其实是日志开头。这也是本插件按窗口读取、本地切片的原因。
145
+
146
+ ## 开发
147
+
148
+ ```sh
149
+ npm install # 会跑一次 prepare(=构建)
150
+ npm run typecheck
151
+ npm test # 235 项断言,无测试框架:每个 tests/*.ts 都是可直接运行的脚本
152
+ npm run build # tsc(Host 半)+ esbuild(浏览器半 → lib/client.js)
153
+ npm run check # test + 密钥扫描 + 打包内容审计
154
+ ```
155
+
156
+ 浏览器半被构建成客户端模块系统的懒加载工厂(`window.__ModuleLoader__.load({ id, factory })`),React / Cordis / 静态 UI 库保持 external 并通过注入的 `require` 解析,其余全部内联——平台基线模块之外没有运行时依赖。
157
+
158
+ `tests/stub-jenkins.ts` 是一个零依赖的 stub Jenkins(`node tests/stub-jenkins.ts`),带阶段、失败构建、参数化 Maven job、写端点与队列延迟,用来验证面板的实时链路和写路径而不碰真实控制器。
159
+
160
+ 发布前的三道闸(CI 也是这三条):
161
+
162
+ ```sh
163
+ npm run check:secrets # 扫"提交会带上的文件"里的凭据形状(内网主机名/个人路径只告警)
164
+ npm run check:payload # 读 npm pack 的清单:产物齐不齐、有没有把 src/ 或内部笔记打进去
165
+ npm pack # 出 tarball,先在一个干净 profile 上装一遍再 publish
166
+ ```
167
+
168
+ ## 许可证
169
+
170
+ [MIT](LICENSE)
@@ -0,0 +1,7 @@
1
+ # dsh-jenkins-plugin bundle layer.
2
+ #
3
+ # The row is inserted with the plugin's shape; a profile's own cordis.patch.yml
4
+ # can override this row by id, and the user's patch layer wins over this file.
5
+ - insert:
6
+ - id: jenkins
7
+ name: dsh-jenkins-plugin
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Asking the model to explain a failed build.
3
+ *
4
+ * The panel's "hand it to the AI" button is one HTTP call and then a model turn,
5
+ * which costs tokens, so what goes into that turn is the whole design: a bounded
6
+ * tail of the console log, the facts the panel already knows (which build, which
7
+ * stage failed, what commits it carried), and an instruction narrow enough that
8
+ * the answer is a diagnosis and a fix rather than a summary of the log.
9
+ *
10
+ * The prompt is built here, as a pure function, because "what exactly did we send
11
+ * the model" is the part worth asserting — the route around it is plumbing.
12
+ * @module dsh-jenkins-plugin/analyze
13
+ */
14
+ import type { JenkinsBuildDetail, JenkinsChange, JenkinsLogPage } from './jenkins/types.js';
15
+ /** What the panel knows about the failed build when it hands it over. */
16
+ export interface FailureContext {
17
+ /** Job path, as `folder/sub/job`. */
18
+ jobPath: string;
19
+ /** Build number. */
20
+ buildNumber: number;
21
+ /** Terminal outcome, which the caller has already established is a failure. */
22
+ outcome: string;
23
+ /** Instance name, so the model can name it back if it needs to. */
24
+ instanceName?: string;
25
+ /** The stage Jenkins reported as failed, when the job is a Pipeline. */
26
+ failedStage?: string;
27
+ /** What triggered the build, when Jenkins reported it. */
28
+ cause?: string;
29
+ /** Commits the build carried, newest first. */
30
+ changes?: readonly JenkinsChange[];
31
+ /** The console log, already sliced to the tail by the caller. */
32
+ log: string;
33
+ /** Size of the whole log in bytes, when Jenkins stated it. */
34
+ totalBytes?: number;
35
+ /** Whether {@link log} is only the tail of a longer log. */
36
+ truncated: boolean;
37
+ }
38
+ /**
39
+ * The key one build's analysis is reserved under.
40
+ *
41
+ * Used to refuse a second hand-off of the same build while the first is still
42
+ * being prepared, so a double click cannot buy two model turns.
43
+ * @param sessionId - the session the answer will appear in.
44
+ * @param jobPath - job path.
45
+ * @param buildNumber - build number.
46
+ * @returns the reservation key.
47
+ */
48
+ export declare function analysisKey(sessionId: string, jobPath: string, buildNumber: number): string;
49
+ /**
50
+ * The two reads one hand-off needs.
51
+ *
52
+ * Narrowed to exactly what is used, so the hand-off can be driven by a fake in a
53
+ * test without standing up a controller.
54
+ */
55
+ export interface AnalyzeReads {
56
+ /**
57
+ * One build's detail — outcome, stages, commits.
58
+ * @param jobPath - folder path of the job.
59
+ * @param selector - build number.
60
+ * @param options - byte bound.
61
+ * @returns the detail.
62
+ */
63
+ buildDetail(jobPath: string, selector: string, options: {
64
+ maxBytes: number;
65
+ }): Promise<JenkinsBuildDetail>;
66
+ /**
67
+ * A page of the build's console log.
68
+ * @param jobPath - folder path of the job.
69
+ * @param selector - build number.
70
+ * @param offset - byte offset; zero means "the tail", which is what a failure
71
+ * analysis wants.
72
+ * @param maxBytes - byte cap on the returned page.
73
+ * @returns the page.
74
+ */
75
+ consoleLog(jobPath: string, selector: string, offset: number, maxBytes: number): Promise<JenkinsLogPage>;
76
+ }
77
+ /** What a completed hand-off reports back to whoever clicked. */
78
+ export interface HandOff {
79
+ /** The build that was handed over. */
80
+ buildNumber: number;
81
+ /** Its outcome, as Jenkins reported it. */
82
+ outcome: string;
83
+ /** Bytes of log actually handed to the model. */
84
+ logBytes: number;
85
+ /** Size of the whole log, when Jenkins stated it. */
86
+ totalBytes?: number;
87
+ /** Whether only the tail was handed over. */
88
+ truncated: boolean;
89
+ /** Characters of prompt queued in the session. */
90
+ promptChars: number;
91
+ }
92
+ /**
93
+ * Read a failed build and queue one prompt about it.
94
+ *
95
+ * The log is read as the **tail**: the failing lines are at the end, and the
96
+ * request is bounded so a hundred-megabyte log cannot turn into a hundred
97
+ * megabytes of context. Everything else rides along from the same read the panel
98
+ * already makes, so one click costs two Jenkins requests and one model turn.
99
+ * @param input.client - the Jenkins reads.
100
+ * @param input.jobPath - job path.
101
+ * @param input.buildNumber - the failed build.
102
+ * @param input.logBytes - byte cap on the log handed over.
103
+ * @param input.followup - queues the prompt in the calling session.
104
+ * @param input.instanceName - instance name, for the prompt's context.
105
+ * @returns what was handed over.
106
+ */
107
+ export declare function handOverFailure(input: {
108
+ client: AnalyzeReads;
109
+ jobPath: string;
110
+ buildNumber: number;
111
+ logBytes: number;
112
+ followup: (prompt: string) => void;
113
+ instanceName?: string;
114
+ }): Promise<HandOff>;
115
+ /**
116
+ * Build the prompt that asks the model to diagnose one failed build.
117
+ *
118
+ * The instruction is deliberately strict about the shape of the answer: a cause
119
+ * with the log line that shows it, then one to three concrete fixes. Without that
120
+ * the model tends to restate the log or hand back generic advice, which is the
121
+ * one thing a person who has already read the log does not need.
122
+ * @param context - the build, its commits, and the log tail.
123
+ * @returns the message text to hand to a model turn.
124
+ */
125
+ export declare function failurePrompt(context: FailureContext): string;
package/lib/analyze.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Asking the model to explain a failed build.
3
+ *
4
+ * The panel's "hand it to the AI" button is one HTTP call and then a model turn,
5
+ * which costs tokens, so what goes into that turn is the whole design: a bounded
6
+ * tail of the console log, the facts the panel already knows (which build, which
7
+ * stage failed, what commits it carried), and an instruction narrow enough that
8
+ * the answer is a diagnosis and a fix rather than a summary of the log.
9
+ *
10
+ * The prompt is built here, as a pure function, because "what exactly did we send
11
+ * the model" is the part worth asserting — the route around it is plumbing.
12
+ * @module dsh-jenkins-plugin/analyze
13
+ */
14
+ /** How many commits the prompt names before it stops listing them. */
15
+ const COMMITS_IN_PROMPT = 5;
16
+ /** How many characters of one commit message the prompt keeps. */
17
+ const COMMIT_MESSAGE_CHARS = 120;
18
+ /**
19
+ * The key one build's analysis is reserved under.
20
+ *
21
+ * Used to refuse a second hand-off of the same build while the first is still
22
+ * being prepared, so a double click cannot buy two model turns.
23
+ * @param sessionId - the session the answer will appear in.
24
+ * @param jobPath - job path.
25
+ * @param buildNumber - build number.
26
+ * @returns the reservation key.
27
+ */
28
+ export function analysisKey(sessionId, jobPath, buildNumber) {
29
+ return `${sessionId}/${jobPath}#${buildNumber}`;
30
+ }
31
+ /**
32
+ * Read a failed build and queue one prompt about it.
33
+ *
34
+ * The log is read as the **tail**: the failing lines are at the end, and the
35
+ * request is bounded so a hundred-megabyte log cannot turn into a hundred
36
+ * megabytes of context. Everything else rides along from the same read the panel
37
+ * already makes, so one click costs two Jenkins requests and one model turn.
38
+ * @param input.client - the Jenkins reads.
39
+ * @param input.jobPath - job path.
40
+ * @param input.buildNumber - the failed build.
41
+ * @param input.logBytes - byte cap on the log handed over.
42
+ * @param input.followup - queues the prompt in the calling session.
43
+ * @param input.instanceName - instance name, for the prompt's context.
44
+ * @returns what was handed over.
45
+ */
46
+ export async function handOverFailure(input) {
47
+ const detail = await input.client.buildDetail(input.jobPath, String(input.buildNumber), {
48
+ // The detail read is capped independently of the log: a job with thousands of
49
+ // commits must not turn a diagnosis into a history dump.
50
+ maxBytes: Math.max(input.logBytes, 65_536),
51
+ });
52
+ const page = await input.client.consoleLog(input.jobPath, String(input.buildNumber), 0, input.logBytes);
53
+ const failedStage = detail.stages.find(stage => stage.status === 'FAILED')?.name;
54
+ const prompt = failurePrompt({
55
+ jobPath: input.jobPath,
56
+ buildNumber: detail.number,
57
+ outcome: detail.outcome,
58
+ ...input.instanceName === undefined ? {} : { instanceName: input.instanceName },
59
+ ...failedStage === undefined ? {} : { failedStage },
60
+ ...detail.cause === undefined ? {} : { cause: detail.cause },
61
+ ...detail.changes.length === 0 ? {} : { changes: detail.changes },
62
+ log: page.text,
63
+ ...page.totalSize === undefined ? {} : { totalBytes: page.totalSize },
64
+ truncated: page.truncated,
65
+ });
66
+ input.followup(prompt);
67
+ return {
68
+ buildNumber: detail.number,
69
+ outcome: detail.outcome,
70
+ logBytes: Buffer.byteLength(page.text, 'utf8'),
71
+ ...page.totalSize === undefined ? {} : { totalBytes: page.totalSize },
72
+ truncated: page.truncated,
73
+ promptChars: prompt.length,
74
+ };
75
+ }
76
+ /**
77
+ * Build the prompt that asks the model to diagnose one failed build.
78
+ *
79
+ * The instruction is deliberately strict about the shape of the answer: a cause
80
+ * with the log line that shows it, then one to three concrete fixes. Without that
81
+ * the model tends to restate the log or hand back generic advice, which is the
82
+ * one thing a person who has already read the log does not need.
83
+ * @param context - the build, its commits, and the log tail.
84
+ * @returns the message text to hand to a model turn.
85
+ */
86
+ export function failurePrompt(context) {
87
+ const commits = (context.changes ?? []).slice(0, COMMITS_IN_PROMPT);
88
+ const lines = [
89
+ 'Jenkins 构建失败。请分析失败原因,并给出最简洁的修复指引。',
90
+ '',
91
+ `任务:${context.jobPath} 构建 #${context.buildNumber}`,
92
+ `结果:${context.outcome}`,
93
+ ];
94
+ if (context.instanceName !== undefined && context.instanceName.length > 0) {
95
+ lines.push(`实例:${context.instanceName}`);
96
+ }
97
+ lines.push(`失败阶段:${context.failedStage ?? '未报告(该 job 不是 Pipeline,或 Jenkins 没给出阶段)'}`);
98
+ if (context.cause !== undefined && context.cause.length > 0) {
99
+ lines.push(`触发原因:${context.cause}`);
100
+ }
101
+ if (commits.length > 0) {
102
+ lines.push('本次构建的提交:');
103
+ for (const change of commits) {
104
+ const first = change.message.split('\n')[0]?.trim() ?? '';
105
+ const message = first.length > COMMIT_MESSAGE_CHARS ? `${first.slice(0, COMMIT_MESSAGE_CHARS)}…` : first;
106
+ lines.push(`- ${change.commitId.slice(0, 7)} ${message}${change.author.length === 0 ? '' : ` (${change.author})`}`);
107
+ }
108
+ }
109
+ const size = context.totalBytes === undefined ? '未知' : `${context.totalBytes} 字节`;
110
+ lines.push('', `下面是构建日志的尾部${context.truncated ? `(日志共 ${size},只给了最后一段)` : ''}:`, '```', context.log.trimEnd(), '```', '', '请按这个顺序回答,不要复述日志:', '1. 最可能的失败原因(一句话),并指出日志里支撑它的关键行。', '2. 最简洁的修复指引(1-3 条,具体到改哪里/做什么)。', '3. 如果这段日志不足以判断,直接说要哪一段,或用 jenkins_log(可加 tail_lines,最多 2000 行)拉取更多;', ' 需要时还有 jenkins_build_status 和 jenkins_workspace 可用。', '不要给与日志无关的通用建议,不要猜测日志里没有依据的东西。');
111
+ return lines.join('\n');
112
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The panel's drill-down: one job's build history, and one build's detail.
3
+ *
4
+ * Everything here is rendered inside the plugin's own panel — no view sends the
5
+ * browser to the Jenkins UI. The single exception is an artifact's download
6
+ * link, because a binary is not something a panel should inline.
7
+ * @module dsh-jenkins-plugin/client/Builds
8
+ */
9
+ import type { ReactNode } from 'react';
10
+ import type { JenkinsKey } from './locales.js';
11
+ /** Copy binder for this plugin's namespace. */
12
+ type Translate = (key: JenkinsKey, params?: Record<string, unknown>) => string;
13
+ /**
14
+ * The job view: build history with paging.
15
+ * @param props - copy binder, instance id, job path, back navigation, and build opening.
16
+ * @returns the history view.
17
+ */
18
+ export declare function JobView({ t, instanceId, jobPath, onBack, onOpenBuild }: {
19
+ t: Translate;
20
+ instanceId: string;
21
+ jobPath: string;
22
+ onBack: () => void;
23
+ onOpenBuild: (build: number) => void;
24
+ }): ReactNode;
25
+ /**
26
+ * The build view: header, progress, stages, log, and result area.
27
+ * @param props - copy binder, instance id, job path, build selector, and back navigation.
28
+ * @returns the build detail view.
29
+ */
30
+ export declare function BuildView({ t, instanceId, jobPath, build, onBack }: {
31
+ t: Translate;
32
+ instanceId: string;
33
+ jobPath: string;
34
+ build: string;
35
+ onBack: () => void;
36
+ }): ReactNode;
37
+ export {};