Files
nexu-io__open-design/.github/workflows/release-gate.yml
lefarcen a50e1f7b06 feat(release): automate release cut + backport + pre-release gate (#4640)
* feat(release): 自动化 release 切版 + backport + 准入闸

引入一套基于 GitHub Action 的 release 流程自动化(配套飞书《发布期间 fix PR 处理手册》):

- cut-release: 每周二 09:00(北京)从 main HEAD 切 release/vX.Y.Z + bump 版本 + 建 backport label
- backport: 合进 main 的 PR 带 backport label → 自动 cherry-pick 到 release 分支(冲突转 draft)
- backport-label-guard: 校验贴标人 write+ 权限,否则撤销 label
- release-gate: 发版前检查目标 release 分支无未落地 backport

全部用 GitHub App (open-design-release-bot) token,非 GITHUB_TOKEN,
以便其 push/PR 能正常触发下游 workflow(notify-release-feishu / ci)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(release): translate workflow comments and messages to English

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(release): validate cut-release version input via env; release-gate fails closed on missing branch

Addresses review (PerishCode):
- cut-release: pass manual version through env + strict x.y.z validation to prevent
  shell injection; downstream steps consume validated outputs via env (no inline ${{ }} in run).
- release-gate: verify the release branch exists before checking PRs, so a mistyped /
  not-yet-cut branch fails the gate instead of falsely passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(release): constrain backport/gate target to the release/vX.Y.Z grammar

Addresses review (PerishCode, round 2):
- backport: tighten label_pattern to ^backport (release/v[0-9]+\.[0-9]+\.[0-9]+)$ so a typo
  like 'backport main' / 'backport feat/foo' no longer matches and is never run by the bot.
- release-gate: validate the input branch against ^release/vX.Y.Z before the existence check,
  so an arbitrary existing branch (e.g. main) with no open PRs cannot falsely pass the gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(release): cut-release always checks out main HEAD

Addresses review (PerishCode, round 3): pin actions/checkout to ref: main so a manual
workflow_dispatch from another branch still cuts the release branch from main HEAD, not
from whatever ref the run was started on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:15:31 +00:00

57 lines
2.5 KiB
YAML

name: release-gate
# Pre-release gate: confirm the target release branch has no un-landed backports.
# Fails if any -> do not tag. Run it before tagging (or wire it as a required pre-release job).
#
# The check = open PRs whose base is the release branch. That single check covers every
# "not landed yet" case:
# - Conflict -> backport-action opens a draft PR (state open, caught here)
# - Clean but not merged / CI still running -> also an open PR, caught here
# Once everything is merged, open PRs targeting the branch hit zero -> the gate passes.
# (korthout has no "label on failure" feature, so this does not rely on any *-failed label.)
on:
workflow_dispatch:
inputs:
release_branch:
description: 'Release branch, e.g. release/v0.12.0'
required: true
permissions:
contents: read
pull-requests: read
jobs:
gate:
if: github.repository == 'nexu-io/open-design'
runs-on: ubuntu-latest
steps:
- name: Check for un-landed backports
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BR: ${{ inputs.release_branch }}
run: |
set -euo pipefail
# Constrain the input to the release/vX.Y.Z grammar first. Any other branch (e.g. main)
# must never pass the gate, even if it exists and happens to have no open PRs targeting it.
if ! printf '%s' "$BR" | grep -Eq '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Invalid release branch '$BR' (expected release/vX.Y.Z)"
exit 1
fi
# Fail closed: the branch must actually exist. A mistyped or not-yet-cut branch makes
# `gh pr list` return an empty list, which would otherwise look like "clean / safe".
if ! gh api "repos/$REPO/git/ref/heads/$BR" >/dev/null 2>&1; then
echo "::error::Release branch '$BR' does not exist — failing closed (refusing to pass the gate)."
exit 1
fi
echo "Checking $BR for un-landed backports (open PRs)…"
open=$(gh pr list --repo "$REPO" --base "$BR" --state open \
--json number,title,isDraft \
--jq '.[] | " #\(.number) \(.title)\(if .isDraft then " [draft/conflict]" else "" end)"')
if [ -n "$open" ]; then
echo "::error::$BR has un-landed backports, do not release:"
echo "$open"
exit 1
fi
echo "✅ $BR has no pending backports, safe to tag and release."