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