* 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>
Cutting a release branch (Tuesday minor via cut-release, Thursday patch via
cut-patch-release) pushes to release/**, which triggers notify-release-feishu to
build a full prerelease before it posts the download card — 20-40 minutes during
which the team has no signal the cut even happened.
Post an eager green "🌿 已切出 release 分支" card the moment the branch exists, from
the cut workflow itself, reusing feishu-notice.ts (node:crypto only, so the
existing setup-node suffices). The card names:
- the branch and whether it is a 大版本 (minor) or 小版本 (patch),
- the exact backport label (`backport release/vX.Y.Z`) so people know which
label to apply for backports,
- a note that the prerelease build is running and a download card will follow.
The download card from notify-release-feishu is unchanged; each cut now yields
an immediate ack card plus the eventual download card. The patch workflow's eager
card only fires on the happy (published) path, alongside the existing skip card.
Topology test asserts both workflows post the eager card after the branch push and
name the backport label.
The first scheduled cut-patch-release run (2026-07-09) failed at "Create branch +
bump version + push" with "nothing to commit, working tree clean". The guard had
passed (open-design-v0.14.0 published → happy path) and it computed patch v0.14.1,
but main's apps/packaged/package.json was already 0.14.1: in steady state main
leads stable by exactly one patch (finalize-release bumps main to X.Y.(Z+1) after
X.Y.Z ships), and the Thursday patch equals that same X.Y.(Z+1). So
`npm pkg set version=0.14.1` is an idempotent no-op, `git commit -am` finds an
empty tree, and under `set -euo pipefail` the step dies before the push — no
branch cut, no prerelease, no Feishu card. This collides every Thursday the
version already matches, so it is structural, not a one-off.
Commit the release marker with `--allow-empty` so the branch is always cut and
pushed (which is what triggers notify-release-feishu); a real version bump still
commits the change normally. Add a topology assertion that the release commit
tolerates an empty tree.
Verified locally: with a real `npm pkg set version=<same>` (idempotent, no diff)
the old `git commit -am` fails exactly as CI did while `--allow-empty` succeeds
with a marker commit; a differing version still commits the bump.
* ci(release): add Thursday patch-release cut gated on the Tuesday minor shipping
cut-release cuts a minor every Tuesday (from main HEAD) and, via the release/**
push, fires notify-release-feishu to build a prerelease and post a Feishu card.
Add cut-patch-release: the same flow one weekday later (Thursday 09:00
Asia/Shanghai) with three deltas:
- Bumps the PATCH of the highest release/vX.Y.Z branch (X.Y.(Z+1)), still cut
from main HEAD like cut-release.
- Gates on this week's minor having actually shipped: the line's minor base
X.Y.0 must be a PUBLISHED stable GitHub Release (open-design-vX.Y.0,
isDraft=false, isPrerelease=false — i.e. release-stable ran
`gh release edit --draft=false --latest`, which rolls the tag back on
failure). If it is not published, no branch is cut and nothing is built;
instead a Feishu notice card announces the skip.
- On the happy path it is otherwise identical to cut-release: create the branch,
bump apps/packaged, App-token push (which triggers notify-release-feishu), and
create the backport label.
feishu-notice.ts is a lightweight sibling of feishu.ts for non-build events
(here: the skip card). It reuses the same signed-webhook + retry transport and
depends only on node:crypto, so it runs under the existing setup-node with no
pnpm install.
packaged-smoke-workflow.test.ts locks the guard: Thursday cron + patch bump, the
published-stable predicate, and that every branch-cutting step is gated so the
skip path posts the notice without cutting or building.
* fix(release): gate the patch cut on the final version's minor, not the highest branch
The guard target MINOR_BASE was derived from the highest release/v* branch before
the manual workflow_dispatch `version=` override was applied. On the auto path V
comes from that same branch, so it matched — but a manual `version=0.15.1` while
the latest branch is release/v0.14.0 would check open-design-v0.14.0 (the wrong
minor), letting the workflow cut release/v0.15.1 even when 0.15.0 has not shipped
(and the inverse could wrongly block a valid manual patch on another line).
Decide V first (validated override wins, else patch-bump the highest branch),
then derive MINOR_BASE from V's own major/minor. Add topology assertions that
MINOR_BASE comes from V and never from the highest branch, covering the manual
dispatch path.