# ChatGPT Web + GitHub + Self-hosted Runner：任意设备开发，统一从 GitHub 部署

> Canonical: https://vxi.me/posts/chatgpt-web-github-self-hosted-runner/
> Locale: zh-cn
> Published: 2026-10-07
> Updated: 2026-10-07
> Markdown: https://vxi.me/posts/chatgpt-web-github-self-hosted-runner.md

我以前做服务器项目，开发和发布基本都绕不开同一台电脑：本地拉代码，Codex 或 IDE 改完，`git push`，再开终端 SSH 到服务器，`git pull`、构建、重启、看日志。这个方式没有什么“不专业”，反而很直接；问题是只要项目开始长期维护，电脑、代码工作区、SSH 会话和生产发布就被绑在一起了。

最近我把自己的博客部署链路重新收了一遍，最后保留的生产路径变成了：

```text
浏览器 / 本地开发 / 云端 Coding Agent
                ↓
              GitHub
                ↓
        GitHub Actions
                ↓
      Self-hosted Runner
                ↓
       build / validate
                ↓
        release directory
                ↓
        current symlink
                ↓
            production
```

这套结构最吸引我的地方，不是“以后再也不用本地电脑”，而是**开发入口可以变化，生产出口尽量保持一个**。Mac 上写代码、手机上看任务、网页里继续一个云端 Coding 任务，都可以只是入口；真正需要长期稳定的是 GitHub 里的代码状态、审查记录和那条确定性的生产 workflow。

我这次还特意把笔记里的几个说法重新和官方文档、当前仓库代码对了一遍。结果有两个地方必须收紧：普通 ChatGPT 连接 GitHub 并不等于自动拥有写仓库权限；我现在这套部署也不是“任何失败都自动回滚”。把这些边界讲清楚以后，这个架构反而更实用。

GitHub 对 Self-hosted Runner 的当前说明在这里：https://docs.github.com/en/actions/reference/runners/self-hosted-runners

## Self-hosted Runner 不是 GitHub 远程 SSH 进你的服务器

我一开始很容易把它脑补成“GitHub 收到 push，然后远程登录 VPS 执行脚本”。实际模型不是这样。

GitHub 官方文档写得很明确：Self-hosted Runner 应用运行在你自己的机器上，由 Runner **主动连接 GitHub**，接收 job assignment；主机需要能够向 GitHub 发起 outbound HTTPS 连接，核心通信走 443 端口。

所以一台 VPS 注册成 Runner 以后，更接近长期在线的任务执行器：

```text
GitHub
  ↑  Runner 主动建立连接
  │
Self-hosted Runner on VPS
  │
  ├─ checkout
  ├─ npm ci
  ├─ test / build
  └─ local deploy
```

workflow 里真正决定“谁来接这份工作”的只是 runner 标签。公开文章里我把自己的标签简化成：

```yaml
jobs:
  deploy:
    runs-on: [self-hosted, linux, prod]
```

这和旧的 SSH/SCP 发布路径差别很大。以前是 CI 在外部机器构建，再把产物推到 VPS；现在 job 本身就在 VPS 的 Runner workspace 里运行，构建完可以直接调用本机部署脚本，不需要再为“CI 怎么登录服务器”维护一套 SSH key、SCP 路径和远程命令。

对我来说，GitHub 在这里更像控制面和状态中心，VPS 才是执行面。只要 Runner 在线，哪个设备发起了代码修改并不重要，最终生产都经过同一份 workflow。

这也解释了为什么我现在更愿意把 GitHub 当 source of truth。服务器上的工作目录不再是“主代码”，它只是某一次 job 的临时执行环境；真正需要追踪的是 commit、PR、workflow run 和 release。

## 浏览器可以成为开发入口，但“连接 GitHub”不等于“能写 GitHub”

这里是我觉得最容易被宣传语带偏的一层。

OpenAI 当前的 GitHub 连接文档明确区分了两件事。普通 ChatGPT GitHub app 用来读取、搜索和分析你授权的仓库；帮助中心甚至直接回答了“能否 push 代码、更新或 PR”这个问题：**标准 GitHub 连接本身是只读的，要生成、编辑并直接推送代码，需要使用 Codex。**

官方说明：https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt

所以我现在画这套架构时，不会再写成：

```text
ChatGPT Web
   ↓
直接修改 GitHub
```

更准确的是：

```text
浏览器 / 手机 / 桌面
        ↓
具备写权限的 Coding 工作流
例如 Codex Cloud / 受控 Agent
        ↓
branch / commit / PR
        ↓
GitHub
```

Codex Cloud 这层确实让“电脑不在身边也能继续开发”变得更现实。OpenAI 当前帮助中心说明，Codex Cloud 的任务运行在 OpenAI 管理的计算机上，准备好项目环境后，可以在支持的桌面、网页或移动端继续云端任务；任务也可以在你的本地电脑休眠时继续运行。

对应说明：https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan

但我不会把它描述成“网页版没有 Token 限制”或者“永远不受额度影响”。产品套餐、工作区权限和使用限制都会变化，真正稳定的优势不是一句“无限”，而是**代码状态不再依赖你当前这台 Mac 的终端会话**。

这点和我之前写 [新版 Codex 为什么越来越像开发环境](/posts/what-changed-in-the-new-codex/) 时的感受是一致的。模型只是入口之一，真正让工作可持续的是仓库、环境、任务状态和验证链能不能被重新接起来。

## 生产出口只剩 GitHub 以后，我才开始真正相信“任意设备开发”

如果只做到“网页里能改代码”，我觉得价值还不算大。最麻烦的发布步骤如果最后仍然是“回到 Mac，SSH 上服务器敲几条命令”，那手机和网页只是把写代码提前了，生产依赖仍然绑在本地电脑上。

我现在这套博客 workflow 是更关键的一步：生产只认受信任的 `main` 更新。

我检查了当前实际运行的 workflow，它的顺序大致是：

```text
main push
  ↓
checkout
  ↓
runtime / directory checks
  ↓
npm ci
  ↓
syntax checks
  ↓
build
  ↓
capture previous release
  ↓
local atomic deploy
  ↓
post-deploy SEO verification
  ↓
submit sitemap
```

不是设计图而已。2026 年 10 月 7 日我核对了实际的 Deploy Production run `#37624628201`，对应审计源 revision 为 `f36a433`；`Build`、`Atomic Deploy`、`Post-Deploy SEO Verification` 和 sitemap submission 都成功结束。因为生产仓库是 private，这里只公开可核对的脱敏标识，不放一个普通读者打不开的私有 GitHub 链接。这个 run 对我最有价值的地方，是它证明生产发布已经真的从“某台本地电脑上的 SSH 操作”迁到了 repository workflow。

部署脚本本身也很朴素。公开示例可以简化成：

```text
/srv/app/
├─ releases/
│  ├─ 20261007-abc1234/
│  └─ 20261007-def5678/
└─ current -> releases/20261007-def5678/
```

新版本先进入独立 release 目录，再用临时 symlink 原子替换 `current`。这样 Nginx 或其他 Web Server 始终只看 `current`，构建工作区不会直接变成线上目录。

这里我顺手纠正了自己笔记里的一个表述。我原来很容易把这套东西概括成“失败就自动回滚”，但真正看 workflow 代码以后，准确说法更窄：

- 构建或前置检查失败时，根本不会进入生产切换；
- 部署后 SEO verification 失败时，workflow 有显式逻辑把 `current` 指回 previous release；
- 不能因此推出“任何发生在 symlink 切换之后的故障都会自动回滚”。

这个区别看起来有点抠字眼，实际很重要。运维架构最怕漂亮的框图比代码更强。以后如果我要继续强化这条链路，应该把所有需要自动恢复的 failure class 明确列出来，而不是靠“atomic deploy”四个字产生安全感。

这和我早先写的 [Markdown + GitHub Actions 自动发布流程](/posts/markdown-blog-publish-workflow/) 其实已经不是同一个层次。前者解决“内容怎么自动上线”，现在这套东西解决的是“无论开发从哪里发起，生产到底认哪一条路径”。

## Self-hosted Runner 最大的坑，是它真的在你的机器上执行代码

把 Runner 装进 VPS 很爽，因为它直接拿到了本机文件系统、网络和部署能力。风险也恰好来自同一个地方。

GitHub 的安全文档对这一点写得非常重：Self-hosted Runner 不像 GitHub-hosted Runner 那样天然保证每个 job 都来自全新、隔离的临时虚拟机；如果不受信任的 workflow code 在上面执行，机器可以被持久化污染。因此 GitHub 明确建议 Self-hosted Runner 几乎不要用于公共仓库，也提醒即使是 private/internal repo，只要有人能 fork 并提交 PR，也要认真考虑这条攻击路径。

官方安全说明：https://docs.github.com/en/actions/reference/security/secure-use

这让我重新看了一遍自己当前的 workflow。生产部署由受信任的 `main` push 触发，这个方向没问题；但内容 PR 的验证任务现在也会落到同一台 Self-hosted Runner 上。

在我目前的私人仓库、受信任作者模型里，这个风险是可控的。可一旦未来把仓库公开，或者让更多外部协作者可以提交 PR，我不会继续让“PR 里的任意代码”接触生产 Runner。更合理的拆法会是：

```text
Untrusted / public PR
        ↓
GitHub-hosted runner
或独立、可销毁的验证 Runner

Protected main
        ↓
Production self-hosted runner
        ↓
Deploy
```

甚至在团队扩大以后，Runner group 也应该只允许指定仓库和指定 workflow 使用生产 Runner。

这也是为什么“AI 不需要拿 VPS SSH key”只是安全改善的一部分。把 SSH key 从 Agent 手里拿走当然很好，但如果 Agent 能修改 workflow，而这个 workflow 可以在生产主机上执行任意 shell，那权限只是换了入口，没有消失。

真正应该保护的是整个执行边界：谁可以修改 workflow、谁可以合并 main、哪些事件能命中生产 Runner、Runner 用户在系统里能写哪些目录、Secrets 有多大权限。

## 本地 Codex 和 SSH 没有消失，只是从“日常发布”退到了更合适的位置

做到这里以后，我并不觉得本地 Codex + 本地仓库 + SSH 变得落后。恰恰相反，它们处理的是浏览器控制面最不擅长的那部分工作。

我现在会这样分：

| 场景 | 云端 / GitHub 工作流 | 本地 Codex + SSH |
| --- | --- | --- |
| 多设备继续代码任务 | 很合适 | 依赖当前机器 |
| 代码历史与 PR 审查 | 很合适 | 仍以 GitHub 为准 |
| 标准 build / test / deploy | 很合适 | 不应每次手工做 |
| 查看 `docker ps` / `journalctl` | 间接 | 很合适 |
| 数据库、磁盘、网络现场排查 | 间接 | 很合适 |
| 生产故障紧急处理 | 可辅助 | SSH 更直接 |
| 可审计发布 | 天然有 run / commit | 手工 SSH 较弱 |

浏览器或手机最适合当 control plane：决定改什么、审查 diff、看 CI、触发一个已经定义好的流程。本地终端更像 observability/debug plane：你需要知道这一秒服务器上哪个进程卡住、哪个容器重启、磁盘为什么满、Nginx 实际读的是哪份配置。

SSH 也不需要被彻底删掉。我更愿意把它降级成 break-glass 通道：日常发布不依赖它，故障现场需要时仍然可以直接进机器。这样即使临时 SSH 维护，最终修复也应该回到 GitHub 形成 commit/workflow，而不是让服务器长期积累只有一台电脑知道的“现场改动”。

如果已经有一套本地 Markdown + GitHub Actions 的发布流程，可以先参考 [我之前的自动发布方案](/posts/markdown-blog-publish-workflow/)；如果还在权衡静态博客和传统 CMS，那篇 [Astro vs WordPress](/posts/astro-vs-wordpress-markdown-github-actions-blog/) 更接近架构选择这一层。

我现在更看重的不是“以后能不能完全脱离 Mac 开发”，而是四个角色终于能分开：

```text
浏览器 / Codex / 本地 IDE   → 开发入口
GitHub                     → 持久代码与审查状态
Self-hosted Runner         → 受信任的自动执行
SSH                        → 深度诊断与应急
```

设备可以换，Agent 可以换，甚至生产服务器也可以以后再换。只要代码状态、发布规则和验证逻辑都留在 GitHub，这条链路就不会跟着某一台电脑一起丢。

这才是我觉得 ChatGPT / Codex + GitHub + Self-hosted Runner 最有价值的地方：不是把终端藏起来，而是把“必须在这台电脑上亲手完成”的事情尽量从日常流程里拿掉。

## Verification context

- Level: first-party
- Verified: 2026-10-07T00:00:00.000Z
- Environment: Linux Self-hosted Runner
- Environment: Source f36a433
- Environment: Deploy run #37624628201
- Environment: Post-deploy SEO
- Evidence mode: mixed
- Evidence receipt: https://vxi.me/evidence/chatgpt-web-github-self-hosted-runner.json
