Files
nexu-io__open-design/.github/workflows/cut-patch-release.yml
lefarcen 34ac405bfe fix(release): gate the Thursday patch cut on the previous patch, not the minor base (#7977)
* refactor(release): extract the patch-cut pre-flight out of inline shell

Faithful port, no behavior change: resolve-patch-cut.ts computes the same
version and the same X.Y.0 gate target the shell did, and runs the same
gh release lookup. Moving it out of YAML is what makes the gate decision
testable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(release): pin the patch cut on the previous patch, not the minor base

Red against the current guard: with release/v0.22.2 cut and unshipped, the
pre-flight gates open-design-v0.22.0, finds it published, and cuts v0.22.3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(release): gate the Thursday patch cut on the previous patch

The guard checked the minor base X.Y.0, so it only ever guarded a line's
first patch. Once X.Y.0 shipped, X.Y.2, X.Y.3, ... all passed no matter how
many unshipped releases had stacked up beneath them.

Gate on the release the cut actually stacks on — the highest release/vX.Y.Z
below it — which is the same target cut-release.yml uses for the Tuesday
minor, and still gives the answer the old minor-base comment wanted (cutting
0.15.1 checks open-design-v0.15.0, not the older 0.14.0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(release): surface patch gate lookup failures

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 08:03:53 +00:00

177 lines
8.6 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: cut-patch-release
# Every Thursday 09:00 Asia/Shanghai, cut the next PATCH release — but only once
# the release it would stack on has actually shipped stable.
#
# This is the Tuesday cut-release flow with three deltas:
# 1. It bumps the PATCH, not the minor: highest release/vX.Y.Z -> X.Y.(Z+1)
# (e.g. 0.14.0 -> 0.14.1). Like cut-release, the branch is cut from main HEAD.
# 2. A pre-flight guard, the same one cut-release applies to the Tuesday minor:
# the PREVIOUS release branch must be a PUBLISHED stable GitHub Release
# (isDraft=false, isPrerelease=false — i.e. release-stable ran
# `gh release edit --draft=false --latest`). If it is not published yet, do
# NOT cut a branch or launch a prerelease publication; instead post a Feishu
# notice that the patch cut was skipped, and stop. The independent scheduled
# Windows canary remains an advisory product-smoke signal and does not block
# this branch cut or its prerelease package.
# 3. On the happy path it is otherwise identical to cut-release: create
# release/vX.Y.(Z+1), bump apps/packaged/package.json, push with the App token
# (which triggers notify-release-feishu -> prerelease build + Feishu card), and
# create the "backport release/vX.Y.(Z+1)" label.
#
# "Cut a patch" here means "cut a new point-release branch + build a prerelease +
# announce it", exactly like Tuesday. It deliberately does NOT auto-promote to
# stable; that stays a human-gated release-stable dispatch.
#
# NOTE: schedule triggers only fire from the default branch (main), so this file
# must live on main to run on its cron.
on:
schedule:
- cron: '0 1 * * 4' # 01:00 UTC = 09:00 Asia/Shanghai, Thursday
workflow_dispatch:
inputs:
version:
description: 'Full patch version x.y.z (empty = bump patch of the highest release branch)'
required: false
default: ''
force:
description: 'Skip the "previous release is published" guard and cut anyway'
required: false
type: boolean
default: false
permissions:
contents: read # branch/label writes still go through the App token below
concurrency:
group: cut-patch-release
cancel-in-progress: false
jobs:
cut:
if: github.repository == 'nexu-io/open-design'
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v2
id: app
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
# Smoke is advisory; release cuts always use the current main tip.
ref: main
fetch-depth: 0
token: ${{ steps.app.outputs.token }}
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Compute next patch version
id: ver
env:
# Never interpolate the raw input into the script; pass via env and validate.
INPUT_VERSION: ${{ inputs.version }}
run: node --experimental-strip-types .github/scripts/release/resolve-patch-cut.ts resolve
# Guard: the release this patch stacks on must already be a PUBLISHED stable
# GitHub Release. release-stable publishes with
# `gh release edit --draft=false --latest` and rolls the tag back on failure,
# so a non-draft, non-prerelease release is the ground-truth "shipped" signal.
- name: Check the previous release is published
id: guard
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
GATE_TAG: ${{ steps.ver.outputs.gate_tag }}
FORCE: ${{ inputs.force }}
run: node --experimental-strip-types .github/scripts/release/resolve-patch-cut.ts gate
# ---- Skip path: previous release not published yet -> no branch/release build; notify and stop ----
# feishu-notice.ts imports only node:crypto (no workspace deps), so the
# setup-node above is all it needs — no pnpm install, same as feishu.ts.
- name: Notify Feishu that the patch cut was skipped
if: steps.guard.outputs.published != 'true'
env:
FEISHU_WEBHOOK: ${{ secrets.FEISHU_RELEASE_WEBHOOK }}
FEISHU_SIGN_SECRET: ${{ secrets.FEISHU_RELEASE_SIGN_SECRET }}
NOTICE_TITLE: "⏭️ 周四小版本已跳过"
NOTICE_TEMPLATE: "orange"
NOTICE_BODY: |
上一个 release **v${{ steps.ver.outputs.gate_version }}** 还没发布成功(stable GitHub Release 尚未 published),本次周四小版本 **v${{ steps.ver.outputs.version }}** 的自动切分支与构建已跳过。
待 v${{ steps.ver.outputs.gate_version }} promote 到 stable 后,可手动重新触发本工作流(或用 `force` 强制切分支)。
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: node --experimental-strip-types tools/release/src/notifications/feishu-notice.ts
- name: Stop here when skipping
if: steps.guard.outputs.published != 'true'
run: |
echo "::notice::Previous release v${{ steps.ver.outputs.gate_version }} is not published; patch cut skipped."
exit 0
# ---- Happy path: previous release published -> cut the patch, same as cut-release ----
- name: Bail out if the branch already exists
if: steps.guard.outputs.published == 'true'
env:
BRANCH: ${{ steps.ver.outputs.branch }}
run: |
set -euo pipefail
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "::error::$BRANCH already exists, skipping"
exit 1
fi
- name: Create branch + bump version + push
if: steps.guard.outputs.published == 'true'
env:
VERSION: ${{ steps.ver.outputs.version }}
BRANCH: ${{ steps.ver.outputs.branch }}
run: |
set -euo pipefail
git config user.name 'open-design-release-bot[bot]'
git config user.email 'open-design-release-bot[bot]@users.noreply.github.com'
git switch -c "$BRANCH"
( cd apps/packaged && npm pkg set version="$VERSION" )
# main leads stable by exactly this patch in steady state (finalize-release
# bumps main to X.Y.(Z+1) after X.Y.Z ships), so apps/packaged is often
# already at $VERSION and the bump is a no-op. --allow-empty commits a
# release marker anyway instead of dying on "nothing to commit"; either way
# the branch is cut + pushed, which is what triggers notify-release-feishu.
git commit --allow-empty -am "chore(release): v$VERSION"
git push origin "$BRANCH" # App token push -> triggers notify-release-feishu (prerelease + Feishu)
- name: Create backport label
if: steps.guard.outputs.published == 'true'
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
gh label create "backport release/v$VERSION" \
--repo nexu-io/open-design \
--color 0E8A16 \
--description "Backport this fix to release/v$VERSION" \
--force
# Post an immediate "branch cut" card so the team is notified the moment the
# patch branch exists, without waiting ~20-40 min for notify-release-feishu to
# finish the prerelease build. The download card still follows from that build.
# feishu-notice.ts only imports node:crypto, so the setup-node above suffices.
- name: Notify Feishu that the branch was cut
if: steps.guard.outputs.published == 'true'
env:
FEISHU_WEBHOOK: ${{ secrets.FEISHU_RELEASE_WEBHOOK }}
FEISHU_SIGN_SECRET: ${{ secrets.FEISHU_RELEASE_SIGN_SECRET }}
NOTICE_TITLE: "🌿 已切出 release 分支 v${{ steps.ver.outputs.version }}(小版本 patch)"
NOTICE_TEMPLATE: "green"
NOTICE_BODY: |
**分支**:[`release/v${{ steps.ver.outputs.version }}`](${{ github.server_url }}/${{ github.repository }}/tree/release/v${{ steps.ver.outputs.version }})(从 main 切出,小版本 / patch)
**Backport label**:`backport release/v${{ steps.ver.outputs.version }}` —— 要回灌到这个版本的修复,给 PR 打这个 label。
Prerelease 打包已开始(约 20–40 分钟);完成后会再发一张带各平台下载链接的卡片。
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: node --experimental-strip-types tools/release/src/notifications/feishu-notice.ts