Files
DeepSeek-Reasonix/.github/workflows/release-stable.yml
T
SivanCola e7a0f1163b fix(release): prevent skipped stable publishers (#6629)
Problem:
The stable orchestrator reported success after approval even though the CLI, npm, and Desktop publication jobs were skipped through a transitive skipped dependency. The existing recovery dispatch also could not use a workflow fix without moving the already-published tags.

Root cause:
Each reusable publisher joined an orchestrated guard with a deliberately skipped standalone gate. The cache guard used an explicit status condition, but its descendants did not, so GitHub propagated the skipped ancestor through the dependency chain. Recovery also checked out and executed control scripts from the old release candidate.

Fix:
Add explicit success conditions to every affected publication stage, keep recovery authorization and verification on protected main-v2 while building the immutable historical tag SHA, and reject non-ancestor or mismatched tag sets. Add a stable postflight that verifies required CLI/Desktop release assets and the exact npm latest version so a no-op release cannot stay green.

Verification:
- bash scripts/release-workflows.test.sh
- go run github.com/rhysd/actionlint/cmd/[email protected] -ignore 'label "windows-11-arm" is unknown' on all release workflows
- bash -n on all changed release scripts
- verify-stable-release-artifacts.sh against public v1.17.14 artifacts
- ./scripts/cache-guard.sh
- go test ./...
- git diff --check
2026-07-18 08:36:22 +08:00

176 lines
6.0 KiB
YAML

name: Release stable
run-name: Release stable ${{ inputs.tag || github.ref_name }}
# Stable publication has exactly one GitHub environment gate. Push the CLI,
# npm, and desktop tags atomically; the vX.Y.Z tag starts this workflow,
# preflight verifies that all three tags point to the current main-v2 commit,
# and one GitHub approval releases every channel. A manual recovery runs the
# fixed control plane from protected main-v2 while preserving an older tagged
# candidate on main-v2 history. SignPath intentionally keeps its separate
# manual confirmations. Prerelease and canary workflows remain independently
# callable.
on:
push:
tags:
- 'v*'
- '!v*-*'
workflow_dispatch:
inputs:
tag:
description: "Existing stable CLI tag to recover (for example v1.18.0)"
required: true
type: string
concurrency:
group: stable-release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false
# Reusable release workflows can only reduce caller permissions, so the
# orchestrator grants the union needed by CLI/Desktop publication.
permissions:
actions: write
contents: write
jobs:
preflight:
name: validate stable release set
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.release.outputs.version }}
cli_tag: ${{ steps.release.outputs.cli_tag }}
npm_tag: ${{ steps.release.outputs.npm_tag }}
desktop_tag: ${{ steps.release.outputs.desktop_tag }}
sha: ${{ steps.release.outputs.sha }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
# A recovery dispatch uses the fixed workflow/scripts from protected
# main-v2. Publishers still check out the immutable approved tag SHA.
ref: ${{ github.sha }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-node@v6
with:
node-version: "22"
- name: Resolve stable release
id: release
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
ALLOW_STABLE_RECOVERY: ${{ github.event_name == 'workflow_dispatch' }}
run: bash scripts/resolve-stable-release.sh
- name: Validate reviewed release notes
run: node scripts/release-notes.mjs render --version "${{ steps.release.outputs.cli_tag }}" --output /tmp/release-notes.md
- name: Cache hit guard
run: ./scripts/cache-guard.sh
authorize:
name: approve stable release
needs: preflight
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
outputs:
version: ${{ steps.approved.outputs.version }}
cli_tag: ${{ steps.approved.outputs.cli_tag }}
npm_tag: ${{ steps.approved.outputs.npm_tag }}
desktop_tag: ${{ steps.approved.outputs.desktop_tag }}
sha: ${{ steps.approved.outputs.sha }}
steps:
- name: Record approved release
id: approved
env:
VERSION: ${{ needs.preflight.outputs.version }}
CLI_TAG: ${{ needs.preflight.outputs.cli_tag }}
NPM_TAG: ${{ needs.preflight.outputs.npm_tag }}
DESKTOP_TAG: ${{ needs.preflight.outputs.desktop_tag }}
RELEASE_SHA: ${{ needs.preflight.outputs.sha }}
run: |
{
echo "version=$VERSION"
echo "cli_tag=$CLI_TAG"
echo "npm_tag=$NPM_TAG"
echo "desktop_tag=$DESKTOP_TAG"
echo "sha=$RELEASE_SHA"
} >> "$GITHUB_OUTPUT"
echo "Approved stable release $VERSION at $RELEASE_SHA"
cli:
name: publish CLI and Homebrew
needs: authorize
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.authorize.outputs.cli_tag }}
approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
approved_sha: ${{ needs.authorize.outputs.sha }}
orchestrated: true
secrets: inherit
npm:
name: publish npm
needs: authorize
uses: ./.github/workflows/release-npm.yml
with:
channel: stable
base_version: ${{ needs.authorize.outputs.version }}
tag: ${{ needs.authorize.outputs.npm_tag }}
approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
approved_sha: ${{ needs.authorize.outputs.sha }}
orchestrated: true
secrets: inherit
desktop:
name: publish desktop
needs: authorize
uses: ./.github/workflows/release-desktop.yml
with:
channel: stable
tag: ${{ needs.authorize.outputs.desktop_tag }}
approved_cli_tag: ${{ needs.authorize.outputs.cli_tag }}
approved_sha: ${{ needs.authorize.outputs.sha }}
orchestrated: true
secrets: inherit
postflight:
name: verify stable release artifacts
needs: [authorize, cli, npm, desktop]
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Require every publisher to succeed
env:
CLI_RESULT: ${{ needs.cli.result }}
NPM_RESULT: ${{ needs.npm.result }}
DESKTOP_RESULT: ${{ needs.desktop.result }}
run: |
set -euo pipefail
for result in "$CLI_RESULT" "$NPM_RESULT" "$DESKTOP_RESULT"; do
if [ "$result" != "success" ]; then
echo "::error::stable publisher result is $result, expected success"
exit 1
fi
done
- uses: actions/checkout@v7
with:
# Postflight belongs to the trusted control plane, not the old build
# candidate, which may predate this verifier.
ref: ${{ github.sha }}
- uses: actions/setup-node@v6
with:
node-version: "22"
- name: Verify public artifacts and npm latest
env:
GH_TOKEN: ${{ github.token }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_VERSION: ${{ needs.authorize.outputs.version }}
CLI_TAG: ${{ needs.authorize.outputs.cli_tag }}
DESKTOP_TAG: ${{ needs.authorize.outputs.desktop_tag }}
run: bash scripts/verify-stable-release-artifacts.sh