
使用 GitHub Actions 自动校验大型开源项目中的 Git Rebase 规约静态合规性。拒绝 Git 历史污染用 GitHub Actions 强制落地 Rebase 规约一、前言在大型开源协作中分支管理是噩梦。很多团队口头约定使用rebase保持历史线性但合并时往往变成merge commit。这导致 Git 日志充满了无意义的Merge branch feat-x into main。历史树变得像意大利面一样混乱git bisect几乎无法使用。手动 Code Review 时没人愿意去翻几十行的合并提交记录。就像异步任务需要状态机约束一样Git 提交历史也需要强制约束。本文不聊虚的直接上 GitHub Actions 方案。在合并代码前自动校验提交历史是否符合 Rebase 规约。不符合直接阻塞合并把问题消灭在 PR 阶段。二、底层原理与核心机制1.1 技术背景与核心架构Git 的两种合并方式决定了历史形态。merge会保留分支拓扑产生额外的合并节点。rebase将提交“搬运”到目标分支顶端形成线性历史。大型项目通常要求线性历史以便快速定位 Bug。我们需要在 CI 流程中插入一个“守门员”。这个守门员不关心代码逻辑只关心提交记录的结构。核心逻辑是比对 PR 的提交列表与目标分支的差异。如果存在Merge branch的提交信息或者提交顺序混乱则报错。下图展示了校验流程的拓扑结构。graph TD A[开发者推送代码 (Push)] -- B[GitHub 触发 Pull Request] B -- C[GitHub Actions 启动 Job] C -- D[拉取 PR 提交列表] D -- E{是否存在 Merge Commit?} E -- 是 -- F[输出错误日志] F -- G[设置 Check Run 为失败] E -- 否 -- H{提交信息是否合规} H -- 否 -- F H -- 是 -- I[设置 Check Run 为成功] I -- J[允许合并代码 (Merge)]这种设计的妙处在于“前置阻断”。不要等代码合并进主分支再去git filter-branch清洗历史。那是灾难性的操作会破坏所有本地克隆。在 PR 阶段拦截开发者只需git rebase -i修改即可。1.2 主流方案对比实现这一目标主要有三种路径。第一种是本地 Git Hook。优点是即时反馈缺点是依赖开发者自觉安装无法强制。第二种是 Server-side Hook如 Gitea/GitLab 端。优点是强制缺点是部署复杂开源项目难以掌控服务端。第三种是 GitHub Actions。优点是配置即代码开源项目无需额外服务器天然集成。下表对比了三种方案在生产环境的适用性。方案强制力部署成本适用场景本地 Git Hook低无个人项目或小型团队服务端 Hook高高私有化部署的企业内部GitHub Actions中低开源项目及云端协作对于 GitHub 上的开源项目Actions 是唯一解。它利用pull_request事件天然具备阻塞合并的权限。三、快速上手与核心 API2.1 环境准备与极简配置首先你需要一个 GitHub 仓库。在.github/workflows目录下创建rebase-check.yml。不需要安装任何额外的 CLI 工具。GitHub Actions 的 Runner 已经内置了git和node环境。我们主要依赖actions/checkout来获取代码上下文。关键配置项是permissions。必须授予checks: write权限否则无法更新 PR 的状态检查。同时需要contents: read来读取仓库文件。2.2 核心 API 速查在编写校验脚本时有几个 GitHub API 非常关键。GITHUB_SHA当前触发事件的提交哈希。GITHUB_BASE_REFPR 的目标分支名称如main。GITHUB_HEAD_REFPR 的源分支名称。actions/core用于设置作业输出和标记失败。actions/exec用于执行本地git命令。以下是核心工具函数的速查表。API 方法作用示例参数getCommitList获取 PR 范围内的提交base..headcheckMergeCommit检测合并提交特征正则匹配Merge branchsetFailed标记 CI 失败错误消息字符串四、生产级核心实现3.1 极简实战最小可运行示例我们先写一个最基础的校验脚本。这个脚本只检查是否存在Merge branch字样。这能过滤掉 90% 的不规范合并。代码放在scripts/check-rebase.js。// scripts/check-rebase.js const { execSync } require(child_process); const core require(actions/core); /** * 核心逻辑获取 PR 范围内的所有提交 * 使用 git log 命令对比当前分支与目标分支的差异 */ function getPullRequestCommits() { try { // 获取目标分支名称默认回退到 main const baseRef process.env.GITHUB_BASE_REF || main; // 构建 git log 查询范围 const range ${baseRef}..HEAD; // 执行命令获取提交哈希和消息 const output execSync(git log ${range} --prettyformat:%H %s, { encoding: utf-8 }); return output.trim().split(\n).filter(line line.length 0); } catch (error) { // 生产级异常处理捕获 git 命令执行错误 core.setFailed(Git 命令执行失败${error.message}); return []; } // 主执行入口 const commits getPullRequestCommits(); let hasMergeCommit false; commits.forEach(line { const message line.split( ).slice(1).join( ); // 检查是否包含典型的合并提交特征 if (message.startsWith(Merge branch) || message.startsWith(Merge pull request)) { console.log(⚠️ 发现非 Rebase 提交${message}); hasMergeCommit true; } }); if (hasMergeCommit) { core.setFailed(提交历史包含 Merge Commit请确保使用 Rebase 保持历史线性。); } else { console.log(✅ 提交历史检查通过符合 Rebase 规约。); }这个脚本只有 40 行但足够解决基础问题。它直接调用系统git命令无需引入庞大的 npm 包。3.2 生产级配置与进阶实战基础版只能检测明显的合并提交。进阶版需要检查提交信息是否符合 Conventional Commits 规范。比如必须以feat:、fix:或docs:开头。这能进一步提升代码库的规范性。我们需要在 GitHub Actions 工作流中集成这个脚本。以下是完整的rebase-check.yml配置。# .github/workflows/rebase-check.yml name: Rebase 规约校验 on: pull_request: types: [opened, synchronize, reopened] permissions: checks: write contents: read jobs: check-rebase: runs-on: ubuntu-latest steps: - name: 检出代码 uses: actions/checkoutv4 with: fetch-depth: 0 # 必须获取完整历史否则无法对比分支 - name: 设置 Node 环境 uses: actions/setup-nodev4 with: node-version: 20 - name: 安装依赖 run: npm install actions/core actions/exec - name: 执行 Rebase 校验 run: node scripts/check-rebase.js进阶校验逻辑需要更复杂的正则。我们修改check-rebase.js增加对提交信息的格式检查。// scripts/check-rebase.js (进阶版片段) const conventionalCommitRegex /^(feat|fix|docs|style|refactor|test|chore):\s/.test; function validateCommitMessage(message) { // 忽略合并提交因为前面已经单独处理了 if (message.startsWith(Merge)) return true; if (!conventionalCommitRegex.test