Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/release-to-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Deploy release to production

# GitHub Release 发布后,把 `production` 分支指针快进到该 release 的 commit,
# 触发 Vercel 对 `production` 分支(Branch Tracking)的生产构建与部署。
# 只更新指针、不创建新提交;strict fast-forward,绝不 force。
# 发布前必须已通过 `.github/workflows/release.yml`(Release verification)的产物验证,
# 「创建 GitHub Release」即代表该 tag 已验收(见 CONTRIBUTING.md「Release 闸门」)。
#
# 推送凭据:专用 deploy key(secret `PRODUCTION_DEPLOY_KEY`,SSH,仅本仓库写权限),
# 在 `production` 分支保护 ruleset 中作为 bypass actor 更新指针。
# `GITHUB_TOKEN` 只读(contents: read),无法触碰 `production`。

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "要部署的 Release tag,例如 v1.3.9(补发/重跑用;回滚请按 CONTRIBUTING.md 手动 force-with-lease)"
required: false
type: string

permissions:
contents: read

# 串行化 production 指针更新,避免连续发布时两个 run 竞争同一个分支。
concurrency:
group: production-branch-update
cancel-in-progress: false

jobs:
deploy-release:
name: fast-forward production to release commit
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Resolve target tag
id: resolve
run: |
tag="${{ github.event.release.tag_name }}"
if [[ -z "$tag" ]]; then
tag="${{ github.event.inputs.tag }}"
fi
if [[ -z "$tag" ]]; then
echo "No release tag resolved." >&2
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "Target tag: $tag"

- name: Set up production deploy key
env:
DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}
run: |
if [[ -z "${DEPLOY_KEY}" ]]; then
echo "::error::Secret PRODUCTION_DEPLOY_KEY is not configured. Add the production deploy key as an Actions secret before running; refusing to update production without a dedicated deploy credential."
exit 1
fi
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
install -m 600 /dev/null "$HOME/.ssh/id_ed25519"
printf '%s\n' "${DEPLOY_KEY}" > "$HOME/.ssh/id_ed25519"
# 固定 github.com 的 RSA host key(校验 SHA256 指纹,避免 TOFU 被中间人替换)。
# 指纹取 GitHub 官方 /meta 的权威值;GitHub 轮换 host key 后需同步更新此处。
ssh-keyscan -t rsa github.com > "$HOME/.ssh/known_hosts"
if ! ssh-keygen -lf "$HOME/.ssh/known_hosts" | grep -q 'SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s'; then
echo "::error::github.com SSH host key fingerprint mismatch; refusing to continue." >&2
exit 1
fi
cat > "$HOME/.ssh/config" <<'EOF'
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
EOF

- name: Fast-forward production branch
env:
TARGET_TAG: ${{ steps.resolve.outputs.tag }}
GIT_SSH_COMMAND: "ssh -F $HOME/.ssh/config"
run: |
set -euo pipefail
commit="$(git rev-parse "${TARGET_TAG}^{commit}")"
echo "Target commit: ${commit} (${TARGET_TAG})"
git fetch origin production
current="$(git rev-parse origin/production)"
echo "Current production: ${current}"
if [[ "$current" == "$commit" ]]; then
echo "production is already at ${TARGET_TAG}; nothing to do."
exit 0
fi
if ! git merge-base --is-ancestor "$current" "$commit"; then
echo "Refusing non-fast-forward update: production (${current}) is not an ancestor of ${TARGET_TAG} (${commit})." >&2
echo "Rollback is a manual force-with-lease push; see CONTRIBUTING.md 'Release 闸门'." >&2
exit 1
fi
git push "git@github.com:${GITHUB_REPOSITORY}.git" "${commit}:refs/heads/production"
16 changes: 13 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,19 @@ Release 必须在 Tag 上执行,不把普通 PR 当作发布验收:
1. 先同步 `package.json`、`package-lock.json`、`README.md`、`CHANGELOG.md` 和 `RELEASE_NOTES.md` 的版本号。
2. 在目标 commit 创建匹配的 `vX.Y.Z` Tag;Tag 推送后等待 `Release verification` 全绿。
3. 自动检查版本和 Tag 一致性、发布说明部署清单、最终 `dist`/压缩包的敏感内容、构建产物、第三方资源 SHA-256 和许可证清单。
4. 部署完成后手动运行 `Release production verification`,填写同一个 Tag 和生产 HTTPS 地址,验收首页、Manifest、CSP、HSTS、X-Frame-Options 等响应头。
5. 创建 GitHub Release 前人工核对:Release 页面 Tag 与已验收 Tag 相同;附件来自已扫描的最终压缩包;附件 SHA-256 与本地/CI 记录一致;没有额外未扫描附件。
6. 涉及 Supabase 迁移时,发布人必须在生产 Supabase 确认目标迁移已执行,再在发布说明记录结果;自动化测试不等于生产迁移已完成。
4. 创建 GitHub Release 前人工核对:Release 页面 Tag 与已验收 Tag 相同;附件来自已扫描的最终压缩包;附件 SHA-256 与本地/CI 记录一致;没有额外未扫描附件。
5. GitHub Release 发布后自动部署:`Deploy release to production` 将 `production` 分支指针快进到该 Tag 的 commit(只快进、不 force、不并入 `main` 开发内容),Vercel 的 Branch Tracking 随之触发生产构建与部署。
6. 部署完成后手动运行 `Release production verification`,填写同一个 Tag 和生产 HTTPS 地址,验收首页、Manifest、CSP、HSTS、X-Frame-Options 等响应头。
7. 涉及 Supabase 迁移时,发布人必须在生产 Supabase 确认目标迁移已执行,再在发布说明记录结果;自动化测试不等于生产迁移已完成。

### production 分支与生产部署

`production` 是纯指针分支:内容 = 最近一次发布的 commit,只被 `.github/workflows/release-to-production.yml` 更新,不在其上直接提交或合并 `main` 开发内容。

- **触发**:GitHub Release 发布(`release: published`)后自动快进;需要补发/重跑时可手动 `workflow_dispatch` 指定同一个 Tag(同样只允许 fast-forward)。
- **安全**:只允许 strict fast-forward——目标 commit 必须是当前 `production` 指针的后代,否则工作流失败并明确报错,绝不 force。发布前仍需先通过 `Release verification`(见上第 2、3 条),创建 GitHub Release 即代表该 Tag 已验收。
- **回滚**:生产异常时把 `production` 指针退回上一个已验收 Tag。回滚无法快进,需维护者手动执行 `git push --force-with-lease origin <上一个vX.Y.Z>^{commit}:production`,随后可运行 `Release production verification` 复核恢复后的站点。
- **分支保护**:`production` 已启用仓库 ruleset 保护,不允许无门禁直接推送。工作流通过专用 deploy key(Actions secret `PRODUCTION_DEPLOY_KEY`,SSH,仅本仓库写权限)更新指针,deploy key 在 `require_pull_request`(1 个审查)规则下作为 bypass actor;同时 `non_fast_forward`(禁止 force push,仅 admin 可 force 以支持回滚)与 `deletion`(禁止删除)规则独立生效。`GITHUB_TOKEN` 在本工作流中为只读,无法触碰 `production`;新增任何写入 `production` 的通道前,先确认其已加入对应 ruleset 的允许 bypass 集合。

普通项目复用这套流程时,只复制通用闸门;在 `release-gate.config.json` 中按项目调整产物目录、第三方资源、发布说明标记和生产响应头,不要照搬影伴的 Piper、Vercel 或 Supabase 假设。

Expand Down
Loading