dsh-github-publish · 插件发布
把本地做好的 DSH 插件一键上传到 GitHub 仓库:自动初始化仓库、提交改动、按需建仓、推送,可选打标签并发布 Release。整个操作在 DSH 的图形界面里完成,不需要记 git 命令,也不需要 gh CLI。
- 界面位置:设置 → 插件发布
- 宿主半区(Node):本地 git 操作 + GitHub REST 调用
- 浏览器半区(Web):设置分区 UI,只通过本机 HTTP 与宿主通信,永远看不到令牌明文
目录
一分钟上手
- 安装本插件并重启 DSH(见下一节)。
- 打开 设置 → 插件发布。
- 在「GitHub 凭据」里粘贴一个 Personal Access Token,点「保存并验证」。令牌会被校验(读一次
/user)后存到本机。 - 在「本地插件」里找到你的插件(默认自动发现 profile 中
link:安装的插件与node_modules下的软链插件),点 一键上传。 - 右侧/下方出现任务面板:准备 → 初始化仓库 → 提交 → 确认仓库 → 设置远端 → 推送 → 校验。完成后给出仓库链接。
没有令牌也能先点 预检:它只做只读检查(目录、git 状态、仓库是否存在),不写盘、不建仓、不推送。
安装
# 默认装官方桌面端 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 只在启动时装载。
卸载:
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 凭据
解析顺序(第一个命中者生效):
- 插件设置里保存的令牌 ——
<DSH_HOME>\dsh-github-publish\token(0600) - 环境变量
GITHUB_TOKEN - 环境变量
GH_TOKEN - 系统凭据管理器:
git credential fill(Git Credential Manager 已登录过 GitHub 时可直接复用;用credential.interactive=false+GCM_INTERACTIVE=never调用,任何情况下都不会弹出登录窗口;本机没有存凭据时会立刻返回“无令牌”,可用设置项关闭这条回退) - 都没有 —— 只能对已存在的仓库尝试推送,建仓一定会失败并提示配置令牌
需要什么权限
- 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/*)
- 只接受回环地址(
127.0.0.1/::1)连接,其余 403; - 必须带
x-dsh-github-publish: 1自定义头 —— 跨站表单/图片请求无法伪造,跨源fetch会先触发预检,而本路由不返回任何 CORS 头; - 带
Origin时必须与Host同源; - 只接受
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 服务 + 本地裸仓库,并断言令牌不落盘):
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 渲染整个面板并点击按钮):
# 一次性装上测试期依赖(不进插件包)
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 与推送之间仍有极小窗口,中间人推送会被拒绝而不是被覆盖(这是设计取向)。
还没有评论,来写第一条。