事故现场

周五下午4:58,距离下班还有2分钟。我信心满满地执行了一条"简单"的命令:

git push origin main --force

然后整个监控面板变成了红色。

发生了什么

这条命令的本意是把本地的几个配置修复推上去。但我忘了本地仓库的 main 分支已经落后远程 origin/main 30 个 commit。--force 把远程那30个 commit 全部覆盖了,其中包括:

  • 数据库迁移脚本
  • 网关路由配置
  • 上周才上线的支付模块热修复

救火过程

# 第一步:深呼吸
# 第二步:查看 reflog
git reflog

# 找到被覆盖前的 commit hash
# 0a1b2c3 HEAD@{1}: commit: 支付模块热修复

# 第三步:恢复
git push origin 0a1b2c3:main

整个恢复花了 47 分钟。但比这更痛的是之后复盘时发现的三个根本问题:

  1. 我们没有分支保护规则——任何人都可以直接 force push 到 main
  2. 没有部署锁——push 触发了自动部署
  3. 没有回滚预案——出了事才知道该怎么做

后来我们做了什么

# .github/workflows/branch-protection.yml 不是这个
# 我们直接在 GitHub Settings 里配置了:
# ✅ Require pull request reviews before merging
# ✅ Require status checks before merging
# ✅ Do not allow bypassing the above settings
# ✅ Restrict who can push to matching branches(禁止直接 push main)

另外,我们加了一个 deploy lock 机制:

#!/bin/bash
# deploy-lock.sh
LOCK_FILE="/tmp/deploy.lock"

if [ -f "$LOCK_FILE" ]; then
    echo "部署锁已存在,当前有人在部署或维护窗口"
    echo "如需强制解锁,请联系 SRE 团队"
    exit 1
fi

touch "$LOCK_FILE"
echo "部署已锁定,维护窗口开始"
# 部署完成后记得 rm "$LOCK_FILE"

经验总结

永远不要在周五下午 force push。

更准确地说:永远不要在没有 branch protection 的仓库里 force push。

运维的核心不是"出了事能修好",而是"出不了事"。