Bundle
dsh-github-publish
DSH 插件发布器:把本地做好的插件一键推送到 GitHub 仓库(凭据 → 建仓 → 提交 → 推送 → 可选 Release)
- Source
- Charlestam218
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# dsh-github-publish · 插件发布
把本地做好的 DSH 插件**一键上传到 GitHub 仓库**:自动初始化仓库、提交改动、按需建仓、推送,可选打标签并发布 Release。整个操作在 DSH 的图形界面里完成,不需要记 git 命令,也不需要 `gh` CLI。
- 界面位置:**设置 → 插件发布**
- 宿主半区(Node):本地 git 操作 + GitHub REST 调用
- 浏览器半区(Web):设置分区 UI,只通过本机 HTTP 与宿主通信,永远看不到令牌明文
---
## 目录
- [一分钟上手](#一分钟上手)
- [安装](#安装)
- [界面说明](#界面说明)
- [一键上传做了什么](#一键上传做了什么)
- [GitHub 凭据](#github-凭据)
- [安全设计](#安全设计)
- [设置项](#设置项)
- [目录结构](#目录结构)
- [自检](#自检)
- [故障排查](#故障排查)
- [已知边界](#已知边界)
---
## 一分钟上手
1. 安装本插件并重启 DSH(见下一节)。
2. 打开 **设置 → 插件发布**。
3. 在「GitHub 凭据」里粘贴一个 Personal Access Token,点「保存并验证」。令牌会被校验(读一次 `/user`)后存到本机。
4. 在「本地插件」里找到你的插件(默认自动发现 profile 中 `link:` 安装的插件与 `node_modules` 下的软链插件),点 **一键上传**。
5. 右侧/下方出现任务面板:准备 → 初始化仓库 → 提交 → 确认仓库 → 设置远端 → 推送 → 校验。完成后给出仓库链接。
没有令牌也能先点 **预检**:它只做只读检查(目录、git 状态、仓库是否存在),不写盘、不建仓、不推送。
---
## 安装
```powershell
# 默认装官方桌面端 profile(%USERPROFILE%\.dsh\profiles\desktop)
pwsh -File install.ps1
# 指定 profile
pwsh -File install.ps1 -ProfileDir "D:\some\profile"
# 不用软链,直接拷贝一份(改了源码要重装才生效)
pwsh -File install.ps1 -Copy
```
脚本做两件事:在 `<profile>\node_modules\dsh-github-publish` 建一个指向本包目录的 junction,并把包名写进 `<profile>\package.json` 的 `dependencies` 与 `dsh.profile.bundles`。
> 如果系统禁止运行未签名脚本(`about_Execution_Policies` 报错),在命令前加一段进程级放行:
> `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force`(只影响当前窗口,不改机器策略)。
**装完必须重启 DSH 客户端**:profile 未启用 HMR 时,新 bundle 只在启动时装载。
卸载:
```powershell
pwsh -File uninstall.ps1 # 保留设置与令牌
pwsh -File uninstall.ps1 -PurgeData # 连设置与令牌一起删
```
---
## 界面说明
| 区块 | 作用 |
|---|---|
| **GitHub 凭据** | 显示令牌是否就绪、来源(插件设置 / 环境变量 / 系统凭据管理器)、掩码与账号;可保存、验证、清除。 |
| **目标仓库** | 账号或组织、可见性(公开/私有)、仓库名模板、是否自动建仓、是否写 topics、是否打标签发 Release、是否允许强制覆盖、是否允许回退系统凭据。 |
| **发现范围** | 额外扫描根目录(每个目录向下扫描两层找 `package.json`)与手动添加的插件目录。 |
| **本地插件** | 每个插件一行:包名/版本、真实路径、git 事实(分支、提交数、是否干净)、GitHub 侧状态(仓库是否存在、是否同步),以及 **预检** / **一键上传** 按钮。 |
| **当前任务** | 步骤进度、实时日志(同时落盘到 `<DSH_HOME>\dsh-github-publish\logs\`)、结果链接与取消按钮;推送被拒绝时出现 **强制覆盖并重试**。 |
---
## 一键上传做了什么
| 步骤 | 内容 | 失败时的行为 |
|---|---|---|
| prepare | 检查 git 可用、目录可读、`package.json` 合法;位于更大仓库内部时默认拒绝 | 给出原因与处理建议 |
| git-init | 目录不是仓库时 `git init -b main`;补一份最小 `.gitignore`(`node_modules/` 等) | 原样回报 git 输出 |
| commit | `git add -A`,有改动就提交(提交信息模板可配),身份取 `git config`,缺失时用 GitHub 账号的无回复邮箱 | 提示补 `user.name/user.email` |
| github-repo | `GET /repos/...` 判断仓库是否存在(插件已有指向 GitHub 的 `origin` 时默认沿用该仓库),不存在且开启自动建仓则创建(用户走 `/user/repos`,组织走 `/orgs/{org}/repos`);按需写 topics | 令牌/权限问题给出对应提示 |
| remote | 把 `origin` 指向 GitHub 返回的 `clone_url`(企业版同样正确) | — |
| push | 用一次性 HTTP 头携带令牌推送当前分支(`--` 终止选项解析,禁用仓库自带钩子),并建立 upstream | 被拒绝时提示开启强制覆盖;强制推送用**远端实时 sha** 作 `--force-with-lease` 预期值;鉴权失败时提示令牌权限 |
| release | 可选:`v<版本>` 标签 + GitHub Release,已存在则跳过 | 跳过并记录原因,不影响整体成功 |
| verify | 读远端分支头提交与本地 HEAD 比对 | **不一致 → 任务判失败**(提示分支保护 / pushurl 被改写等可能原因);没令牌或分支还不存在 → 步骤标记为「跳过」并给出提示 |
任何一个步骤失败都会:把该步骤标红、把错误与建议写进任务面板与日志文件,**且不回滚已完成的本地工作**(重试即可继续)。
---
## GitHub 凭据
解析顺序(第一个命中者生效):
1. 插件设置里保存的令牌 —— `<DSH_HOME>\dsh-github-publish\token`(0600)
2. 环境变量 `GITHUB_TOKEN`
3. 环境变量 `GH_TOKEN`
4. 系统凭据管理器:`git credential fill`(Git Credential Manager 已登录过 GitHub 时可直接复用;用 `credential.interactive=false` + `GCM_INTERACTIVE=never` 调用,**任何情况下都不会弹出登录窗口**;本机没有存凭据时会立刻返回“无令牌”,可用设置项关闭这条回退)
5. 都没有 —— 只能对**已存在**的仓库尝试推送,建仓一定会失败并提示配置令牌
**需要什么权限**
- classic token:勾选 `repo`(要发 Release / 写 topics 也在这个范围内)
- fine-grained token:`Contents: Read and write`;要自动建仓还需 `Administration: Read and write`,并授权目标账号/组织
令牌只用于两处:GitHub REST 调用(`Authorization: Bearer …`)与 `git push`(`http.extraheader` 一次性注入)。
---
## 安全设计
**令牌**
- 只在宿主进程内存里流转,接口与界面只返回掩码(`ghp_…abcd`)与来源;
- 推送时用环境变量 `GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0` 注入 HTTP 头,**不进 argv、不写 `.git/config`**,进程退出即消失;
- 带令牌的 git 调用(push / ls-remote / tags)与**发布期间的每一次 git 调用**都指向一个空 hooks 目录(`<DSH_HOME>/dsh-github-publish/no-hooks`):仓库自带的 `pre-push`/`pre-commit`、仓库级 `core.hooksPath` 都不会执行(命令行 `-c` 优先级最高,实测压得住仓库本地配置);
- git 子进程的环境会被**剃掉机密**:删除 `GITHUB_TOKEN`/`GH_TOKEN` 等常见变量名,并删除任何取值等于已登记令牌的变量——仓库里的 `filter.*.clean`、`core.fsmonitor` 这类"必然要执行"的程序即使跑了,也拿不到令牌;
- 所有写进日志/错误信息的文本都过 `redact()`:既抹当前令牌(含其 base64 形式),也按模式抹掉 **任何** URL userinfo、`ghp_/github_pat_` 形态、`Authorization` 头与 `password=` 行——origin 里残留的**别的**令牌同样不会被打印;
- 令牌单独存放(`token` 文件),删文件即撤销,不需要动设置;
- 自检里有专门断言:`.git/config`、`.git` 目录全量文件、发布日志、任务响应里都不允许出现令牌明文。
**参数与远端安全**
- `branch` / `owner` / `repo` 全部走白名单校验(`lib/validate.js`):非法直接 400。git 会解析位置参数之后的选项,不做这层校验时 `branch: "--mirror"` 会变成真正的 `--mirror`(删远端引用);
- 纵深防御:所有 refspec 都放在 `--` 之后,`git push origin -- <branch>`、`git fetch/ls-remote ... -- <branch>`;
- 推送前清掉仓库里能"把带令牌的请求引到别处"的配置:`remote.origin.pushurl` / `receivepack` / `uploadpack` / `proxy`、`http.proxy` / `https.proxy` / `http.sslVerify`、`core.gitProxy`、`url.*.insteadOf` / `pushInsteadOf`(`git remote set-url` 只改 `url`,不会动 `pushurl`);清掉哪些键会写进任务告警,**清理失败直接中止推送**;同样的中和也内建在 `lib/git.js` 的 `push()` 里,直接调用内层 API 同样受保护;
- 强制推送的预期值来自 `git ls-remote` 的**实时** sha,并**按 `refs/heads/<branch>` 精确比对**(ls-remote 的 pattern 是后缀匹配,远端同时有 `a/b` 与 `b` 时取错行会让 lease 失真);远端没有该分支时用「期望不存在」;换 origin 时会清掉旧的 `refs/remotes/origin/*`;
- GitHub API 只跟随**同源**重定向;跨主机或降级到 `http://` 的重定向直接报错,不把 Bearer 令牌交出去。
**发布行为**
- 位于更大仓库内部的插件目录**默认拒绝发布**(否则 `git add -A` 会把整个仓库推上去),需要显式勾选「只发布这个子目录(在插件目录内单独 git init)」;
- 默认**不做**强制推送;开启后只覆盖「读取到的那个远端快照」(`--force-with-lease=<branch>:<sha>`);
- **不删除**远端任何内容,不做 `push --delete`,不 `remote prune`;
- `git add -A` 之后扫描将要提交的文件名,命中 `.env`、`*.pem`、`id_rsa*`、`credentials*` 等形态时在界面与日志里**告警**(默认可见性是 public,误推代价很大);
- 默认 `.gitignore` 只忽略 `node_modules/`、`*.log` 与系统垃圾文件;`dist/`、`build/` **不再**被静默排除——发布的是成品,是否包含由你决定;
- 远端默认分支与本次推送分支不一致时给出提示(否则 GitHub 首页看起来"没更新");
- 每次发布的完整日志落在 `<DSH_HOME>\dsh-github-publish\logs\publish-*.log`。
> Windows 说明:`token`/日志目录按 POSIX 权限位创建(0600 / 0700),但 **Windows 上 Node 的 mode 不生效**,实际保护来自用户目录 ACL。需要更强的保护请使用系统凭据管理器(本插件支持回退)或自行给目录收紧 ACL。
**HTTP 接口**(`/dsh-github-publish/api/*`)
1. 只接受回环地址(`127.0.0.1`/`::1`)连接,其余 403;
2. 必须带 `x-dsh-github-publish: 1` 自定义头 —— 跨站表单/图片请求无法伪造,跨源 `fetch` 会先触发预检,而本路由不返回任何 CORS 头;
3. 带 `Origin` 时必须与 `Host` 同源;
4. 只接受 `application/json`,请求体上限 256 KB(超出回 413),响应一律 `no-store`。
---
## 设置项
保存在 `<DSH_HOME>\dsh-github-publish\settings.json`。
| 键 | 默认 | 说明 |
|---|---|---|
| `owner` | `""` | 空 = 用令牌所属账号;组织名直接填组织 |
| `visibility` | `public` | `public` / `private` |
| `repoTemplate` | `{name}` | 支持 `{name}`(去 scope 包名)、`{scope}`、`{full}`、`{version}` |
| `createRepo` | `true` | 仓库不存在时自动创建 |
| `reuseExistingRemote` | `true` | 插件已有指向 GitHub 的 `origin` 时,沿用该仓库而不是另建新仓(账号/仓库名被显式指定时以指定值为准) |
| `setTopics` | `true` | 用 `package.json` 的 keywords + `dsh-plugin`/`deepseek-harness` 写 topics |
| `release` | `false` | 打 `v<version>` 标签并发布 Release |
| `allowForce` | `false` | 允许 `--force-with-lease` |
| `useCredentialHelper` | `true` | 无令牌时回退到系统凭据管理器 |
| `commitMessage` | `chore(release): {name} v{version}` | 提交信息模板 |
| `extraRoots` | `[]` | 额外扫描根目录(向下两层) |
| `manualDirs` | `[]` | 手动添加的插件目录 |
| `includeInstalled` | `true` | 是否扫描 profile 的 `node_modules` 软链插件 |
| `apiBase` | `https://api.github.com` | GitHub REST 基址(企业版改这里) |
| `remoteBase` | `https://github.com` | 展示用基址 |
环境变量覆盖(不会写进磁盘,适合企业部署与自检):
```
DSH_GITHUB_PUBLISH_API_BASE=http://127.0.0.1:8080 # GitHub API 基址
DSH_GITHUB_PUBLISH_REMOTE_BASE=https://ghe.corp # 远端基址
DSH_GITHUB_PUBLISH_OWNER=some-org # 默认 owner
DSH_GITHUB_PUBLISH_NO_CREDENTIAL_HELPER=1 # 禁止回退系统凭据管理器
```
---
## 目录结构
```
dsh-github-publish/
package.json dsh.bundle.patch + dsh.client 声明
cordis.patch.yml 插入 profile 配置树的挂载声明
lib/
index.js 宿主半区入口:HTTP 路由、请求防护、状态与发现接口
publish.js 一键发布流水线(job/步骤/日志/失败提示)
discover.js 插件发现(link: 依赖、node_modules 软链、扫描根、手动目录)
git.js git 调用封装(非交互、有界输出、一次性鉴权头)
github.js GitHub REST 客户端(零依赖 node:https)
credentials.js 令牌解析(设置 → 环境变量 → git credential)与脱敏
validate.js 分支/账号/仓库名白名单校验 + 机密文件名识别
settings.js 路径推导、设置读写、令牌文件(0600)
client.js 浏览器半区成品 bundle(设置分区 UI,手写无编译)
test/
harness.mjs 宿主半区端到端自检(假 GitHub + 本地裸仓库)
client-smoke.mjs 浏览器半区冒烟测试(真 React + jsdom)
install.ps1 / uninstall.ps1
```
依赖:只用 Node 内置模块(`node:http(s)`、`node:child_process`、`node:fs`…),**零运行时依赖**;浏览器半区只 `require('react')`(DSH 的 `PLATFORM_MODULES` 提供),无需构建步骤。
---
## 自检
**宿主半区端到端**(不需要真实 GitHub,会起一个假 REST 服务 + 本地裸仓库,并断言令牌不落盘):
```powershell
node test\harness.mjs # 130 项断言
node test\harness.mjs --keep # 保留临时目录便于排查
```
覆盖:环境状态、请求防护(缺头/错误 Content-Type/超大请求体)、无令牌失败路径、令牌保存/校验/清除、插件发现、预检不写盘、首次发布全链路(建仓+提交+推送+校验)、幂等二次发布、沿用既有 origin、远端分叉(拒绝→强制覆盖)、陈旧跟踪引用下的强制推送、嵌套仓库防护、设置持久化、并发 409 与取消、参数注入防护(`--mirror`/`--dry-run`/非法 owner 与仓库名)、远端校验不一致判失败、机密文件告警、钩子隔离(A/B 对照)、`pushurl`/`insteadOf`/`http.proxy` 重定向防护、`a/b` 与 `b` 同名后缀下的 lease 精确匹配、仓库级 `filter.clean` 环境脱敏、内层 `push()` 防护、脱敏与校验函数单测。
**浏览器半区冒烟**(真 React 18 + jsdom 渲染整个面板并点击按钮):
```powershell
# 一次性装上测试期依赖(不进插件包)
npm i --prefix ..\..\.testdeps react@18.3.1 react-dom@18.3.1 jsdom@24.1.3
node test\client-smoke.mjs # 44 项断言
```
> npm 默认缓存目录不可写时(`EPERM: ...\_cacache`),加 `--cache ..\..\.npm-cache` 指定一个工作区内的缓存目录。
---
## 故障排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 设置里没有「插件发布」 | 未安装 / 未重启 / 客户端 bundle 未送达 | 确认 `dsh.profile.bundles` 含本包,重启 DSH;看客户端控制台有没有 `client-modules: client bundle not found` |
| 面板提示「读不到宿主接口」 | 宿主半区未加载(fiber 失败) | 看 DSH 日志里 `dsh-github-publish` 的加载行;确认 `lib/index.js` 存在 |
| 保存令牌返回 401 | 令牌无效或过期 | 重新生成(classic 勾 `repo`;fine-grained 给 Contents 读写) |
| 建仓返回 403 / 422 | 权限不足 / 仓库名被占用 | fine-grained 需要 Administration 读写;换个仓库名或先手动建仓 |
| 推送被拒绝(non-fast-forward) | 远端有本地没有的提交 | 先 `git pull --rebase`,或开启「允许强制覆盖」后点 **强制覆盖并重试** |
| 提示「强制推送被拒绝:预期值与远端不一致」 | 读取远端之后远端又变了(stale info) | 再点一次「强制覆盖并重试」即可(会重新读取远端 sha) |
| 提示「远端校验不一致」 | 分支保护改写了提交 / `remote.origin.pushurl` 指向别的仓库 / 远端被同时推送 | 打开仓库确认实际内容;这类情况插件按失败处理,不会假装成功 |
| 提示「疑似机密文件将被提交」 | 目录里有 `.env`、私钥等 | 确认要发布就忽略;不想发就加进 `.gitignore` 再重试 |
| 推送提示 `could not read Username` | 没令牌且系统凭据不可用 | 保存令牌,或关闭 `DSH_GITHUB_PUBLISH_NO_CREDENTIAL_HELPER` 限制 |
| 拒绝发布「位于更大的仓库内」 | 插件目录在某个仓库的子目录里 | 勾选「只发布这个子目录」,或把插件移出该仓库 |
| 提交身份报错 | 本机没配 `git user.name/user.email` | 用 GitHub 令牌时插件会自动兜底;否则 `git config --global user.name/user.email` |
| 发布成功但远端没有文件 | 目录被 `.gitignore` 全部忽略 | 看任务日志里的「已暂存 N 处改动」与 `.gitignore` |
---
## 已知边界
- **一个插件一个仓库**:`<owner>/<repo>` 由模板决定,不做 monorepo 子目录推送(需要的场景可以用「扫描根目录 + 手动目录」分别发布)。
- **不发 Release 资产**:只创建标签与 Release 记录,不上传构建产物。
- **不建组织**:组织必须已存在且令牌有建仓权限。
- **不做 submodule / subtree / LFS 特殊处理**:按普通 Git 仓库推送;`.gitattributes` 里的 LFS 规则由本机 git-lfs 处理。
- **只服务本机回环**:接口有意不接受非回环请求,即使 DSH 的 web server 绑定了 `0.0.0.0`。
- **不做自动重试与后台队列**:一次点击一个任务(同一目录同时只允许一个任务,路径按 realpath 归一化比较,重复点击返回 409),失败后由人决定是否重试(可强制覆盖或取消)。
- **接口只防浏览器不防本机进程**:回环 + 自定义头挡得住跨站请求,但本机任何进程都能调用这套 API(它本来就是本机工具);因此参数校验与 `--` 隔离是硬要求,而不是可选项。
- **`--force-with-lease` 只保护「读到的那一刻」**:读取远端 sha 与推送之间仍有极小窗口,中间人推送会被拒绝而不是被覆盖(这是设计取向)。
Install
dsh plugin --profile web add github:Charlestam218/dsh-github-publish
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-github-publish from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.