dsh-plugin-cicd 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +389 -2
- package/RELEASING.md +63 -0
- package/client.js +3786 -0
- package/cordis.patch.yml +63 -0
- package/icon.svg +8 -0
- package/index.js +4662 -0
- package/lib/bilibili.mjs +822 -0
- package/lib/config-store.mjs +159 -0
- package/package.json +62 -3
- package/scripts/configure.mjs +213 -0
- package/scripts/push-via-api.ps1 +111 -0
- package/scripts/verify-bundle.mjs +320 -0
- package/tests/bilibili-check.mjs +398 -0
- package/tests/bump-e2e.mjs +352 -0
- package/tests/client-render.mjs +1267 -0
- package/tests/host-checks.mjs +521 -0
- package/tests/mount-check.mjs +297 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 林冠宇 (SOH4C4759) and dsh-plugin-cicd contributors
|
|
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,390 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-cicd(DSH 插件发布台)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把 **DSH 插件的持续构建与发布(CI/CD)**搬进 DeepSeek Harness:侧边栏一个图标,点开是一个占满主区域的独立面板。
|
|
4
|
+
|
|
5
|
+
> **名字**:界面显示名为「DSH 插件发布台」(副标题「持续构建与发布(CI/CD)」;英文界面为 `DSH Plugin CI/CD`)。包名与仓库 id 保持 `dsh-plugin-cicd` —— 它们只是技术标识,改名会破坏已有安装(profile 的 `link:` 路径、bundles 条目、patch 里的 row id、配置文件路径)。
|
|
6
|
+
|
|
7
|
+
它回答四个 GitHub 页面各自只说了一部分的问题:
|
|
8
|
+
|
|
9
|
+
- **构建过了吗** —— 每个仓库最近一次运行的状态、耗时、触发事件。
|
|
10
|
+
- **发布了吗** —— 已发布的 Release、还在草稿箱里的 Release、资产清单。
|
|
11
|
+
- **本地领先于发布吗** —— 本地 `package.json` 版本、领先/落后提交数、未提交文件数。
|
|
12
|
+
- **装的是发布出去的那份吗** —— 本机 profile 里这份是 `link:` 的本地检出、还是 Release 上的 tgz;差几号版本。
|
|
13
|
+
- **npm 上是哪一版** —— 这个版本在不在公开源上;不在的话,一次点击就能推上去。
|
|
14
|
+
|
|
15
|
+
也就是说:**一个包,两条分发渠道**。GitHub Release 给人下载与审阅,npm 让 `dsh plugin add <名字>` 一条命令装完。
|
|
16
|
+
|
|
17
|
+
面板上能直接做的动作,**行上的顺序就是工作的顺序**:`提交`(`git add -A` + commit + push)、`构建`(触发 `ci.yml`)、`发布`(触发 `release.yml`,默认产出草稿)、`升版本并发布`、`公开发布草稿`、`推送到 npm vX`(+ 在面板里写 npm token)、`装 Release vX` / `更新到 vX`、`重跑`、`取消`、`看失败日志`,以及更新之后的 `立即重启 DSH`。
|
|
18
|
+
|
|
19
|
+
**`提交` 为什么排在最前面**:`构建` 和 `发布` 都作用于 **GitHub 上的那个提交**——workflow 跑的是推送上去的 commit。所以只存在于工作区、或只存在于本机的改动,它们**都看不见**。面板一直在警告这件事(脏工作区那行、`↑N` 那个 chip),在这之前唯一的出路是开终端。它**永远不是主按钮**:它是步骤,不是目的。
|
|
20
|
+
|
|
21
|
+
`提交` 一个按钮两种含义,因为两者是同一个陷阱的两半:**有改动** → 提交并推送;**工作区干净但领先上游** → 只推送(按钮此时显示`推送`);**两者皆无** → 按钮禁用并写明原因。`git add -A` 是刻意的(发布也需要新增文件),所以确认框会把**待提交的文件名列出来**——"7 个改动"可能是你要的 3 个加上你没见过的 4 个。提交信息为空时确认按钮保持禁用。
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
本机 profile 用 `link:` 指向检出目录,和其它插件一致:
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
dsh plugin --profile desktop add "link:F:\CodeProj\dsh-plugin-cicd"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
装完需要重启 DSH 才会被组合进运行时(宿主半边与浏览器半边都要)。
|
|
32
|
+
|
|
33
|
+
## 零代码操作(给不写程序的人)
|
|
34
|
+
|
|
35
|
+
面板的目标是**只用鼠标**就能完成日常使用。凡是下表打了勾的,都不需要打开终端:
|
|
36
|
+
|
|
37
|
+
| 要做的事 | 怎么做 | 需要命令行吗 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| 登录 GitHub | 面板顶部的「用浏览器登录 GitHub」→ 打开授权页 → 输入面板显示的一次性代码 | **不需要**(Host 替你跑 `gh auth login --web`,把码显示出来) |
|
|
40
|
+
| 补齐权限 | 同一处的「补齐权限」,走同样的浏览器流程 | **不需要** |
|
|
41
|
+
| 添加仓库 | 「管理仓库」→ 从你的仓库列表勾选(本地检出自动匹配) | **不需要** |
|
|
42
|
+
| 移除仓库 | 取消勾选 | **不需要** |
|
|
43
|
+
| 触发构建 / 发布出草稿 | 仓库那一行的「构建」「发布」 | **不需要** |
|
|
44
|
+
| 公开草稿 | 展开行 →「公开草稿」→「确认公开?」(两次点击) | **不需要** |
|
|
45
|
+
| 给 npm 一个 token | 面板顶部(未登录时才出现)粘贴 Access Token →「写入并验证」 | **不需要**(Host 写进 npm 自己读的 `~/.npmrc`,插件不保存 token) |
|
|
46
|
+
| 推送到 npm | 展开行 →「推送到 npm vX」→「确认推送」(两次点击;开了 2FA 就在同一行填一次性密码) | **不需要**(Host 用 DSH 自带的 pnpm 发布,复用同一个包管理器) |
|
|
47
|
+
| 更新已装的那份 | 行上的「更新到 vX」(**只有 profile 装的是更旧的发布包时才有**;`link:` 检出与"装它会降级"两种情况的按钮在展开处)→ 展开处「确认更新」 | **不需要**(Host 把 tgz 交给 DSH 自己的插件管理器安装) |
|
|
48
|
+
| 让新版生效 | 更新后面板直接问「现在重启?」→「立即重启 DSH」 | **不需要**(转发给 `dsh-plugin-restart`;没装它会明确说) |
|
|
49
|
+
| 看失败日志 / 重跑 / 取消 | 展开行里的对应按钮 | **不需要** |
|
|
50
|
+
| 账号、权限、仓库清单 | 设置 → **插件** → **DSH 插件发布台** → **GitHub** | **不需要** |
|
|
51
|
+
|
|
52
|
+
**仍然需要命令行的两件事**,如实列出,不假装不需要:
|
|
53
|
+
|
|
54
|
+
1. **安装这个插件本身**(一次性)——见下一节。
|
|
55
|
+
2. **改版本号并打 tag**——版本号写在 `package.json`,tag 指向提交,这是"发布的到底是什么"的唯一来源;面板不去改你的源码。面板的「发布」按**当前**版本号出包,要在同一版本上重发就再点一次(会替换资产)。
|
|
56
|
+
|
|
57
|
+
「更新」装完要重启才生效,这一步**面板会主动问**(见下文《更新已安装的插件》);重启由 `dsh-plugin-restart` 执行,发布台自己不碰进程。
|
|
58
|
+
|
|
59
|
+
连 `gh` 都没装时,面板给下载页链接;而安装 `gh` 这一步在 Windows 上目前仍需一次终端(或用安装包)。
|
|
60
|
+
|
|
61
|
+
## 别人怎么装(下载即用)
|
|
62
|
+
|
|
63
|
+
Release 里有两个资产,回答两个不同的问题:
|
|
64
|
+
|
|
65
|
+
| 资产 | 用途 |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `dsh-plugin-cicd-<version>.tgz` | **可安装的那份**:一条命令装,不用解压、不需要本地检出 |
|
|
68
|
+
| `dsh-plugin-cicd-<version>.zip` | 整棵仓库树,给人看/审/做 diff |
|
|
69
|
+
| `SHA256SUMS.txt` | 上面两个的校验和(用途是"确认你下载的没坏",不是防篡改——见 [RELEASING.md](RELEASING.md)) |
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
# 1. 下载 tgz(或从 Release 页面直接下)
|
|
73
|
+
gh release download v0.1.0 -R SOH4C4759/dsh-plugin-cicd -p '*.tgz'
|
|
74
|
+
|
|
75
|
+
# 2. 装进你的 profile(把 desktop 换成你自己的)
|
|
76
|
+
dsh plugin --profile desktop add "file:$PWD\dsh-plugin-cicd-0.1.0.tgz"
|
|
77
|
+
|
|
78
|
+
# 3. 重启 DSH,侧边栏出现「发布台」图标
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`file:` 指向本地的 tgz,pnpm 会解包进 profile 的 `node_modules`。这条路径已经在发布资产上实测过:解包后 `scripts/verify-bundle.mjs` 与 `tests/host-checks.mjs` 都能跑通,`cordis.patch.yml` 与 `scripts/configure.mjs` 都在包里(`npm pack` 只带 `files` 白名单,CI 每次都验这两点)。
|
|
82
|
+
|
|
83
|
+
## 更新已安装的插件(把上一条从终端搬进面板)
|
|
84
|
+
|
|
85
|
+
上面那三行命令,现在就是面板上一个按钮:**「装 Release vX」/「更新到 vX」**。它做的正是那三行的前两步,第三步(重启)由面板接着问。
|
|
86
|
+
|
|
87
|
+
每一行都会先回答"这个 profile 里现在装的是什么",因为**版本号相等不代表同一份代码**:
|
|
88
|
+
|
|
89
|
+
| profile 里的依赖 | 面板的判断 | 行上有没有按钮 |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `link:F:\...`(本地检出) | **本地检出** —— 跑的不是发布出去的那份,即使版本号与 Release 完全相同 | **没有**。展开处有次要按钮【装 Release vX】(+ 说明会替换掉什么),因为这台机器是**开发这份插件的地方**,行上的按钮只会把正在改的代码换成 tgz |
|
|
92
|
+
| `file:...\x-1.2.0.tgz`,装的是 1.2.0,Release 是 1.3.0 | **可更新** —— 行上出现「可更新 v1.3.0」标记 | **有**,【更新到 v1.3.0】——这是行上唯一会被摆出来的安装动作 |
|
|
93
|
+
| 装的是 2.0.0,Release 是 1.3.0 | 面板明说**装它会往回退**,不假装是"更新" | **没有**(等于把降级当日常维护)。展开处仍可点,但只是次要按钮 |
|
|
94
|
+
|
|
95
|
+
**为什么把行让出来**:面板的行是"你现在要做的事"。对**插件作者自己**来说,`link:` 是常态,装 Release 不是要做的事,而是**取消掉让开发能进行的那套设置**;而 `ahead`(profile 里更新)那种情况,行上摆按钮等于**把降级摆在最显眼的位置**。这个能力保留在展开处——"发布出去的那份到底装不装得起来"仍然要能在这里回答——只是不再叫卖。
|
|
96
|
+
| 装的是 1.2.0-rc.1 | 两边**无法比较**,如实说不能比较,而不是猜一个先后 |
|
|
97
|
+
| 版本相同、且就是 Release 的 tgz | **已是最新**,没有按钮 |
|
|
98
|
+
| 不在这个 profile 的依赖里 | **未安装** —— 没有"已装的那份"可以更新,并写出找的是哪个 profile |
|
|
99
|
+
| 还没有已发布的 Release(草稿不算) | **没有可装的** |
|
|
100
|
+
|
|
101
|
+
装的时候:
|
|
102
|
+
|
|
103
|
+
1. `gh release download <tag> -p <tgz>` 把资产下到**插件自己的目录** `<DSH_HOME>\dsh-plugin-cicd\downloads\`(不是临时目录——profile 的依赖会指向这个路径,下完就删会让清单再也装不上)。
|
|
104
|
+
2. 交给 **DSH 自己的插件管理器**(`ctx.get('pluginManager').installBundle`),也就是 `dsh plugin add` 走的同一条 pnpm 路径:它持有 profile 锁,失败时把 `package.json` / `pnpm-lock.yaml` 还原。发布台**不**自己起一个 `dsh plugin` 子进程——那会是对同一个 profile 的第二个写入者。
|
|
105
|
+
3. 面板接着**问要不要重启**(不假设答案)。重启转发给 `dsh-plugin-restart`;没装这个插件时回一句明确的「没挂载」,而不是一个看不出所以然的失败。
|
|
106
|
+
|
|
107
|
+
一次只跑一个更新(再来一个回 `409 busy`),因为插件管理器本来就会排队,而两个转圈比一个明确的拒绝更难懂。
|
|
108
|
+
|
|
109
|
+
**没有装它就没有这部份**:`update` 路由需要 Host 提供 `pluginManager` 服务,没有就回 `501 plugin-manager-missing`,而不是假装装过。
|
|
110
|
+
|
|
111
|
+
## 推送到 npm(第二条分发渠道)
|
|
112
|
+
|
|
113
|
+
Release 是给人下载的,npm 是给 `dsh plugin add` 装的。面板把它们并排放:一行上既有 GitHub 的 tag,也有 npm 上的版本。**独立按钮,不和「发布」绑在一起**——推送不可逆,不该搭在另一个动作上顺带发生。
|
|
114
|
+
|
|
115
|
+
每一行先说清 npm 现在是什么状态:
|
|
116
|
+
|
|
117
|
+
| 面板看到 | 判定 | 按钮 |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| 这个版本已在 npm 上 | **已发布** | 没有按钮(npm 不允许同一版本推第二次) |
|
|
120
|
+
| 名字在 npm 上还没人占 | **未注册** | 【推送到 npm vX】——这次就是**首次发布** |
|
|
121
|
+
| 名字已存在,但这个版本没推过 | **未发布** | 【推送到 npm vX】 |
|
|
122
|
+
| 问不到源(离线/被拦) | **未知** | **没有按钮**——宁可不给,也不给一个注定失败的 |
|
|
123
|
+
|
|
124
|
+
推送前会逐条拒绝,且**每一条都发生在任何字节上传之前**:
|
|
125
|
+
|
|
126
|
+
- **`private: true`** —— 作者的决定,不是这里能绕过的(本仓库集里的 `dsh-knowledge-console` 就是这种)。
|
|
127
|
+
- **工作区有未提交改动** —— `publish` 打包的是**工作目录**;`files` 白名单挡不住"白名单目录里的新文件"。
|
|
128
|
+
- **这个版本已经在 npm 上** —— npm 不接受同一版本推两次,撤回也只在很短的时间内可行。
|
|
129
|
+
- **没有登录 npm** —— 面板直接给出解决路径(见下)。
|
|
130
|
+
- **没有本地检出 / 没有版本号 / 读不到源**。
|
|
131
|
+
|
|
132
|
+
### 凭据:和复用 `gh` 同一个原则
|
|
133
|
+
|
|
134
|
+
插件**不保存任何 token**。它用 DSH 自带的 pnpm(`profileContext.packageManager`,也就是插件管理器安装时用的那个),凭据来自 npm 自己读的用户级 `.npmrc`。
|
|
135
|
+
|
|
136
|
+
`gh` 那侧不需要教:它自己开浏览器设备码流程。npm 这侧不一样——token 得**手动在网站上建**,而且那条路的 UI 在 2025 年 11 月变过。所以未登录时面板给的是**从零开始的四步**,不是一句话:
|
|
137
|
+
|
|
138
|
+
| 步 | 面板说什么 |
|
|
139
|
+
|---|---|
|
|
140
|
+
| ① 没有账号 | 「打开 npmjs.com 注册」按钮 + **注册后必须到邮箱点确认链接**(未验证邮箱的账号发不出去) |
|
|
141
|
+
| ② 建 token | 「打开 token 页面」→ `npmjs.com/settings/~/tokens`。**类型只有 Granular Access Token 一种可选**:Classic token 已于 **2025-11-19 被 npm 全部撤销**,现在也建不出来。Permissions 选 `Read and write`,Packages 选全部或只勾这个包 |
|
|
142
|
+
| ②a 有效期 | **最长 90 天**,这是 npm 对可写 token 的硬限制。到期就静默失效,回到第 ② 步再建一个 |
|
|
143
|
+
| ②b 2FA | 只有"给 CI 用、没人能输一次性密码"时才勾 **Bypass 2FA**。在这里推**不要勾** |
|
|
144
|
+
| ③ 粘贴 | 写进 `%USERPROFILE%\.npmrc`(**就是 `npm login` 会写的那个文件**;其他行原样保留,旧版本留一份 `.bak`),然后立刻用 `whoami` 验证 |
|
|
145
|
+
| ④ 2FA | 推送的确认行里会出现一次性密码输入框 |
|
|
146
|
+
|
|
147
|
+
token 不回显、不落插件、不进日志;页面在请求返回的那一刻就清空输入框。token 被拒时,面板直接把**排查清单**摊在同一块里(复制不全 / 权限不是 Read and write / 已撤销或超过 90 天 / 范围是别的包 / 邮箱未验证),而不是只回一句 `ERR_PNPM_WHOAMI_UNAUTHORIZED`。
|
|
148
|
+
|
|
149
|
+
**如果 npmjs.com 在你这台机器上打不开**——实测过:整站被 Cloudflare 挑战拦下(`/`、`/signup`、`/settings/~/tokens` 一起回 **403 `Just a moment...`**,走本地代理也一样)——那就**换一台设备或换一个网络**(手机流量最省事)生成 token,再粘到第 ③ 步。**面板只认这个 token,不在乎它在哪里生成**,这一步和你本机的浏览器无关。
|
|
150
|
+
|
|
151
|
+
同时**别再指望命令行**:`POST registry.npmjs.org/-/user/org.couchdb.user:*` 现在回 **405 Method Not Allowed**,也就是 `npm login --auth-type=legacy` / `pnpm login` 这条路已被 npm 移除;而建 token 的 `POST /-/npm/v1/tokens` 虽然还活着并真在认证(假凭据回 `404 User not found`),但 npm 公告写明新 classic token 不能再经 API 创建。所以**网站(或在另一台设备上打开它)是唯一的路**。发布本身不受影响:`registry.npmjs.org` 一直是通的。
|
|
152
|
+
|
|
153
|
+
**凭据是三个状态,不是一个布尔值**(`npmAuthState`)。`whoami` 是用户级端点,而 granular token 是**包级范围**的——它完全可能在 `whoami` 上被拒、却发布得好好的。把这种拒绝读成"没登录",就会正好挡住引导让用户去建的那种 token,还会对刚做完第 ③ 步的人说"你还没开始"。所以:
|
|
154
|
+
|
|
155
|
+
| 状态 | 面板 |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `signed-in`(`whoami` 答了) | 顶部显示 `npm <账号>`,不出引导 |
|
|
158
|
+
| `credential-present`(`.npmrc` 里有 token,但 `whoami` 不确认) | 一句说明 + 原样显示 `whoami` 的拒绝,**照着可以推**;真伪由推送来判 |
|
|
159
|
+
| `none`(两者都没有) | 四步引导 |
|
|
160
|
+
| 没有答案(`/npm-status` 未响应) | **什么都不显示**——"没有答案"不等于"没有凭据" |
|
|
161
|
+
|
|
162
|
+
同理,推送的闸门是"**有没有可用来认证的东西**",不是"`whoami` 答没答":token 不对的话推送本来就会失败,而失败已经被归类成下一步。
|
|
163
|
+
|
|
164
|
+
推送失败也**归类**成下一步做什么,而不是把 npm 的原话丢给用户:`otp-required`(去填一次性密码)、`email-unverified`(去点确认链接)、`not-logged-in`(重建 token)、`already-published`(先升版本)、`payment-required`(私有包要付费)、`forbidden`(检查 token 的 Packages 范围)、`not-found`、`rate-limited`、`registry-error`、`network`、`timeout`、`unknown`。npm 的原话仍然原样显示在下面——归类是补充,不是替换。
|
|
165
|
+
|
|
166
|
+
开了 2FA 的账号,推送时 npm 要一次性密码:确认行里就有那个输入框。子进程的 stdin 是关闭的(`ignore`),所以它**不会**挂着等一个没人持有的 stdin——失败会明说是要 OTP,而不是转圈到超时。
|
|
167
|
+
|
|
168
|
+
### 另一条路:完全不用 token(trusted publishing)
|
|
169
|
+
|
|
170
|
+
如果你的目标是"CI 里自动发布",**不该**用上面这条路。npm 支持 **trusted publishing(GitHub Actions OIDC)**:`release.yml` 加 `id-token: write` 权限并在 npm 上登记这个仓库,发布时用 OIDC 换一次性凭据——**没有任何长期 token**,还自带 provenance。这与本插件"不存凭据"的原则完全一致,也是本仓库更推荐的方向。
|
|
171
|
+
|
|
172
|
+
面板**不做**这件事:它是在**这台机器上推一次**的路径,不是 CI 的替代品。CI 发布需要 token 时,token 应放在仓库 secret 里(`NPM_TOKEN`),而不是这台机器上。
|
|
173
|
+
|
|
174
|
+
## B 站更新播报(第三条渠道)
|
|
175
|
+
|
|
176
|
+
插件更新了,除了 Release 和 npm,还有一件观众真的会看的事:在**介绍这个插件的视频**下面留一句"更新了什么"。这一条也做进面板里——每个仓库绑一个 BV 号,Release 公开之后自动在评论区补一条更新说明。
|
|
177
|
+
|
|
178
|
+
**触发条件只有一个:Release 已公开。** 草稿不算——草稿只有作者能看见,为它发"上线了"是对着空气说话。所以:
|
|
179
|
+
|
|
180
|
+
- 面板每 `bilibiliWatchSeconds`(默认 90 秒)巡检一次每个**已绑定**的仓库;
|
|
181
|
+
- 点【公开草稿】成功后立刻巡检一次(这一下就是"发布成功"的那一刻);
|
|
182
|
+
- DSH 启动约 15 秒后巡检一次——关机期间发布的那些版本,只有这一次能补上;
|
|
183
|
+
- 幂等键是 `(仓库, tag)`,落在 `<DSH_HOME>\dsh-plugin-cicd\bilibili-announcements.json` 里。**同一个版本永远只发一条**;这份记录读不出来时**什么也不发**(否则会把已经发过的评论再刷一遍),面板会把原因写在最上面。
|
|
184
|
+
|
|
185
|
+
绑定视频时会写一条**基线**:绑定那一刻已经公开的版本不算"这次更新"。所以把视频接上来不会追发历史版本,只有之后的新版本会播报。
|
|
186
|
+
|
|
187
|
+
### 凭据:为什么必须是"网页登录"
|
|
188
|
+
|
|
189
|
+
发评论走的是 B 站 **Web** 接口,所以凭据必须是网页会话。这里踩过一回,记在文档里免得再踩:`biliup login` 写下的 `cookies.json` 是 **BiliTV 登录**(`platform: BiliTV`)——它能投稿(APP 接口),但所有 Web 会员接口一律回 `-101 账号未登录`。所以本插件**不假设"文件里有 SESSDATA 就能用"**,而是拿凭据去问一次账号接口,并把答案写在面板上;认不出来时给出的原话是"这份凭据是 BiliTV 登录(APP/TV)……",不是"你没登录"。
|
|
190
|
+
|
|
191
|
+
凭据有两种给法,面板里都能做,不需要终端:
|
|
192
|
+
|
|
193
|
+
1. **【登录 B 站】**:走 passport 的网页二维码接口,生成一个链接——用手机 B 站扫,或者在你已经登录 B 站的浏览器里打开确认。面板轮询到确认后把 Cookie 存进插件自己的文件。
|
|
194
|
+
2. **粘贴一次**:浏览器 F12 → Application → Cookies → `bilibili.com`,把 `SESSDATA` 与 `bili_jct`(即 csrf)复制进来。B 站**当场接受才写入**——存一份已经回 `-101` 的凭据,只会让面板显示一个假的"已登录"。
|
|
195
|
+
|
|
196
|
+
两者都落在 `<DSH_HOME>\dsh-plugin-cicd\bilibili-cookies.json`(`bilibiliCookieFile` 可以指向别处,比如 biliup 那份,作为**兜底**读取;面板自己写的永远优先)。插件不回显这个文件的内容,【退出 B 站登录】也只删自己写的这一份——外部那份属于别的工具,不动。
|
|
197
|
+
|
|
198
|
+
### 评论内容
|
|
199
|
+
|
|
200
|
+
默认模板是 `【更新 {tag}】{summary}`:`{summary}` 取 Release 标题;标题就是版本号时改取 Release 正文第一段(去掉 Markdown、去掉 GitHub 自动生成的 "What's Changed / Full Changelog" 和 `by @someone in https://…` 尾巴)。可用占位符:`{tag}` `{version}` `{label}` `{repo}` `{title}` `{summary}` `{url}` `{date}`;不认识的占位符**留在原地并报出来**,不会被悄悄删掉。
|
|
201
|
+
|
|
202
|
+
默认模板**不带链接**:带外链的评论更容易被 B 站过滤,而被过滤和发成功在这边看起来一模一样。想要链接就把 `{url}` 写进 `bilibiliTemplate`。
|
|
203
|
+
|
|
204
|
+
发之前**一定先预览**:面板上点【发更新评论】会先问 Host "这条会写成什么"(`dryRun`),把即将发送的原文和 Host 的判决一起显示出来,确认后才真的发。预览由 Host 组装,不是浏览器——否则会出现"看到的是一句、发出去的是另一句"。
|
|
205
|
+
|
|
206
|
+
### 失败怎么处理
|
|
207
|
+
|
|
208
|
+
B 站的拒绝会被归类,而不是原样抛给用户:`-101` 未登录、`-400` 被拒、`-403` 无权限、`-412` 风控拦截、`-509` 频率限制、`12061` 内容被过滤……每一类给的是下一步做法。失败会记进播报记录;同一个版本失败 3 次后就停下等人工判断——风控拦下来的东西,连续重试只会更糟。
|
|
209
|
+
|
|
210
|
+
## 配置:用脚本,不要手改 YAML
|
|
211
|
+
|
|
212
|
+
包本身**不带仓库列表**——哪些仓库被监视是部署状态,不是包状态;把某个人的私有仓库名打进公开包,每个安装者都得先去拆它。
|
|
213
|
+
|
|
214
|
+
列表放在一个由脚本管理的 JSON 文件里(默认 `%USERPROFILE%\.dsh\dsh-plugin-cicd\repos.json`),profile patch 只留两行机器相关的配置:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
- id: dsh-plugin-cicd
|
|
218
|
+
config:
|
|
219
|
+
owner: SOH4C4759
|
|
220
|
+
ghPath: 'C:\Program Files\GitHub CLI\gh.exe'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
日常操作全走插件自带的脚本:
|
|
224
|
+
|
|
225
|
+
```powershell
|
|
226
|
+
$cfg = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-plugin-cicd\scripts\configure.mjs"
|
|
227
|
+
# 或直接用你的检出目录:F:\CodeProj\dsh-plugin-cicd\scripts\configure.mjs
|
|
228
|
+
|
|
229
|
+
node $cfg list # 现在登记了什么
|
|
230
|
+
node $cfg add dsh-plugin-restart --path F:\CodeProj\dsh-plugin-restart
|
|
231
|
+
node $cfg add someone-else/their-plugin # 别人的仓库,只读
|
|
232
|
+
node $cfg remove dsh-plugin-restart
|
|
233
|
+
node $cfg owner SOH4C4759
|
|
234
|
+
node $cfg check # 面板此刻会显示什么
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- 插件**每次请求都重读这个文件**,所以 `add` 在面板下一次轮询(默认 30 秒)就生效,**不需要重启**。
|
|
238
|
+
- 写入是原子的(临时文件 + rename),旧版本留一份 `.bak`。
|
|
239
|
+
- 校验与宿主同规则:仓库名必须是 `name` 或 `owner/name`,`--path` 必须是绝对路径——相对路径会按宿主进程的目录解析,那不是你的 shell 目录。
|
|
240
|
+
- 文件读不了或格式错时,面板会**明确报错**而不是假装"没有仓库",并回退到 patch 里的 `repos`。
|
|
241
|
+
|
|
242
|
+
patch 里仍可用的选项(每个都有默认值):
|
|
243
|
+
|
|
244
|
+
| 键 | 默认 | 含义 |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `owner` | `''` | 裸 `repo` 名用的 GitHub 账号;配置文件里的同名值优先 |
|
|
247
|
+
| `repos` | `[]` | 手写兜底;配置文件存在时以文件为准 |
|
|
248
|
+
| `configFile` | `<DSH_HOME>\dsh-plugin-cicd\repos.json` | 受管列表的位置 |
|
|
249
|
+
| `projectsRoot` | `''` | 本地检出所在根目录;面板添加仓库时据此自动填 `localPath`(按各目录 `package.json` 的 `name` 匹配,所以目录名与仓库名不同也能找到) |
|
|
250
|
+
| `defaultBranch` | `main` | 面板触发 workflow 用的 ref |
|
|
251
|
+
| `buildWorkflow` | `ci.yml` | `构建` 按钮触发的 workflow |
|
|
252
|
+
| `releaseWorkflow` | `release.yml` | `发布` 按钮触发的 workflow |
|
|
253
|
+
| `ghPath` | `''` | 空 = 从 PATH 与 `%ProgramFiles%\GitHub CLI\gh.exe` 等位置解析 |
|
|
254
|
+
| `requestTimeoutMs` | `20000` | 单次 `gh` 调用的硬超时 |
|
|
255
|
+
| `overviewTtlMs` | `15000` | 概览缓存窗口;面板轮询会走缓存 |
|
|
256
|
+
| `logTailLines` | `120` | 失败日志取最后多少行 |
|
|
257
|
+
| `pollSeconds` | `30` | 面板自动刷新间隔 |
|
|
258
|
+
| `npmRegistry` | `https://registry.npmjs.org/` | 推送目标源。改成私有源即可(只能 http/https,且 URL 里带凭据会被拒绝);`.npmrc` 里的 token 行按这个源的 host 匹配 |
|
|
259
|
+
| `enabled` | `true` | 关掉后路由只回「已停用」,不碰 GitHub |
|
|
260
|
+
|
|
261
|
+
所有值都做**夹取**而不是拒绝:手写的 patch 不该能让宿主起不来,最坏情况是某个路由报告「未配置」。
|
|
262
|
+
|
|
263
|
+
## 首次使用:授权引导
|
|
264
|
+
|
|
265
|
+
面板不自己存凭据,它用你机器上的 `gh`。所以第一次打开时它可能无事可做——这时面板不会只显示一句"读不到",而是**按缺什么给什么**:
|
|
266
|
+
|
|
267
|
+
| 状态 | 面板给出 |
|
|
268
|
+
|---|---|
|
|
269
|
+
| 找不到 `gh` | `winget install --id GitHub.cli`(可一键复制)+ cli.github.com 链接 |
|
|
270
|
+
| 装了但没登录 | `gh auth login`,并说明选 GitHub.com → HTTPS → 浏览器登录,完成后点「重新检测」 |
|
|
271
|
+
| 登录了但缺 scope | `gh auth refresh -h github.com -s repo,workflow`,缺哪个补哪个 |
|
|
272
|
+
| 没有任何仓库 | `configure.mjs add ...` 的实际命令 + 配置文件路径 |
|
|
273
|
+
|
|
274
|
+
scope 是从 `gh auth status` 真读出来的:缺 `repo` 读不到私有仓库,缺 `workflow` 无法触发构建——这两种情况在按钮按下去之前就会说明白。细粒度 token 不报 scope 行时按"未知"处理,不会误报成"全都缺"。
|
|
275
|
+
|
|
276
|
+
## 为什么复用 `gh` 而不是自带 token
|
|
277
|
+
|
|
278
|
+
本机 `gh` 已经登录、已经带好了 token scope。复用它意味着:插件**不存任何凭据**,不需要你再走一次 OAuth/PAT,而面板执行的命令就是你会手敲的那条(`gh run list` / `gh workflow run` / `gh release edit`)。反过来,任何要求你再输一次 token 的方案,都是把同一条权限链复制了第二份。
|
|
279
|
+
|
|
280
|
+
npm 这一侧是同一条原则的两个面:**包管理器**复用 Host 自己的 pnpm(`profileContext.packageManager`——插件管理器安装插件时用的就是它,第二个答案早晚会和第一个不一致),**凭据**放在 npm 自己读的 `.npmrc` 里。面板提供了一个写 token 的入口,但写的是那个文件,不是插件的任何存储:这与"面板能登录 GitHub 但插件不存 token"是同一句话。
|
|
281
|
+
|
|
282
|
+
调用一律用 `execFile` 传 argv、**不经 shell**;仓库名、workflow 名、tag 在进入 `gh` 之前都按 slug 形状校验过。子进程带 `GH_PROMPT_DISABLED=1`,否则一个想提问的 `gh` 会挂在没人持有的 stdin 上,面板只会转圈到超时。
|
|
283
|
+
|
|
284
|
+
## 安全边界
|
|
285
|
+
|
|
286
|
+
- 所有路由都是 **POST + 仅回环 + 同源**(`isTrustedRequest`),与宿主设置桥对自家回环路由的信任策略一致:只有「来自本机」且「来自这个 Host 服务的文档」的请求能过。这些路由以本机 GitHub 凭据行事,所以不能只按端口放行。
|
|
287
|
+
- 只读部分:状态、概览、运行、日志、npm 状态。**有副作用的是九条**:`dispatch`、`run-action`、`release-action`、`version-bump`、`commit`、`update`、`restart`、`npm-login`、`npm-publish`。
|
|
288
|
+
- `version-bump` 只改 `package.json` 的版本行,然后 `git commit` **只提交这一个文件**并推送当前分支。工作区不干净、分支没有上游、或落后于上游时它直接拒绝,不做任何写入。
|
|
289
|
+
- `commit` 是**唯一会提交整个工作区**的路由(`git add -A` + commit + push)。工作区干净且与上游同步时回 `409 nothing-to-commit`;`message` 为空且确实有改动时回 `400 message-required`(先拒绝,不写任何东西);分支没有上游时回 `409 no-upstream`——发布构建的是 GitHub 上的提交,推不上去就等于没提交。**提交信息为空但工作区干净**是合法用法,含义是「把已经提交的推上去」。
|
|
290
|
+
- `update` 是唯一会**改 profile 依赖**的路由:下载 Release 里的 tgz,再交给 Host 的插件管理器安装;失败时由管理器还原 `package.json` 与 lockfile。
|
|
291
|
+
- `restart` 自己不重启任何东西:它把请求转发到同一个 Host 上的 `/api/dsh-restart/restart`,由 `dsh-plugin-restart` 决定停哪个进程、用什么命令拉起来。没有那个插件就回 `501 restart-unavailable`。
|
|
292
|
+
- `npm-login` 是唯一会**写用户级配置**的路由:把 token 合并进 `~/.npmrc`(其他行原样保留,旧版本留 `.bak`)。token **绝不出现在任何响应、日志或状态里**——`npm-status` 只回答"有没有那一行",不回答"那行是什么"。
|
|
293
|
+
- `npm-publish` 是另一个**不可逆**动作:npm 不允许同一版本推两次,撤回也只在很短的时间内可行。所以它也要两次点击,且推送前逐条拒绝(`private: true`、脏工作区、版本已存在、未登录)。
|
|
294
|
+
- `公开发布草稿`、`npm-publish` 是仅有的两个不可逆动作,两者都要两次点击、中间那一步明说后果。
|
|
295
|
+
|
|
296
|
+
## HTTP 接口
|
|
297
|
+
|
|
298
|
+
面板用的就是这些,脚本也可以直接用:
|
|
299
|
+
|
|
300
|
+
| 路由 | 作用 |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `POST /api/dsh-cicd/status` | `gh` 身份与解析后的仓库列表 |
|
|
303
|
+
| `POST /api/dsh-cicd/overview` | 每个仓库的运行 + Release + 本地 git 状态(`{force:true}` 绕过缓存) |
|
|
304
|
+
| `POST /api/dsh-cicd/runs` | `{repo, limit}` 单仓库运行列表 |
|
|
305
|
+
| `POST /api/dsh-cicd/dispatch` | `{repo, workflow?, ref?, inputs?}` 触发 workflow_dispatch;触发**发布流程**时会先做发布预检(见下节) |
|
|
306
|
+
| `POST /api/dsh-cicd/run-action` | `{repo, runId, action}`,action ∈ `rerun` / `rerun-failed` / `cancel` |
|
|
307
|
+
| `POST /api/dsh-cicd/release-action` | `{repo, tag, action}`,action ∈ `publish` / `delete`(`delete` 有意不给按钮) |
|
|
308
|
+
| `POST /api/dsh-cicd/version-bump` | `{repo, release?}`,release ∈ `patch`(默认)/ `minor` / `major`:升 `package.json`、提交、推送当前分支 |
|
|
309
|
+
| `POST /api/dsh-cicd/logs` | `{repo, runId}` 失败步骤日志的尾部 |
|
|
310
|
+
| `POST /api/dsh-cicd/update` | `{repo, tag?}`:把该 Release 的 `.tgz` 下载到 `<DSH_HOME>\dsh-plugin-cicd\downloads\` 并装进当前 profile。tag 缺省 = 最新一个**非草稿**且带 tgz 的 Release |
|
|
311
|
+
| `POST /api/dsh-cicd/restart` | 无参数:转发到 `dsh-plugin-restart` 的重启路由;没挂载则 `501 restart-unavailable` |
|
|
312
|
+
| `POST /api/dsh-cicd/npm-status` | 无参数(`{force:true}` 绕缓存):`whoami` 的结果 + 每个包在源上的状态。**按需拉取,不参与 30 秒轮询**——每个仓库一次 HTTPS 加一次 `whoami` |
|
|
313
|
+
| `POST /api/dsh-cicd/npm-login` | `{token}`:把 token 合并进用户级 `.npmrc` 再用 `whoami` 验证。token 不回显 |
|
|
314
|
+
| `POST /api/dsh-cicd/npm-publish` | `{repo, otp?}`:用 Host 自己的 pnpm 发布当前版本。`otp` 是 2FA 一次性密码(6–8 位) |
|
|
315
|
+
| `POST /api/dsh-cicd/bilibili-status` | 无参数(`{force:true}` 绕缓存):凭据状态 + 每个仓库的绑定/已播报/失败 + 播报记录文件。**不碰 GitHub** |
|
|
316
|
+
| `POST /api/dsh-cicd/bilibili-login-start` | 无参数:生成 B 站网页登录二维码,返回 `{ url, key, expiresAt }` |
|
|
317
|
+
| `POST /api/dsh-cicd/bilibili-login-poll` | 无参数:问一次是否已确认。`state` ∈ `waiting` / `scanned` / `succeeded` / `expired`;成功后 Cookie 落盘 |
|
|
318
|
+
| `POST /api/dsh-cicd/bilibili-login-cancel` | 无参数:放弃这次登录 |
|
|
319
|
+
| `POST /api/dsh-cicd/bilibili-credential` | `{cookie}` 或 `{sessdata, bili_jct}`:验证通过才写入插件自己的凭据文件 |
|
|
320
|
+
| `POST /api/dsh-cicd/bilibili-logout` | 无参数:删掉插件自己写的那份凭据(不动 `bilibiliCookieFile` 指定的外部文件) |
|
|
321
|
+
| `POST /api/dsh-cicd/bilibili-bind` | `{repo, bvid, auto?}`:绑定/解绑视频;绑定会写一条基线,`bvid: ''` 即解绑(播报记录保留) |
|
|
322
|
+
| `POST /api/dsh-cicd/bilibili-announce` | `{repo, tag?, text?, force?, dryRun?}`:组装(`dryRun`)或发送这一条更新评论。`tag` 缺省 = 最新的**已公开** Release |
|
|
323
|
+
|
|
324
|
+
一律返回 `{ ok: true, value }` 或 `{ ok: false, code, message }`。
|
|
325
|
+
|
|
326
|
+
`npm-status` 的每条仓库记录:`{ repo, label, localPath, packageName, version, dirty, privatePackage, state, latest, blockers, canPublish, registryProblem, registry, pageUrl }`。`state` ∈ `unregistered` / `unpublished` / `published` / `unknown`;`blockers` 是**具名原因**(`private-package`、`dirty-tree`、`not-logged-in`、`already-published`、`registry-unreachable`、`no-checkout`、`no-version`、`no-package-name`)——"没有按钮"是最没用的一句话,`private: true` 和"没登录"在屏幕上同样是空白,修法却完全不同。
|
|
327
|
+
|
|
328
|
+
`overview` 的每一行多一个 `install` 块:`{ profile, profileDir, profileReadable, packageName, present, spec, kind, installedVersion, latestTag, latestVersion, latestAsset, state }`。`state` ∈ `not-installed` / `no-release` / `current` / `update` / `ahead` / `differs` / `checkout`——判决在 Host 上做,所以面板和 `update` 路由不会各说一套。
|
|
329
|
+
|
|
330
|
+
`status` 另外报出 `profile: { name, dir, readable }`:更新写的是哪个 profile,是**报出来**的而不是假设的——「没装」和「面板看错了 profile」是两句不同的话,只有路径能把它们分开。
|
|
331
|
+
|
|
332
|
+
## 为什么「发布」曾经必然失败,以及现在的做法
|
|
333
|
+
|
|
334
|
+
**根因**:发布用的是 `package.json` 里的版本号,而 `release.yml` 有意拒绝覆盖**属于另一个提交的同号 Release**(否则公开草稿会创建旧提交上的 tag,tag 与资产从此不一致,而且整个过程静默)。于是只要树往前走了而版本没升,点「发布」触发的运行一定在第一步失败,约 10 秒后红掉;面板却提前宣告「已产出草稿」,真正的原因还藏在两层点击之后。实测有四个失败运行属于这一类(`dsh-plugin-restart` ×3、`dsh-plugin-cicd` ×1)。
|
|
335
|
+
|
|
336
|
+
**现在**:
|
|
337
|
+
|
|
338
|
+
1. **概览行先给出判决**(`releaseCheck`)。宿主拿 `releases[].target_commitish` 与「这次发布会构建的提交」对比:只有**能证明**版本已被另一个提交占用时才判 `blocked`(脏工作区、分支名 target、读不到远端提交都算「证明不了」,一律放行)——预检拦下一个本来能成功的发布,比没有预检更糟。行上出现【版本被占用】标记,展开即写明 `v1.0.0 已属于 48ce81c,而这次发布会构建 e6a1cc1`。
|
|
339
|
+
2. **`dispatch` 会拒绝注定失败的发布**,返回 `409 version-taken`,不再浪费一次运行。
|
|
340
|
+
3. **面板给出解法**:被占用时按钮变成【升版本并发布】,确认框里写明「从 1.0.1 升到 1.0.2、提交并推送到 main」——一次点击完成 `version-bump` + 发布触发。
|
|
341
|
+
4. **文案不再替运行结果打包票**:触发后说的是「是否真的产出草稿要看这次运行的结果,失败时展开点【日志】」。
|
|
342
|
+
|
|
343
|
+
`version-bump` 的拒绝清单(都保证**一个字节都没写**):工作区有未提交改动(发布构建的是已推送的提交,这些文件会**静默缺席**于发布包)、分支没有上游、落后于上游、版本号不是 `major.minor.patch`、清单里出现第二个 `version` 键。推送失败**不回滚**:提交是真的,失败通常是 `github.com:443` 的老问题,所以如实回「本地已提交 X,推送失败」并让你重试推送。
|
|
344
|
+
|
|
345
|
+
## 设计取舍
|
|
346
|
+
|
|
347
|
+
- **UI 落在 `sidebar.panellist` + `main`**,不是浮层:侧边栏拿按钮,主区域拿页面,点开顶开内容而不是遮住它。
|
|
348
|
+
- **颜色只取 `--dsw-alias-*` 主题 token**,没有自带调色板——面板跟随亮/暗主题,而不是自带一套深色。
|
|
349
|
+
- **后台轮询失败不清空、不弹错**:合盖的笔记本不会产生一个不是用户造成的错误横幅,只是停止更新。
|
|
350
|
+
- **HTML/CSS/JS 三处都没有 `position: absolute` 的浮窗**:`main` 面板是布局里的一列。
|
|
351
|
+
|
|
352
|
+
## 本仓库自身的 CI/CD
|
|
353
|
+
|
|
354
|
+
它自己也用同一套:[`ci.yml`](.github/workflows/ci.yml) 每次 push 跑 [`tests/host-checks.mjs`](tests/host-checks.mjs)(167 条离线检查,覆盖入口校验/配置夹取/降级路径/发布预检与版本号运算/安装态与更新判决/npm 源与 token 与每条拒绝)、[`tests/mount-check.mjs`](tests/mount-check.mjs)(35 条:真挂载、真起 HTTP、真走路由,含更新、转发重启与 npm 三条拒绝路径)、[`tests/bump-e2e.mjs`](tests/bump-e2e.mjs)(29 条:真 git 仓库、真提交、真推送,以及每条拒绝都不留半截改动)与 [`tests/client-render.mjs`](tests/client-render.mjs)(71 条:用桩 React 真渲染面板,证明「发布」与【升版本并发布】确实按判决切换、被拦的原因真的到了屏幕上、一次更新真的 POST 了 `/update` 并接着问要不要重启、以及一次 npm 推送真的 POST 了 `/npm-publish` 且未登录时真的能只靠粘贴 token 完成),并真造一个发布包;[`release.yml`](.github/workflows/release.yml) 在 `v*` tag 上构建并上传 Release,**对解包后的资产**再跑一遍这几套。发布流程见 [RELEASING.md](RELEASING.md)。
|
|
355
|
+
|
|
356
|
+
`tests/host-checks.mjs` 里另有一半检查需要真实的 `gh` 与特定的仓库状态,用 `DSH_CICD_LIVE=1` 打开:
|
|
357
|
+
|
|
358
|
+
```powershell
|
|
359
|
+
node tests/host-checks.mjs # 离线,CI 跑的就是这个
|
|
360
|
+
$env:DSH_CICD_LIVE = 1; node tests/host-checks.mjs
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 推送通道被阻断时
|
|
364
|
+
|
|
365
|
+
本机实测:`github.com:443` 会被间歇阻断(`git push` 报 `Connection was reset` 或直接连不上),而 `api.github.com:443` 一直可达——所以 `git` 挂了但 `gh` 一切正常。
|
|
366
|
+
|
|
367
|
+
[`scripts/push-via-api.ps1`](scripts/push-via-api.ps1) 走 API 造出**逐字节相同 SHA** 的提交对象再移动 ref,因此本地与远程不会分叉,不需要事后 `git reset --hard`:
|
|
368
|
+
|
|
369
|
+
```powershell
|
|
370
|
+
pwsh -File scripts/push-via-api.ps1 -RepoPath 'F:\CodeProj\dsh-plugin-cicd'
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
它在每一步比对重建出的 blob / tree / commit SHA 与本地提交,任何一处不一致就中止并**不移动 ref**;成功后连同 `refs/remotes/origin/main` 一起更新,否则 `git status` 会显示一个并不存在的「领先」。仓库已经同步时它什么都不做。
|
|
374
|
+
|
|
375
|
+
## 已知边界
|
|
376
|
+
|
|
377
|
+
- 路径按 Windows 书写(`F:\...`),`ghPath` 的默认候选也是 Windows 安装位置;其它平台请显式配 `ghPath`。
|
|
378
|
+
- 私有仓库要求 `gh` 已登录且 token 有 `repo` scope。
|
|
379
|
+
- 只观察 GitHub 上的仓库;本地没有远程的仓库(例如还在本地开发的)不在范围内。
|
|
380
|
+
- **更新需要 Host 提供 `pluginManager` 服务**(DSH 自带)。没有它时 `update` 回 `501`,不会退化成自己起一个 `dsh` 子进程去写 profile。
|
|
381
|
+
- **重启需要装了 `dsh-plugin-restart`**。发布台不自己杀进程——停哪个、怎么拉起来是那个插件的契约。
|
|
382
|
+
- 更新的判定读的是 `node_modules/<name>/package.json` 的版本,不是依赖字符串。`link:` 时它等于本地检出的版本,所以"版本一样"绝不能被当成"装的是发布的那份"——那正是 `checkout` 这个状态存在的理由。
|
|
383
|
+
- **npm 推送复用 Host 自己的包管理器**:优先 `profileContext.packageManager`(DSH 自带的 pnpm),其次 `DSH_PNPM`,再其次打包应用自带的 `resources/runtime/pnpm`,最后才是 PATH 上的 `pnpm`。走到最后一步而机器上没有 pnpm 时,报错会明说 `ENOENT` 与它找的是什么。源码里 `npm-status` 只做 `whoami`,网络那一侧是 HTTPS `fetch`(**不走** `HTTP_PROXY`,本机实测直连可达)。
|
|
384
|
+
- **npm 推送没有 provenance**,而且用的是长期 token(可写 token 最长 90 天)。真需要 `--provenance`(构建来源可验证)或不想在机器上放任何 token 的包,应当走 **trusted publishing(GitHub Actions OIDC)**——那才是 CI 的正路,面板这条路是"在这台机器上推一次"。
|
|
385
|
+
- npm 在 2025-11 之后**只有 Granular Access Token**:Classic token 已全部撤销且不能再创建。可写 token 默认强制 2FA、最长 90 天。任何还写着"建一个 Classic Automation token"的文档都已经过期了。
|
|
386
|
+
- `private: true` 的包(例如 `dsh-knowledge-console`)面板**不会**提供推送按钮,这是有意的:那行字段是作者的决定。
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT,见 [LICENSE](LICENSE)。
|
package/RELEASING.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# 发布流程
|
|
2
|
+
|
|
3
|
+
本仓库的持续构建与发布由两个自包含的 workflow 承担,不依赖任何第三方 action。
|
|
4
|
+
|
|
5
|
+
| 文件 | 职责 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `.github/workflows/ci.yml` | 每次 push / PR:契约检查 + 构建一次发布包并留 artifact |
|
|
8
|
+
| `.github/workflows/release.yml` | 打 tag 或被手动触发时:构建并上传 Release 资产 |
|
|
9
|
+
|
|
10
|
+
两者都先跑 `scripts/verify-bundle.mjs`。它检查的是「装得上但什么都不加载」这类缺陷:`dsh.bundle.patch` 指向不存在的文件、`exports` 目标被改名、`files` 里的条目不存在、入口文件语法错误。这些缺陷在安装那一刻之前都是隐形的,所以门禁放在构建之前。
|
|
11
|
+
|
|
12
|
+
## 正式发布
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# 1. 改 package.json 的 version —— release 会拿 tag 与它核对
|
|
16
|
+
# 2. 提交并推送
|
|
17
|
+
git commit -am "chore: release v1.2.3"
|
|
18
|
+
git push origin main
|
|
19
|
+
|
|
20
|
+
# 3. 打 tag 并推送,这一步才触发发布
|
|
21
|
+
git tag v1.2.3
|
|
22
|
+
git push origin v1.2.3
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
tag 与 `package.json` 的 version 不一致时 workflow **直接失败**,不会发出版本漂移的包;tag 打错提交也一样会失败,而不是发一个内容不对的 Release。
|
|
26
|
+
|
|
27
|
+
## 手动触发
|
|
28
|
+
|
|
29
|
+
`Actions → Release → Run workflow`,默认勾选 draft。
|
|
30
|
+
|
|
31
|
+
- 它用 `package.json` 的 version 推出 tag `v<version>`,你不需要先在本地打 tag。
|
|
32
|
+
- **默认发成草稿**:草稿对外不可见,确认无误后在 Release 页面点 `Publish release`。
|
|
33
|
+
- 同一个 tag 再跑一次会**替换资产**(`--clobber`),不会产生第二个 Release。
|
|
34
|
+
|
|
35
|
+
## 资产内容
|
|
36
|
+
|
|
37
|
+
| 资产 | 用途 |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `<name>-<version>.tgz` | **可安装的那份**:`dsh plugin add file:<tarball>`,不用解压、不需要本地检出 |
|
|
40
|
+
| `<name>-<version>.zip` | 整棵仓库树,给人看 / 审 / 做 diff |
|
|
41
|
+
| `SHA256SUMS.txt` | 上面两个的校验和;用途是「确认你下载的没坏」,不是防篡改 |
|
|
42
|
+
|
|
43
|
+
`.tgz` 由 `npm pack` 生成,**只带 `package.json` 的 `files` 白名单**;CI 与发布流程都会解包它并跑一次契约检查,所以"白名单漏了一个被 import 的文件"会在推送时就失败,而不是等到别人装上才发现。
|
|
44
|
+
|
|
45
|
+
- 用 `git archive` 打包**该提交的完整仓库树**,所以 `node_modules/`、`.git/`、编辑器与运行期本机状态天然不在里面——不需要手写排除规则,也不可能混入本机专有文件。
|
|
46
|
+
- 解压后是 `<name>-<version>/` 目录,内容就是一个标准 DSH bundle 包(`package.json` + `cordis.patch.yml` + 入口文件)。本机 profile 用 `link:` 指向本地检出目录安装,解压出来的目录同理。
|
|
47
|
+
- `SHA256SUMS.txt` 的用途是校验**下载到的那一份没坏**。
|
|
48
|
+
|
|
49
|
+
### 关于「可复现」
|
|
50
|
+
|
|
51
|
+
内容是确定的:同一个 tag 永远打出同一棵树。但 **zip 字节不保证跨机一致**——容器里会写入打包工具的 deflate 实现信息,实测本机预演与 CI 产物的 zip 同尺寸、sha256 不同。所以不要把两台机器构建出的 sha256 拿来互相比对,也不要用它做「构建是否被篡改」的判据。
|
|
52
|
+
|
|
53
|
+
## 本地预演
|
|
54
|
+
|
|
55
|
+
CI 上跑的就是这几条命令,本地可以原样复现:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node scripts/verify-bundle.mjs
|
|
59
|
+
mkdir -p dist
|
|
60
|
+
NAME=$(node -p 'require("./package.json").name'); VERSION=$(node -p 'require("./package.json").version')
|
|
61
|
+
git archive --format=zip --prefix="$NAME-$VERSION/" -o "dist/$NAME-$VERSION.zip" HEAD
|
|
62
|
+
# 解压后再验一次:证明的是「发布出去的那份资产」完整,而不是检出目录完整
|
|
63
|
+
```
|