mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-08 23:59:36 +08:00
* ci(release): finalize-release — bump main + drop backport label after a stable ships * ci(release): arm merge queue + request core-maintainer review on the bump PR main enforces require_code_owner_review and the bump touches CODEOWNERS-owned release manifests, so a bot approval can't merge it hands-free (unlike backport-automerge / bake-plugin-previews-automerge, whose targets/diffs avoid code-owner review). Open the PR, arm the squash merge queue (gh pr merge --auto, proven on main by bake-plugin-previews-automerge), and request the @nexu-io/core-maintainers review that the code-owner rule needs. * ci(release): trigger finalize via workflow_run, not release:published release-stable.yml promotes its own draft to published with gh release edit using GITHUB_TOKEN, and a release event minted by GITHUB_TOKEN does not trigger other workflows — so a release:published trigger would never fire. React to release-stable completing instead (the backport-automerge / bake pattern), and gate on the open-design-vX.Y.Z release actually being published (draft=false) so dry-runs (metadata/prepublish) correctly no-op. * ci(release): resolve version from latest published release, not head_branch PerishCode review (blocking): release-stable supports building one ref while dispatched from another via its `ref` input, so workflow_run.head_branch can be the dispatch ref, not the built release branch — parsing it would miss-fire or read the wrong version. Read the version from the latest published release instead (release-stable marks it --latest), which is branch-independent. Drop the head_branch job-if guard; the draft/prerelease/X.Y.Z guards + bump idempotency keep dry-runs a clean no-op. * test(release): pin finalize-release workflow topology PerishCode review (blocking): .github/AGENTS.md asks cross-workflow behavior changes to update the topology tests, not rely on YAML review alone. Add a focused [P2] test in packaged-smoke-workflow.test.ts (alongside the backport / bake automerge coverage) that pins finalize-release's trusted-consumer contract: workflows: [release-stable] + success/dispatch gating, App-token use, version resolved via gh release view (latest/tag) with NO workflow_run.head_branch dependency, draft/prerelease/X.Y.Z + skip handling, forward-only synchronized bump, core-maintainer review request, and SHA-pinned merge-queue arming. --------- Co-authored-by: lefarcen <ontf116@gmail.com>
205 lines
11 KiB
YAML
205 lines
11 KiB
YAML
name: finalize-release
|
|
# Post-publish bookend to cut-release.yml. When a STABLE release ships, this:
|
|
# 1. Deletes the now-obsolete "backport release/vX.Y.Z" label — a shipped
|
|
# release takes no more backports, so the label only invites mistakes.
|
|
# 2. Opens a PR bumping main's synchronized workspace versions to X.Y.(Z+1),
|
|
# keeping main strictly ahead of the just-shipped stable tag (the build
|
|
# guard requires apps/packaged version > the latest open-design-v* tag),
|
|
# arms it for the merge queue (gh pr merge --auto enqueues on green), and
|
|
# requests a @nexu-io/core-maintainers review.
|
|
#
|
|
# Trigger: workflow_run on release-stable completing — NOT `release: published`.
|
|
# release-stable.yml promotes its own draft to published with `gh release edit
|
|
# --draft=false` using GITHUB_TOKEN, and a release event minted by GITHUB_TOKEN
|
|
# does not trigger other workflows (GitHub's loop guard). So we react to the
|
|
# build finishing instead, exactly like backport-automerge / bake-plugin-
|
|
# previews-automerge react to ci. The version is read from the LATEST published
|
|
# release — release-stable promotes its release with `--latest`, so after a real
|
|
# publish that IS the just-shipped stable. We deliberately do NOT parse
|
|
# workflow_run.head_branch: release-stable can be dispatched from one ref while
|
|
# building another via its `ref` input, so head_branch may be the dispatch ref,
|
|
# not the built release branch. A dry-run (metadata/prepublish) publishes nothing,
|
|
# so `--latest` stays the previous (already-finalized) stable and every step
|
|
# no-ops by idempotency. The draft/prerelease/X.Y.Z guards add belt-and-braces.
|
|
#
|
|
# Why the bump PR still needs ONE human approval (unlike the other automerge
|
|
# bots, which merge hands-free): main enforces require_code_owner_review, and the
|
|
# bump touches CODEOWNERS-owned release manifests (apps/packaged, apps/desktop,
|
|
# packages/platform, packages/sidecar*, tools/pack, tools/dev). A bot approval
|
|
# can't satisfy the code-owner gate, so a core-maintainer must approve. Once they
|
|
# do, the armed merge queue squashes it in automatically — no manual file editing,
|
|
# label cleanup, or enqueue. (release/v* has require_code_owner_review=false,
|
|
# which is why backports CAN auto-approve.)
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: ["release-stable"]
|
|
types: [completed]
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Stable release tag to finalize, e.g. open-design-v0.12.0'
|
|
required: true
|
|
default: ''
|
|
|
|
permissions:
|
|
contents: read # actual writes go through the App token below
|
|
|
|
jobs:
|
|
finalize:
|
|
if: >-
|
|
github.repository == 'nexu-io/open-design' &&
|
|
(github.event_name == 'workflow_dispatch' ||
|
|
github.event.workflow_run.conclusion == 'success')
|
|
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 }}
|
|
|
|
- name: Resolve the shipped version (latest published stable, or the dispatch tag)
|
|
id: ver
|
|
env:
|
|
GH_TOKEN: ${{ steps.app.outputs.token }}
|
|
EVENT: ${{ github.event_name }}
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
# workflow_dispatch backfill -> finalize the exact tag asked for.
|
|
# workflow_run -> the latest published release, which release-stable marked --latest;
|
|
# NEVER parse workflow_run.head_branch (it can be the dispatch ref, not the built
|
|
# release branch, when release-stable runs with a `ref` input).
|
|
if [ "$EVENT" = "workflow_dispatch" ]; then
|
|
req="$INPUT_TAG"
|
|
case "$req" in open-design-v*) : ;; *) req="open-design-v$req" ;; esac # allow a bare X.Y.Z
|
|
state=$(gh release view "$req" --repo nexu-io/open-design --json tagName,isDraft,isPrerelease 2>/dev/null || echo '')
|
|
else
|
|
state=$(gh release view --repo nexu-io/open-design --json tagName,isDraft,isPrerelease 2>/dev/null || echo '')
|
|
fi
|
|
if [ -z "$state" ]; then
|
|
echo "No release to finalize (no published stable / dry-run / not shipped)."
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0
|
|
fi
|
|
tag=$(printf '%s' "$state" | jq -r '.tagName')
|
|
draft=$(printf '%s' "$state" | jq -r '.isDraft')
|
|
pre=$(printf '%s' "$state" | jq -r '.isPrerelease')
|
|
ver="${tag#open-design-v}"
|
|
# Only a published (draft=false), non-prerelease, open-design-vX.Y.Z release finalizes.
|
|
# On a dry-run the latest published release is the previous (already-finalized) stable,
|
|
# which the bump idempotency + tolerant label delete below turn into a clean no-op.
|
|
if [ "$draft" != "false" ] || [ "$pre" != "false" ] || ! printf '%s' "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
echo "Latest release '$tag' is draft=$draft prerelease=$pre — not a stable X.Y.Z; skipping."
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0
|
|
fi
|
|
major=${ver%%.*}; rest=${ver#*.}; minor=${rest%%.*}; patch=${rest##*.}
|
|
next="${major}.${minor}.$((patch+1))"
|
|
{
|
|
echo "skip=false"
|
|
echo "version=$ver"
|
|
echo "next=$next"
|
|
echo "branch=release-bot/bump-main-v$next"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "Shipped stable v$ver -> bump main to v$next"
|
|
|
|
- name: Checkout main
|
|
if: steps.ver.outputs.skip != 'true'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: main # we bump main, regardless of which ref the release tag points at
|
|
fetch-depth: 0
|
|
token: ${{ steps.app.outputs.token }}
|
|
|
|
- uses: actions/setup-node@v4
|
|
if: steps.ver.outputs.skip != 'true'
|
|
with:
|
|
node-version: 24
|
|
|
|
- name: Delete the shipped release's backport label
|
|
if: steps.ver.outputs.skip != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ steps.app.outputs.token }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
label="backport release/v$VERSION"
|
|
if gh label list --repo nexu-io/open-design --json name --jq '.[].name' | grep -qxF "$label"; then
|
|
gh label delete "$label" --repo nexu-io/open-design --yes
|
|
echo "Deleted label: $label"
|
|
else
|
|
echo "Label '$label' not present; nothing to delete."
|
|
fi
|
|
|
|
- name: Bump main's synchronized workspace versions
|
|
id: bump
|
|
if: steps.ver.outputs.skip != 'true'
|
|
env:
|
|
NEXT: ${{ steps.ver.outputs.next }}
|
|
run: |
|
|
set -euo pipefail
|
|
current=$(node -p "require('./package.json').version")
|
|
# Idempotency: only ever move main forward. If main is already at or ahead
|
|
# of NEXT (re-run, or a later cut already moved it), do nothing.
|
|
lower=$(printf '%s\n%s\n' "$current" "$NEXT" | sort -V | head -1)
|
|
if [ "$current" = "$NEXT" ] || [ "$lower" = "$NEXT" ]; then
|
|
echo "main is at $current (>= $NEXT); no bump needed."
|
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# Bump only the synchronized set: every workspace package.json whose version
|
|
# equals the current monorepo version. Independently-versioned packages
|
|
# (telemetry-worker, components, agui-adapter, tools/serve, …) keep their own
|
|
# version. `npm pkg set version` touches only the top-level "version" field,
|
|
# never a pinned dependency that happens to share the number (e.g. @xterm/addon-fit).
|
|
changed=0
|
|
for f in package.json apps/*/package.json packages/*/package.json tools/*/package.json e2e/package.json; do
|
|
[ -f "$f" ] || continue
|
|
v=$(node -p "require('./$f').version" 2>/dev/null || echo '')
|
|
if [ "$v" = "$current" ]; then
|
|
( cd "$(dirname "$f")" && npm pkg set version="$NEXT" )
|
|
changed=$((changed+1))
|
|
fi
|
|
done
|
|
echo "Bumped $changed manifests from $current to $NEXT."
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Open the bump PR, arm the merge queue, request review
|
|
if: steps.ver.outputs.skip != 'true' && steps.bump.outputs.changed == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ steps.app.outputs.token }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
NEXT: ${{ steps.ver.outputs.next }}
|
|
BRANCH: ${{ steps.ver.outputs.branch }}
|
|
run: |
|
|
set -euo pipefail
|
|
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
|
|
echo "::warning::$BRANCH already exists; a bump PR is likely already open. Skipping."
|
|
exit 0
|
|
fi
|
|
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"
|
|
git commit -am "chore(release): bump main to $NEXT ahead of stable $VERSION"
|
|
git push origin "$BRANCH"
|
|
head_sha=$(git rev-parse HEAD)
|
|
body=$(printf '%s\n' \
|
|
"Automated by \`finalize-release\` after publishing \`open-design-v$VERSION\`." \
|
|
"" \
|
|
"Keeps \`main\` strictly ahead of the shipped stable tag — the build guard requires \`apps/packaged\` version to be greater than the latest \`open-design-v*\` tag. Bumps only the synchronized workspace manifests (those already sharing the monorepo version); independently-versioned packages are left untouched." \
|
|
"" \
|
|
"The \`backport release/v$VERSION\` label was already deleted by this same run." \
|
|
"" \
|
|
"**Needs one core-maintainer approval:** this touches CODEOWNERS-owned release manifests, so \`main\`'s code-owner rule requires a human review (a bot approval can't satisfy it). The merge queue is already armed — approve and it squashes in automatically.")
|
|
pr=$(gh pr create --repo nexu-io/open-design --base main --head "$BRANCH" \
|
|
--title "chore(release): bump main to $NEXT ahead of stable $VERSION" \
|
|
--body "$body")
|
|
echo "Opened $pr"
|
|
# Request the code owners who must approve (best-effort; never fail finalize on a hiccup).
|
|
gh pr edit "$pr" --repo nexu-io/open-design --add-reviewer nexu-io/core-maintainers || \
|
|
echo "::warning::could not request nexu-io/core-maintainers review on $pr"
|
|
# main is a squash merge queue; --auto arms enqueue-on-green (proven by
|
|
# bake-plugin-previews-automerge — allow_auto_merge=false does not block the queue).
|
|
# --match-head-commit pins the armed SHA so a later push isn't merged untested.
|
|
gh pr merge "$pr" --repo nexu-io/open-design --squash --auto --match-head-commit "$head_sha"
|