From 6dd68330d688abaefe6e863f8ba4a40274f2a5a1 Mon Sep 17 00:00:00 2001 From: WardLu Date: Tue, 18 Aug 2026 18:58:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ci):=20GitHub=20Release=20=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=90=8E=E8=87=AA=E5=8A=A8=E5=BF=AB=E8=BF=9B=20produc?= =?UTF-8?q?tion=20=E5=88=86=E6=94=AF=E8=A7=A6=E5=8F=91=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=20(SHA-4/SHA-11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 release-to-production.yml:GitHub Release 发布后把 production 分支指针 快进到该 release 的 commit(strict fast-forward,绝不 force),Vercel Branch Tracking 随之触发生产构建与部署;workflow_dispatch 支持补发/重跑。回滚为 维护者手动 force-with-lease。CONTRIBUTING.md Release 闸门同步 production 分支机制与回滚命令。 --- .github/workflows/release-to-production.yml | 71 +++++++++++++++++++++ CONTRIBUTING.md | 16 ++++- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/release-to-production.yml diff --git a/.github/workflows/release-to-production.yml b/.github/workflows/release-to-production.yml new file mode 100644 index 0000000..6df1146 --- /dev/null +++ b/.github/workflows/release-to-production.yml @@ -0,0 +1,71 @@ +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 闸门」)。 + +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: write + +# 串行化 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: Fast-forward production branch + env: + TARGET_TAG: ${{ steps.resolve.outputs.tag }} + 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 origin "${commit}:refs/heads/production" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f342685..7f7721a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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` 当前未设分支保护,工作流用 `GITHUB_TOKEN`(`contents: write`)更新指针。若后续启用保护,`GITHUB_TOKEN` 无法推送,需改用细粒度 PAT 或 deploy key(存为 secret),或改用 environment 保护。 普通项目复用这套流程时,只复制通用闸门;在 `release-gate.config.json` 中按项目调整产物目录、第三方资源、发布说明标记和生产响应头,不要照搬影伴的 Piper、Vercel 或 Supabase 假设。 From 22850391e5884b6801d720e20933e4770806e6c5 Mon Sep 17 00:00:00 2001 From: WardLu Date: Tue, 18 Aug 2026 22:13:28 +0800 Subject: [PATCH 2/2] ci(security): protect production branch with rulesets and switch deploy to dedicated SSH key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - production 分支启用 ruleset 保护:require_pull_request(1 审查) + 禁止 force push + 禁止删除 - 工作流改用专用 deploy key(secret PRODUCTION_DEPLOY_KEY) 通过 SSH 更新指针 - GITHUB_TOKEN 降为只读(contents: read),无法再触碰 production - 固定 github.com 官方 RSA host key 指纹(校验 ssh-keyscan 结果,防 TOFU 中间人) - CONTRIBUTING.md 更新分支保护现状说明 --- .github/workflows/release-to-production.yml | 36 +++++++++++++++++++-- CONTRIBUTING.md | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-to-production.yml b/.github/workflows/release-to-production.yml index 6df1146..03b785c 100644 --- a/.github/workflows/release-to-production.yml +++ b/.github/workflows/release-to-production.yml @@ -5,6 +5,10 @@ name: Deploy release to production # 只更新指针、不创建新提交;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: @@ -17,7 +21,7 @@ on: type: string permissions: - contents: write + contents: read # 串行化 production 指针更新,避免连续发布时两个 run 竞争同一个分支。 concurrency: @@ -49,9 +53,37 @@ jobs: 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}")" @@ -68,4 +100,4 @@ jobs: echo "Rollback is a manual force-with-lease push; see CONTRIBUTING.md 'Release 闸门'." >&2 exit 1 fi - git push origin "${commit}:refs/heads/production" + git push "git@github.com:${GITHUB_REPOSITORY}.git" "${commit}:refs/heads/production" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f7721a..43bd884 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ Release 必须在 Tag 上执行,不把普通 PR 当作发布验收: - **触发**: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` 当前未设分支保护,工作流用 `GITHUB_TOKEN`(`contents: write`)更新指针。若后续启用保护,`GITHUB_TOKEN` 无法推送,需改用细粒度 PAT 或 deploy key(存为 secret),或改用 environment 保护。 +- **分支保护**:`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 假设。