"帮我把这个项目上传到 GitHub"听起来只有三步:

git add
git commit
git push

但只要目标从"临时备份"变成"正式开源",真正要处理的事情会迅速变多:

  • 项目里是否混入 .env、Token、私钥或带凭据的 URL?
  • 缓存、日志、虚拟环境、构建产物和大文件哪些应该进入 Git?
  • 本地 origin 是否真的指向用户给出的仓库?
  • 远端已经有提交时,本地历史能不能普通 fast-forward?
  • 版本号从哪里来,Tag 是否已经存在?
  • Release Notes、源码 ZIP、附加资产和 SHA-256 校验和如何一起生成?
  • git push 或 gh release create 返回 0 以后,怎样证明 GitHub 上的分支、Tag 和资产就是这次结果?

我之前已经做过一个 github-safe-publish,它解决的是"审计本地项目,然后安全提交、普通推送并校验远端分支"。这次我没有继续把所有功能塞进旧入口,而是新建了一个覆盖完整项目发布流程的 Skill:

github-project-publisher

开源仓库:

https://github.com/wangzifan396-wzf/skills

本文对应提交:

https://github.com/wangzifan396-wzf/skills/commit/7a90e7eede0c5029c131d7496ee479ae8353f539

项目使用 MIT License。这个新 Skill 的目标是:让用户只面对一个入口,内部再由模块化脚本完成审计、Git 操作、版本准备、Release 创建和独立验证。

它可以自动完成很多动作,但不会把"自动"理解成"默认拥有所有写权限"。默认命令始终是 dry-run;提交和推送需要显式 --execute;创建 GitHub Release 则必须同时出现 --release --execute。

先定义"发布完成"到底是什么

如果把完成条件写成"git push 没报错",这套工具其实不需要做成 Skill。

我给 github-project-publisher 设定的完成条件是:

  1. 本地发布边界已经审计,且不存在敏感信息或 GitHub 100 MiB 单文件阻断;
  2. 目标仓库与用户确认的 OWNER/REPOSITORY 完全一致;
  3. 非空远端必须属于同一段历史,并能通过普通 fast-forward 更新;
  4. 暂存内容通过 git diff --cached --check;
  5. 分支推送后,远端精确 ref 与本地最终 HEAD 一致;
  6. 创建 Release 时,版本 Tag、源码 ZIP、Release Notes、显式资产和 SHA256SUMS.txt 都来自最终提交;
  7. GitHub Release 创建后,再次查询 Tag、Release 和资产名称,而不是只相信一次 CLI 退出码;
  8. 全过程生成机器可读 receipt,保留候选文件、计划动作、最终提交和验证结果。

这组条件决定了 Skill 的结构:AI 负责理解用户目标、确认授权和解释异常,Python 脚本负责可重复、低自由度、容易验证的动作。

一个入口,内部仍然模块化

图 1:用户只调用一个 Skill,内部依次完成本地审计、远端检查、dry-run、普通推送、版本产物准备和独立验证。完成条件是远端精确结果匹配,不是命令"看起来成功"。

我最终选择的是"一个用户面对的综合 Skill,内部脚本模块化",而不是把审计、Tag、归档和 Release 拆成四五个需要用户手工串联的小 Skill。

原因很直接:这些步骤共享同一批关键事实。

  • 它们必须指向同一个 Git 根目录;
  • 必须使用同一个最终 HEAD;
  • 必须使用同一个目标仓库和分支;
  • 版本 Tag、源码归档和 Release Notes 不能来自不同时间点;
  • 校验必须针对本次真正推送后的结果。

如果拆成多个独立用户入口,调用者就要负责在不同 Skill 之间传递提交号、版本号、资产目录和授权状态。任何一次遗漏,都可能让"上传成功"和"Release 正确"变成两回事。

所以对外只保留:

github-project-publisher

内部则分为三个文件:

github-project-publisher/
├── SKILL.md
├── agents/
│   └── openai.yaml
├── references/
│   ├── github-cli.md
│   ├── release-schema.md
│   └── safety-policy.md
└── scripts/
    ├── publisher_lib.py
    ├── publish_project.py
    └── verify_publish.py

publisher_lib.py 处理审计、Git、远端解析、归档、校验和与验证等确定性逻辑;publish_project.py 负责编排 dry-run 和执行模式;verify_publish.py 刻意保持独立,用第二次查询验证发布结果。

详细授权规则、receipt 字段和 GitHub CLI 前置条件放进 references/,按需读取,避免每次调用都把所有细节塞进上下文。

完全自动化不等于默认拥有写权限

图 2:默认只读预览;--execute 开启提交和普通推送;--release --execute 才开启版本 Release。敏感信息、大文件、历史冲突、已有 Tag、远端冲突或验证不一致都会立即停止。

这类工具最容易出现的误解是:既然目标是"全自动上传",为什么还要 dry-run 和多个显式参数?

因为"自动执行流程"和"自动扩大权限"是两件不同的事。

默认模式:只生成完整计划

不加 --execute 时,发布器会读取项目和远端,输出:

  • Git 根目录、当前分支和 HEAD;
  • 工作树状态、现有 remotes;
  • 候选文件数、候选体积和具体路径;
  • Secret、大文件、README、LICENSE 和 .gitignore 检查;
  • 目标分支是否为空、是否属于同一历史;
  • 将要执行的 Git 操作;
  • 版本来源、Tag、Release 资产和产物目录;
  • blocker、warning 与最终状态。

但它不会暂存、提交、增加远端、推送或创建 Tag。

--execute:只开放分支发布

显式增加 --execute 后,Skill 才允许:

  1. 在没有同名远端时增加目标远端;
  2. 对非空目标执行 fetch 和祖先关系检查;
  3. 暂存审计通过的项目边界;
  4. 执行 git diff --cached --check;
  5. 有变更时创建普通提交;
  6. 使用普通 git push --set-upstream 推送;
  7. 重新查询远端分支 SHA。

这里没有任何 force 参数。如果非空远端不是本地 HEAD 的祖先,流程会停止并解释,而不是自动合并无关历史或覆盖目标仓库。

--release --execute:再开放版本发布

Release 是另一层外部写入,所以还要显式增加 --release。

只有两个开关同时存在,且版本、Tag、GitHub CLI 登录状态、资产和远端历史都通过检查后,才会创建 Tag 与 GitHub Release。

这种设计多敲了两个参数,但也让 receipt 可以明确回答:这次到底只是计划、发布了分支,还是创建了一个公开版本。

本地审计具体检查什么

发布器不是通用杀毒软件,也不代替 GitHub Secret Scanning,但它会阻断最常见的开源事故。

敏感信息

检查范围包括:

  • .env、私钥、keystore 和凭据类文件;
  • GitHub、AWS、Slack、OpenAI 等常见 Token 形态;
  • password、client_secret、api_key、access_token 等疑似非占位赋值;
  • 把用户名、密码或 Token 嵌进 HTTPS 远端 URL 的写法。

报告只记录规则、路径和行号,不复制匹配到的秘密值。否则"安全扫描报告"本身就可能变成新的泄露文件。

文件大小与发布边界

普通 GitHub 仓库的单文件硬限制是 100 MiB,因此超过这一阈值直接阻断。较大的文件也应该被显式复核:它可能是应该忽略的生成物,也可能更适合作为 Release 附件或 Git LFS 内容。

Skill 不会看到 dist/、build/、图片或视频就一律删除。不同项目的交付边界不同,脚本负责把差异暴露出来,维护者负责决定它们究竟是源码、发布产物还是本地缓存。

README、LICENSE 与 Git 状态

README、LICENSE 和 .gitignore 会进入 readiness 检查,但 Skill 不会擅自替维护者选择许可证,也不会虚构运行命令、测试结果和兼容性。

如果项目已经有暂存内容,发布器会把它纳入审计,而不是先清空索引。用户已有的工作树变化也不会被回滚。

远端为什么不能"地址对了就直接推"

一个 GitHub URL 可能合法,但仍然不是应该推送的目标。

发布器要求 URL 与下面这个显式确认完全一致:

--confirm-repository OWNER/REPOSITORY

如果本地已经有同名远端,但 URL 指向别处,Skill 会拒绝静默执行 git remote set-url。维护者可以检查是不是选错目录、选错仓库,或者在明确情况下改用新的 remote name。

对非空远端,默认同样会停止。只有显式 --allow-existing,并且远端目标分支已经是本地 HEAD 的祖先,才进入普通 fast-forward 更新。

对应的判断可以写成:

empty remote
  -> allow first publication

non-empty remote + no --allow-existing
  -> stop

non-empty remote + --allow-existing + remote branch is ancestor of local HEAD
  -> allow ordinary fast-forward push

non-empty remote + unrelated or diverged history
  -> stop

这里有意不自动执行 --allow-unrelated-histories,也不 force push。自动化工具无法只凭 URL 判断远端历史是否应该被本地项目覆盖。

Release 不是再执行一次 push

图 3:版本号、最终 HEAD、Git 历史和显式资产进入同一条打包链,输出 Tag、Release Notes、源码 ZIP、资产副本、SHA256SUMS.txt 和机器可读 receipt。

分支发布解决"GitHub 上有最新代码",Release 解决的是"这个版本有哪些稳定、可下载、可校验的产物"。

新 Skill 支持从以下位置获取版本:

  • 命令行 --version 1.2.3;
  • package.json;
  • pyproject.toml;
  • Cargo.toml。

版本会统一规范化成:

v1.2.3

如果本地或远端已经存在同名 Tag,流程直接停止,不覆盖旧版本。

Release Notes 从 Git 历史生成。第一次发布使用当前历史;后续版本可以从最近的版本 Tag 开始整理提交。这里不是让 AI 随意写一篇宣传文案,而是让版本说明保持可追溯。

源码包使用最终提交执行 git archive。这意味着 ZIP 只包含 Git 中的正式内容,不会把工作区缓存、未跟踪文件或 Release 输出目录再次打包进去。

额外资产必须通过 --asset 显式指定。例如:

--asset dist/project-windows.zip
--asset dist/project-linux.tar.gz

Skill 会把源码 ZIP 与这些资产复制到项目外的 Release 产物目录,并为每个上传文件生成 SHA-256:

SHA256SUMS.txt

最后通过已经认证的 GitHub CLI 创建 Release。Skill 不读取 Token,也不要求用户把 Token 粘贴到聊天中;它只使用系统现有的 gh 登录状态。

三种实际用法

1. 先做 dry-run

Windows PowerShell:

python github-project-publisher\scripts\publish_project.py `
  --project D:\path\to\project `
  --remote https://github.com/OWNER/REPOSITORY `
  --confirm-repository OWNER/REPOSITORY `
  --branch main `
  --commit-message "feat: publish project" `
  --receipt D:\path\to\dry-run.json

这条命令不修改 Git 状态。先检查 receipt 中的候选文件、远端、blocker、warning 和 operations。

2. 发布或更新分支

确认计划后,在同一条命令增加:

--execute

如果远端已经属于同一项目历史,再显式增加:

--allow-existing

完整示例:

python github-project-publisher\scripts\publish_project.py `
  --project D:\path\to\project `
  --remote https://github.com/OWNER/REPOSITORY `
  --confirm-repository OWNER/REPOSITORY `
  --allow-existing `
  --branch main `
  --commit-message "feat: publish project" `
  --execute `
  --receipt D:\path\to\publish-receipt.json

3. 创建版本 Release

先确认:

gh auth status

然后执行:

python github-project-publisher\scripts\publish_project.py `
  --project D:\path\to\project `
  --remote https://github.com/OWNER/REPOSITORY `
  --confirm-repository OWNER/REPOSITORY `
  --allow-existing `
  --version 1.2.3 `
  --release `
  --asset dist\project-windows.zip `
  --execute `
  --receipt D:\path\to\release-receipt.json

如果不需要额外二进制资产,可以省略 --asset,仍然会生成源码 ZIP、Release Notes 与 SHA256SUMS.txt。

我怎样验证它真的完成了发布

图 4:仓库 24/24 测试通过,真实发布审计得到 58 个候选文件、0 blocker、0 warning;独立校验再次读取 GitHub main,本地和远端均指向 7a90e7e...。

验证分成三层。

第一层:标准库单元测试

仓库执行:

python -m unittest discover -s tests -v

结果是 24/24 通过,其中 4 个测试专门覆盖 github-project-publisher:

  • GitHub 仓库 slug 与版本号规范化;
  • Secret 和大文件阻断只报告规则与路径,不泄露原值;
  • Release Notes 与源码归档可复现;
  • 针对本地 bare remote 执行真实 commit、普通 push 和远端 SHA 验证。

本地 bare remote 很重要,因为它让执行路径不依赖临时公开仓库,也能测试真正的 Git 写入、fetch、push 和独立验证,而不是只 mock 子进程返回值。

官方 skill-creator 校验结果:

Skill is valid!

第二层:用真实 GitHub 仓库完成自发布

这次新 Skill 正式开源时,我先执行 dry-run,再让它把自己提交并推送到:

https://github.com/wangzifan396-wzf/skills

真实执行 receipt 中的结果:

项目 结果
候选文件 58
候选总大小 287,679 B
blocker 0
warning 0
最终分支 main
最终提交 7a90e7eede0c5029c131d7496ee479ae8353f539
分支校验 true

对应提交包含 10 个变更文件,新增 1,102 行、删除 3 行。

第三层:脱离发布器再次验证

发布完成后,单独运行:

python github-project-publisher\scripts\verify_publish.py `
  --project D:\path\to\project `
  --remote https://github.com/wangzifan396-wzf/skills `
  --branch main `
  --json-out D:\path\to\independent-verification.json

得到:

{
  "localHead": "7a90e7eede0c5029c131d7496ee479ae8353f539",
  "remoteBranch": "7a90e7eede0c5029c131d7496ee479ae8353f539",
  "branchMatches": true,
  "verified": true
}

这次真实公开验证执行的是 branch-only 发布

Release 功能已经实现,并通过本地 Git 仓库闭环测试覆盖版本、Notes、源码归档和校验和逻辑,但我没有为了文章测试额外创建一个没有实际版本意义的公开 GitHub Release。这一点必须明确,否则"功能已实现"很容易被误写成"已经在公开仓库创建 Release"。

为什么新建 Skill,而不是直接改旧项目

图 5:旧 Skill 保留"安全上传"的窄语义;新 Skill 继承审计与普通推送,再增加版本、Tag、归档、校验和与 GitHub Release。

github-safe-publish 并没有因为新 Skill 出现就失去价值。

如果用户只需要:

  • 审计本地项目;
  • 准备 .gitignore;
  • 首次上传或同历史普通更新;
  • 推送后校验远端分支;

那么原来的窄 Skill 更容易理解,也有更小的依赖和授权面。

而 github-project-publisher 面向的是完整项目发布:

  • 保留本地审计和远端历史保护;
  • 增加版本号检测与 vX.Y.Z Tag;
  • 增加 Git 历史 Release Notes;
  • 增加 git archive 源码 ZIP;
  • 增加显式 Release 资产;
  • 增加 SHA256SUMS.txt;
  • 增加 GitHub CLI 创建 Release;
  • 增加分支、Tag 和资产的独立校验。

如果直接升级旧 Skill,原本一句"安全上传到 GitHub"就可能隐含创建公开 Release 的权限。新建入口可以让触发语义更清楚,也保留旧流程的稳定性。

这并不是把功能拆散。对需要完整发布的人来说,新 Skill 仍然是一个整体;只是两个入口分别服务"安全 push"和"版本化发布"两种不同意图。

怎样安装和调用

Windows PowerShell:

git clone https://github.com/wangzifan396-wzf/skills.git
Copy-Item -Recurse .\skills\github-project-publisher `
  "$HOME\.codex\skills\github-project-publisher"

macOS / Linux:

git clone https://github.com/wangzifan396-wzf/skills.git
cp -R skills/github-project-publisher ~/.codex/skills/github-project-publisher

重启 Codex 后,可以直接说:

使用 $github-project-publisher,把当前项目发布到
https://github.com/OWNER/REPOSITORY。
先做 dry-run,检查敏感信息、文件大小、远端历史和完整操作计划;
确认没有 blocker 后再提交、普通推送,并独立验证远端 main。

需要 Release 时再明确:

使用 $github-project-publisher,把当前项目以 1.2.3 发布到现有 GitHub 仓库。
先审计和 dry-run;通过后创建普通提交与 push、v1.2.3 Tag、源码 ZIP、
Release Notes、显式资产和 SHA256SUMS.txt,再创建 GitHub Release 并核对所有资产。
不要 force push,不要覆盖已有 Tag,不要改写已有远端 URL。

运行依赖分两层:

  • 分支发布只需要 Python 3.10+ 和系统 Git;
  • GitHub Release 额外需要已经安装并登录的 GitHub CLI gh。

认证继续交给 Git Credential Manager、SSH Agent 或 GitHub CLI 自己管理。Skill 不要求把 Token 放进参数、文件或聊天记录。

当前边界

V1 故意没有把所有 GitHub 能力都塞进来。

它不会自动:

  • 创建 GitHub 仓库;
  • 修改仓库 visibility、description、topics 或组织设置;
  • 开启 GitHub Pages;
  • 创建 Issue、Pull Request 或 Discussion;
  • force push、覆盖已有 Tag 或重写 Git 历史;
  • 清洗已经进入 Git 历史的秘密值;
  • 配置 Git LFS、子模块或组织 SSO;
  • 决定第三方二进制资产是否拥有再分发权;
  • 把 CSDN、视频平台或其他外部发布权限顺带包含进来。

目标仓库默认也要求用户已经创建。创建仓库涉及 owner、visibility、组织策略、初始化文件和权限等新决策,不应该被一句"上传项目"静默包含。

未来如果增加仓库创建,我更倾向于继续放在同一个用户入口里,但要求单独的显式参数和 preview 字段,例如 --create-repository --visibility public,而不是在找不到远端时自动创建。

文章配图如何复现

本文封面和 5 张正文图都读取:

docs/images/github-project-publisher/article-data.json

其中提交号、候选文件数、测试数、blocker、warning 和本地/远端 SHA 来自真实 Git 输出、发布 receipt 和独立验证 JSON,不是为了排版手工编造。

生成命令:

Set-Location promo-video
npm.cmd run article:github-project-publisher:images

图片使用 Pillow 生成 1920×1080 封面与 1600×900 正文图。以后测试数、提交或发布结果变化时,只需先更新结构化 JSON,再统一重建图片,避免同一个数字散落在多张图里。

这次真正想解决的问题

"完全自动上传 GitHub"最有价值的部分,不是把十几条命令压缩成一条命令,而是让这条命令仍然能回答:

  • 它准备上传什么?
  • 它为什么拒绝某个文件或远端?
  • 它获得了哪一层写权限?
  • 它有没有改写历史或远端配置?
  • Release 里的每个文件来自哪个最终提交?
  • 最后谁证明 GitHub 上的结果真的一致?

我最终采用的结构是:一个综合 Skill 负责完整用户意图,内部模块化脚本负责确定性执行,dry-run 和显式参数负责权限边界,独立验证器负责给出第二份证据。

这让"全自动"不再等于"遇到任何情况都继续向前",而是:

在边界清楚、证据充分、授权明确时,把重复的发布动作完整做完;遇到历史冲突、秘密值或目标不一致时,自动停止。

项目已经开源:

https://github.com/wangzifan396-wzf/skills

如果你正在把本地工具、网页项目、Python 包或小游戏仓库整理成正式开源项目,可以先安装 github-project-publisher 跑一次 dry-run。它给出的候选文件、远端历史和 Release 计划,往往比直接执行一次 push 更能暴露真正的发布问题。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐