Files
nexu-io__open-design/.github/workflows/cut-patch-release.yml

228 lines
11 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
# this week's minor (the branch cut-release created on Tuesday) 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 current line's minor base X.Y.0 must be a PUBLISHED
# stable GitHub Release (tag open-design-vX.Y.0, 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 "Tuesday minor 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 shell; pass via env and validate.
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
# Decide the final version V FIRST: a validated manual override wins,
# otherwise bump the patch of the highest release branch.
if [ -n "$INPUT_VERSION" ]; then
# Strict x.y.z, digits only — rejects shell metacharacters, newlines, etc.
if ! printf '%s' "$INPUT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Invalid version '$INPUT_VERSION' (expected x.y.z, digits only)"
exit 1
fi
V="$INPUT_VERSION"
else
latest=$(git ls-remote --heads origin 'release/v*' \
| sed -E 's#.*/release/v##' \
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
if [ -z "$latest" ]; then
echo "::error::No release/vX.Y.Z branch found to base a patch on."
exit 1
fi
major=${latest%%.*}; rest=${latest#*.}; minor=${rest%%.*}; patch=${rest#*.}
V="${major}.${minor}.$((patch+1))"
fi
# V is now guaranteed to match ^[0-9]+\.[0-9]+\.[0-9]+$.
# Gate on the minor base of the FINAL V — never the highest branch — so a
# manual `version=` on a different line is checked against ITS own minor
# (e.g. version=0.15.1 gates open-design-v0.15.0, not the latest 0.14.0).
vmajor=${V%%.*}; vrest=${V#*.}; vminor=${vrest%%.*}
MINOR_BASE="${vmajor}.${vminor}.0"
{
echo "version=$V"
echo "branch=release/v$V"
echo "minor_base=$MINOR_BASE"
echo "minor_tag=open-design-v$MINOR_BASE"
} >> "$GITHUB_OUTPUT"
echo "Cutting patch v$V (branch release/v$V); gating on stable v$MINOR_BASE"
# Guard: the minor this patch sits on (the Tuesday cut) 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 Tuesday minor is published
id: guard
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
MINOR_TAG: ${{ steps.ver.outputs.minor_tag }}
FORCE: ${{ inputs.force }}
run: |
set -euo pipefail
if [ "${FORCE:-false}" = "true" ]; then
echo "force=true: skipping the publish guard"
echo "published=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# "published" = the release exists AND is neither a draft nor a prerelease.
# gh's --jq evaluates `(.isDraft or .isPrerelease) | not` to true only when
# both are false. A missing release makes `gh release view` exit non-zero,
# so the `|| published=false` fallback treats "not found" as "not published".
published=$(gh release view "$MINOR_TAG" \
--repo nexu-io/open-design \
--json isDraft,isPrerelease \
--jq '(.isDraft or .isPrerelease) | not' 2>/dev/null) || published=false
echo "release $MINOR_TAG -> published=$published"
echo "published=$published" >> "$GITHUB_OUTPUT"
# ---- Skip path: minor 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.minor_base }}** 还没发布成功(stable GitHub Release 尚未 published),本次周四小版本 **v${{ steps.ver.outputs.version }}** 的自动切分支与构建已跳过。
待 v${{ steps.ver.outputs.minor_base }} 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::Tuesday minor v${{ steps.ver.outputs.minor_base }} is not published; patch cut skipped."
exit 0
# ---- Happy path: minor 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