ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

自动合并PR与GitHub Actions:构建任何人可编辑的开放网站

2026/9/7 16:11:13 拓冰建站 浏览量
自动合并PR与GitHub Actions:构建任何人可编辑的开放网站 之前在做社区站点的时候最耗费精力的往往不是功能开发而是“内容更新”。只要内容需要多数人参与维护就会遇到一系列重复劳动审核提交内容、合并代码、打包发布。时间一长维护者就成了“人肉 CI”。今天要聊的项目思路非常直接把所有内容当成一个 GitHub 仓库任何访客都可以通过 Pull RequestPR修改网站再由自动化工作流完成校验和自动合并最后自动部署上线。整个过程不再需要人工点击 merge也不需要给访客开通任何后台账号。本文会从概念、架构、关键机制讲起然后给出一个可以完整跑通的实战示例包括 GitHub Actions 配置、分支保护、JSON 数据校验、GitHub Pages 部署以及自动合并模式下的安全风险与加固方案。1. 背景与核心概念1.1 这是一个什么样的项目“Show HN: A website anyone can edit through auto-merged GitHub PRs” 描述的系统本质上是一个“由 PR 驱动内容更新”的静态网站。它的工作方式可以理解为网站内容文件存放在一个公开 GitHub 仓库中。任何人无需申请权限只需要提交 PR。自动化脚本检查 PR 是否只修改了允许修改的内容文件。校验通过后PR 被自动合并。合并后触发部署流程网站内容更新。一句话概括用代码协作的流程管理网站内容的发布。这种模式很容易让人联想到维基百科但实现机制完全不同。维基百科依赖独立后台系统和人工审核而这个项目建立在 Git、GitHub、GitHub Actions 之上天然具备版本历史、Diff 对比、回滚能力、审核留痕等特性。1.2 它解决了什么问题在传统网站内容维护中常见的痛点有下面几类。痛点传统方案PR 自动合并方案内容更新需要人工处理管理员登录后台编辑发布访问者提交 PR自动合并发布权限边界不清晰给不同人分配后台账号仓库公开PR 天然带身份和权限边界缺少变更记录依赖后台日志Git 提交历史天然完整、可回溯发布流程重复人工 build、部署GitHub Actions 自动化误操作影响大改错直接上线PR 自动校验 静态回滚方便也就是说这个方案本质上是把“内容发布”从传统的 CMS 后台操作迁移到了开发者熟悉的 Git 协作流程中。1.3 适用场景这种模式非常适合以下类型的网站网址导航站、资源聚合站开源项目的 FAQ / Wiki / 文档站社区公告板、链接收藏站数据驱动的展示页例如“站点列表”“项目推荐”“工具集”如果网站的内容主要是结构化数据或 Markdown 文本那么 PR 驱动的方式会比传统后台更透明、更容易自动化。2. 整体架构与核心工作流2.1 一次完整的内容更新流程先看整体架构。为了便于理解把整个过程拆成几步贡献者 │ 1. fork 仓库 或 创建分支 │ 2. 修改 site/websites.json ▼ 提交 Pull Request │ ▼ GitHub Actions 自动校验 │ ├─ 检查变更文件是否在白名单内 │ ├─ 校验 JSON / Markdown 格式 │ └─ 审核是否满足自动合并条件 ▼ PR 自动合并到 main 分支 │ ▼ GitHub Pages 部署工作流触发 │ ▼ 静态网站更新完成从贡献者角度看整个流程只有两步是人工动作修改内容文件提交 PR。后续的校验、合并、部署全部自动化。2.2 核心组成模块这个系统由五个关键部分组成。内容仓库网站的源码和内容数据放在同一个仓库中通常是公开仓库。分支保护规则强制所有对主分支的修改都必须通过 PR不能直接 push。自动合并工作流核心自动化逻辑负责检查 PR 安全性、校验数据、执行自动合并。部署工作流当主分支内容变化时自动构建静态站点并发布。贡献者任何能够访问仓库的人都可以通过 PR 参与内容维护。3. 环境准备与版本说明开始写代码之前先确认需要准备的环境3.1 平台与工具要求工具用途说明GitHub 账号创建仓库、使用 Actions必须Git本地提交代码、推送分支也可以用 GitHub 网页编辑Python 3校验 JSON 数据格式用于 Actions 中的命令浏览器访问 GitHub 页面、查看结果必须如果本地访问 GitHub 不稳定也可以直接用 GitHub 网页端的“创建新文件”功能来修改内容并创建 PR不强制使用本地 Git。3.2 版本说明GitHub Actions 的 action 版本更新速度较快本文中的几个 action 如下actions/checkoutv4actions/github-scriptv7actions/upload-pages-artifactv3actions/deploy-pagesv4这些版本在实际使用时建议以 GitHub Marketplace 上的最新版本为准。如果版本有更新按官方文档调整即可核心逻辑不会变。另外示例项目会使用 GitHub Pages 作为部署目标因此需要在仓库设置中开启 Pages并将 Source 设置为 “GitHub Actions”。4. 核心机制如何安全地自动合并 PR自动合并 PR 是这类项目的核心。但 GitHub 的权限模型限制很多如果不理解底层机制很容易遇到“工作流没有权限”“PR 无法合并”的问题。4.1 为什么pull_request事件不够用在 GitHub Actions 中最常用的 PR 触发事件是on: pull_request: types: [opened, synchronize, reopened]但这里有一个关键限制pull_request事件触发的 job默认只有只读权限无法修改仓库内容。也就是说如果你在pull_request工作流里执行gh pr merge实际上没有权限合并 PR。还有一个安全设计来自 fork 仓库的 PR 默认拿不到仓库的 secrets。这样做是为了防止陌生人通过 PR 窃取密钥。那么自动合并应该怎么实现常见方案有两种第一种是使用pull_request_target事件。它会在“目标仓库”的上下文中运行具备写入权限且可以使用 secrets。第二种是使用 GitHub App 机器人账号。机器人拥有独立的 Token通过pull_request事件监听 PR再调用 GitHub API 完成合并。对于入门项目来说pull_request_target是最简单直接的方式。4.2pull_request_target的安全边界pull_request_target虽然好但要小心一个问题它会在 Base 仓库的上下文中执行并且可以访问 secrets 和仓库写入权限。因此绝不能在这个工作流里直接 checkout 并执行 PR 提交的任意脚本。如果 PR 恶意修改了仓库中的scripts/validate.py而工作流又执行了这个脚本攻击者就能让校验永远通过。安全做法是工作流中保留固定路径白名单检查使用 GitHub API 获取变更文件列表。只允许修改指定数据文件例如site/websites.json。对数据文件只做纯解析例如用json.load()不执行其中的脚本。把自动合并 job 设置为 required status check。4.3gh pr merge --auto的作用gh pr merge是 GitHub CLI 提供的合并命令。它支持三种合并策略# 使用 Merge 方式合并 gh pr merge 123 --merge # 使用 Squash 方式合并 gh pr merge 123 --squash # 使用 Rebase 方式合并 gh pr merge 123 --rebase加上--auto参数后含义变成“开启自动合并”gh pr merge 123 --merge --auto--auto不会在任何条件下都立即合并而是在 PR 满足全部合并条件后自动执行合并。这些合并条件包括分支保护检查通过required status check 通过没有合并冲突没有未解决的 conversation。这意味着工作流只负责“打开自动合并开关”真正的合并保护仍由分支保护规则负责。4.4 校验和合并必须放在同一个 Job为什么校验逻辑和合并逻辑必须放在同一个 Job 中因为如果 Webhook 触发了一个“校验 Job”和一个“另一个合并 Job”它们之间有时间差。攻击者可以在校验结束后、合并执行前再向同一 PR 推送新的恶意内容形成典型的 TOCTOUTime-of-check to Time-of-use问题。把“获取变更文件列表 → 校验数据 → 执行自动合并”放在同一个 Job 中可以最大程度减少这个风险窗口。5. 完整实战搭建一个 PR 驱动的可编辑网站下面我们从零开始搭建一个真实的示例网站一个“开放编辑的网址导航站”。任何人都可以提交 PR在site/websites.json中添加一条网址记录。PR 通过校验后会自动合并并自动部署到 GitHub Pages。5.1 创建仓库与目录结构在 GitHub 上创建新仓库例如命名为anyone-can-edit-site仓库类型选择 Public。仓库结构如下anyone-can-edit-site/ ├── .github/ │ └── workflows/ │ ├── auto-merge.yml # 自动合并工作流 │ └── deploy-pages.yml # GitHub Pages 部署工作流 ├── site/ │ └── websites.json # 网站数据任何人都可编辑 ├── index.html # 网站页面 ├── CONTRIBUTING.md # 贡献指南 └── README.md # 项目说明5.2 编写网站页面和 JSON 数据文件先创建index.html它是一个非常简单的静态页面!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleAny One Can Edit Site/title style body { font-family: system-ui, -apple-system, sans-serif; max-width: 640px; margin: 48px auto; padding: 0 16px; color: #333; } #list { display: grid; gap: 12px; margin-top: 24px; } .item { padding: 16px; border: 1px solid #e2e2e2; border-radius: 8px; } .item a { font-size: 18px; font-weight: 600; color: #0366d6; text-decoration: none; } .item p { margin: 8px 0 0; color: #666; } .tip { margin-top: 40px; padding: 16px; background: #f6f8fa; border-radius: 8px; font-size: 14px; } /style /head body h1开放编辑的网址导航/h1 div idlist/div div classtip 想添加新链接请提交 PR 修改 codesite/websites.json/code合并后网站会自动更新。 /div script fetch(site/websites.json) .then((response) response.json()) .then((data) { const list document.getElementById(list); // 注意这里使用 textContent 而不是 innerHTML是为了防止 XSS data.forEach((item) { const div document.createElement(div); div.className item; const a document.createElement(a); a.href item.url; a.textContent item.name; div.appendChild(a); const p document.createElement(p); p.textContent item.description; div.appendChild(p); list.appendChild(div); }); }); /script /body /html再创建site/websites.json[ { name: CSDN, url: https://blog.csdn.net, description: 中文技术社区 }, { name: GitHub, url: https://github.com, description: 代码托管与协作平台 } ]这里要注意前端渲染数据时使用textContent而不是innerHTML这是内容类网站的底线。因为任何人都能提交内容无法保证数据文件中的字符串一定安全。5.3 编写自动合并工作流创建.github/workflows/auto-merge.ymlname: Auto-Merge Safe PR on: pull_request_target: types: [opened, synchronize, reopened] permissions: contents: write pull-requests: write jobs: validate-and-merge: runs-on: ubuntu-latest steps: - name: 获取 PR 变更文件列表 uses: actions/github-scriptv7 with: script: | const prNumber context.payload.pull_request.number; const { data: files } await github.rest.pulls.listFiles({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, }); const changedFiles files.map((file) file.filename); console.log(变更文件, changedFiles); // 只允许修改 site/websites.json const allowList [site/websites.json]; const isAllowed changedFiles.length 1 allowList.includes(changedFiles[0]); if (!isAllowed) { core.setFailed(自动合并已阻止只允许修改 site/websites.json); } - name: 检出 PR 分支代码 uses: actions/checkoutv4 with: ref: ${{ github.event.pull_request.head.sha }} - name: 校验 JSON 格式 run: | python3 -c import json; json.load(open(site/websites.json)) - name: 自动合并 PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr merge ${{ github.event.pull_request.number }} --merge --auto这段工作流脚本的逻辑很简单但每一步都对应前面讲的安全原则用 GitHub API 获取 PR 变更文件列表而不是执行 PR 中任何代码严格要求只修改site/websites.json一个文件防止有人顺手改 workflow 或其他目录pull_request_target事件提供写入权限自动合并使用--auto让分支保护检查成为最后防线。5.4 编写 GitHub Pages 部署工作流创建.github/workflows/deploy-pages.ymlname: Deploy to GitHub Pages on: push: branches: [main] paths: [site/**] permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: true jobs: deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: 检出代码 uses: actions/checkoutv4 - name: 上传静态站点 uses: actions/upload-pages-artifactv3 with: path: . - name: 部署到 GitHub Pages id: deployment uses: actions/deploy-pagesv4这里有一个关键点部署工作流只在main分支的site/**路径发生变更时触发。其实也可以去掉paths让任何 push 都触发部署。加上paths是为了减少不必要的构建次数。5.5 配置分支保护这一步非常重要不能省略。在仓库页面进入Settings → Branches → Add branch protection rule然后配置以下内容Branch name patternmain勾选Require a pull request before merging勾选Require status checks to pass before merging在搜索框中找到validate-and-merge并勾选可选勾选Require branches to be up to date before mergingvalidate-and-merge这个名称来自工作流中的 job 名称。分支保护配置完成后自动合并不是“想合并就合并”而是必须满足这张保护网。注意如果分支保护规则没有勾选Require status checks to pass before merging那么自动合并 job 即使失败PR 也仍然可能被手动合并。这会让校验流程形同虚设。5.6 启用 GitHub Pages进入Settings → Pages在 “Build and deployment” 中把 Source 选择为GitHub Actions。因为部署工作流已经存在当main分支发生 push 时Pages 部署任务会自动启动。5.7 本地提交修改并提交 PR完成以上配置后仓库已经具备了“任何人可编辑”的能力。下面模拟一个贡献者提交内容修改。克隆仓库git clone https://github.com/你的用户名/anyone-can-edit-site.git cd anyone-can-edit-site创建分支git checkout -b add-github-docs编辑site/websites.json添加一条新数据[ { name: CSDN, url: https://blog.csdn.net, description: 中文技术社区 }, { name: GitHub, url: https://github.com, description: 代码托管与协作平台 }, { name: GitHub Docs, url: https://docs.github.com, description: GitHub 官方文档 } ]提交并推送git add site/websites.json git commit -m docs: 添加 GitHub Docs 链接 git push origin add-github-docs然后在 GitHub 网页上 Create Pull Request。5.8 观察工作流运行结果PR 创建后会自动触发validate-and-merge工作流。进入 PR 页面的 “Checks” 标签可以看到validate-and-merge running → success如果没问题PR 会在几秒内自动合并。然后进入仓库的 Actions 页面会看到Deploy to GitHub Pages工作流开始运行。部署完成后访问https://你的用户名.github.io/anyone-can-edit-site/页面会显示三条站点记录其中就包括刚通过 PR 添加的 GitHub Docs。到这里一个完整的“任何人通过自动合并 PR 编辑网站”的闭环就搭建完成了。6. 常见问题与排查思路在实战中以下问题出现频率最高。问题现象常见原因解决思路PR 创建后没有触发任何 Actions仓库 Actions 被禁用或事件类型不匹配检查 Settings → Actions 权限确认pull_request_target已启用工作流报“只允许修改 site/websites.json”PR 修改了其他文件让贡献者只改动数据文件或调整白名单JSON 格式非法导致 job 失败少了逗号、引号等本地运行python3 -m json.tool site/websites.json检查合并没有发生但 job 显示绿色分支保护检查未通过或--auto执行时 PR 不满足合并条件查看 PR 底部的 status check 详情合并没有发生且 job 显示失败自动合并权限不足或条件不满足在 job 日志中找到失败步骤检查permissions配置合并后网站没有更新分支保护规则中卡在“等待 status check”阶段。原因是新推送的 commit 导致旧的 status check 失效GitHub 会等待所有 required check 在新 SHA 上通过后才会允许 merge。如果后续有新的 push自动合并不会立即执行必须等新检查通过才行。重大提示不要把validate-and-merge设置为“非 required”状态否则自动合并可能绕过校验。自动合并后部署失败部署工作流的权限不足或 Pages Source 未设置为 GitHub Actions检查permissions.pages和id-token配置检查 Pages 设置页面内容无法加载前端 fetch 路径写错或 JSON 文件未发布打开浏览器开发者工具 Network 面板检查site/websites.json的请求状态6.1 排查思路总结遇到自动合并不工作时按以下顺序排查先看 Actions 日志确认工作流是否真的被触发。确认 PR 分支是否包含触发事件注意 fork 仓库的 PR 在pull_request_target下也可以运行。确认工作流中的校验步骤是否通过。确认分支保护规则中 required status check 是否包含validate-and-merge。确认 PR 没有 merge conflict且分支没有过期。查看 PR 底部 GitHub 给出的具体错误提示。7. 安全风险与生产级加固自动合并 PR 是一把双刃剑。如果只是个人项目或小社区上面的简单配置够用但如果面向真正的公开互联网以下几点必须认真考虑。7.1 威胁模型谁会攻击这个网站自动合并模式下攻击者不需要破解后台只需要提交一个“看起来正常”的 PR。常见的攻击类型包括修改 workflow 文件使自动合并逻辑失效在数据文件中插入恶意链接引流到钓鱼网站向 JSON 数据中注入恶意脚本利用 XSS 攻击访问者大量提交无关内容破坏数据质量利用 PR 内容触发 CI 中的漏洞执行任意命令。所以自动合并不是“AI 代码审查”它只是把“人工点击合并”这个动作省掉了安全边界需要通过代码和配置显式构建。7.2 建议的加固方案风险加固方案修改非白名单文件通过 API 严格限制文件路径与数量执行 PR 提供的恶意脚本不在pull_request_target中执行仓库内任意脚本XSS前端用textContent不允许 HTML 渲染用户输入恶意链接增加 URL 协议校验建立域名黑名单人工抽查内容质量垃圾在CONTRIBUTING.md中写明规则使用 PR 模板自动合并被滥用同一用户多次失败时可以在 workflow 中暂停自动合缺少审计所有历史都在 Git 中定期 review 合并记录更进一步的方案是允许内容文件只能追加不能删除。例如用脚本检查 PR 中变更的类型如果site/websites.json中删除了已有条目则阻止自动合并并转为人工审核。# 思路示例检查 JSON 是否只增加、未删除 # 可以通过 git diff 或者对比 base 分支的 JSON 长度实现这类逻辑实现起来并不复杂但对于防止恶意清空数据非常有效。7.3 是否应该使用 GitHub App 机器人使用GITHUB_TOKEN是简单的选择但它的权限范围是整个仓库。生产级项目更推荐使用 GitHub App例如 Renovate、Dependabot 或自建 Probot 机器人来完成自动合并。GitHub App 的优点是权限更精细可以只授予Pull requests: writeToken 独立管理不依赖仓库 secrets可以记录机器人身份便于审计可以监听多个仓库。如果你未来想把这个模式做成一个公开服务那么 GitHub App 方案是正确的方向。7.4 回滚与恢复即使做了各种保护恶意 PR 仍可能漏过校验。好消息是基于 Git 的内容站回滚非常简单git log --oneline git revert 恶意提交的 SHA git push origin main一条 revert 就能让网站恢复到上一个安全版本。这正是这种架构相比传统 CMS 的巨大优势。8. 总结与下一步学习方向本文完整拆解了 “Show HN: A website anyone can edit through auto-merged GitHub PRs” 这个项目背后的核心设计并给出了一个可以跑通的实战案例。你至少应该掌握以下几点PR 驱动内容更新的整体架构和适用场景pull_request与pull_request_target的核心区别gh pr merge --auto的工作机制分支保护、路径白名单、数据校验在自动合并中的作用GitHub Pages 作为部署目标的完整流程自动合并模式下的安全威胁与加固手段。下一步可以从四个方向继续深入把 JSON 导航站升级为 Markdown 文档站用 Python 或 Node.js 构建静态页面使用 GitHub App 替代GITHUB_TOKEN实现更细粒度的权限管理增加内容预览机制例如合并前通过 Vercel / Netlify 预览站点效果接入 GitHub GraphQL API查询 PR 状态、贡献者活跃度等数据做出更完整的社区运营面板。这种“内容即仓库、发布即合并”的思维方式非常适合技术社区、文档站、聚合页等场景。建议你动手把示例仓库跑通一次然后再根据自己项目的规模和受众逐步加入更严格的内容审核和安全策略。等第一份 PR 被自动合并、网站自动更新的时候你会对 GitHub 协作生态有更真切的感受。