mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
34ac405bfe
* 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>
177 lines
8.6 KiB
YAML
177 lines
8.6 KiB
YAML
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
|