mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
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
591 lines
26 KiB
YAML
591 lines
26 KiB
YAML
name: Release desktop
|
|
|
|
# Desktop (Wails) release line. Stable releases are called by
|
|
# release-stable.yml after its single GitHub environment approval. Direct tag
|
|
# pushes are retained only for desktop prereleases so they can still ship
|
|
# independently.
|
|
#
|
|
# Wails cannot cross-compile a CGO/WebKit binary, so build/ fans out to one native
|
|
# runner per platform. Artifacts are minisign-signed (MINISIGN_* secrets), a
|
|
# latest.json manifest is generated, and everything is published to a GitHub
|
|
# release and mirrored to R2 (the updater reads R2 first, then the crash worker
|
|
# release gateway; stable desktop releases own GitHub's repository-wide "latest").
|
|
#
|
|
# The same build/sign/manifest pipeline serves two channels. An orchestrated
|
|
# `desktop-v*` tag (or manual stable recovery) publishes a GitHub release and
|
|
# moves R2's latest/ pointer. A manual `channel: canary` dispatch builds with
|
|
# -X main.channel=canary
|
|
# and uploads only to R2's canary/ pointer — no GitHub release, so canary never
|
|
# shows on the releases page and never touches latest/.
|
|
on:
|
|
push:
|
|
tags: ["desktop-v*-*"]
|
|
workflow_dispatch:
|
|
inputs:
|
|
channel:
|
|
description: "Release channel"
|
|
type: choice
|
|
options: [stable, canary]
|
|
default: stable
|
|
tag:
|
|
description: "stable: tag to publish (e.g. desktop-v1.1.0)"
|
|
required: false
|
|
type: string
|
|
base_version:
|
|
description: "canary: intended next stable version, e.g. 1.5.0"
|
|
required: false
|
|
type: string
|
|
workflow_call:
|
|
inputs:
|
|
channel:
|
|
description: "Release channel selected by the approved orchestrator"
|
|
required: true
|
|
type: string
|
|
tag:
|
|
description: "Existing desktop tag to publish"
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
base_version:
|
|
description: "Base version used for canary builds"
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
approved_cli_tag:
|
|
description: "Stable CLI tag recorded by the approved orchestrator"
|
|
required: true
|
|
type: string
|
|
approved_sha:
|
|
description: "Immutable commit recorded by the approved orchestrator"
|
|
required: true
|
|
type: string
|
|
orchestrated:
|
|
description: "True only when called by release-stable.yml after approval"
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
permissions:
|
|
contents: write # create the release and upload artifacts
|
|
|
|
jobs:
|
|
orchestration-guard:
|
|
name: verify approved orchestrator
|
|
if: ${{ inputs.orchestrated }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ github.sha }}
|
|
- name: Verify caller and approved release ref
|
|
env:
|
|
ACTUAL_CALLER_WORKFLOW_REF: ${{ github.workflow_ref }}
|
|
EXPECTED_CALLER_WORKFLOW_REF: ${{ format('{0}/.github/workflows/release-stable.yml@{1}', github.repository, github.ref) }}
|
|
CALLER_EVENT_NAME: ${{ github.event_name }}
|
|
CALLER_REF: ${{ github.ref }}
|
|
CALLER_REF_PROTECTED: ${{ github.ref_protected }}
|
|
CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }}
|
|
CALLER_SHA: ${{ github.sha }}
|
|
APPROVED_CLI_TAG: ${{ inputs.approved_cli_tag }}
|
|
APPROVED_SHA: ${{ inputs.approved_sha }}
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
VERIFY_RELEASE_CHECKOUT: false
|
|
run: |
|
|
bash scripts/verify-release-authorization.sh
|
|
bash scripts/verify-release-tag.sh
|
|
|
|
release-gate:
|
|
name: approve standalone desktop release
|
|
if: ${{ !inputs.orchestrated && !(github.event_name == 'workflow_dispatch' && inputs.channel == 'canary') }}
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
steps:
|
|
- env:
|
|
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
|
|
run: echo "Approved standalone desktop release $RELEASE_TAG"
|
|
|
|
cache-guard:
|
|
name: cache hit guard
|
|
needs: [orchestration-guard, release-gate]
|
|
if: ${{ always() && !cancelled() && ((inputs.orchestrated && needs.orchestration-guard.result == 'success') || (!inputs.orchestrated && ((github.event_name == 'workflow_dispatch' && inputs.channel == 'canary') || needs.release-gate.result == 'success'))) }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.approved_sha || inputs.tag || github.ref }}
|
|
|
|
- uses: actions/setup-go@v6
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Cache hit guard
|
|
run: ./scripts/cache-guard.sh
|
|
|
|
build:
|
|
name: build (${{ matrix.name }})
|
|
needs: cache-guard
|
|
if: ${{ always() && !cancelled() && needs.cache-guard.result == 'success' }}
|
|
permissions:
|
|
contents: read # checkout only; the publish job holds contents: write
|
|
actions: read # SignPath reads run details + downloads the unsigned artifact
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# One universal macOS build (Intel + Apple Silicon) on a single arm64
|
|
# runner — avoids the scarce/slow macos-13 Intel runner.
|
|
- { runner: macos-14, platform: darwin/universal, name: darwin-universal }
|
|
- { runner: windows-latest, platform: windows/amd64, name: windows-amd64 }
|
|
- { runner: windows-11-arm, platform: windows/arm64, name: windows-arm64 }
|
|
- { runner: ubuntu-22.04, platform: linux/amd64, name: linux-amd64 }
|
|
runs-on: ${{ matrix.runner }}
|
|
env:
|
|
# Windows Authenticode signing engages only when the SignPath token is set, so
|
|
# forks / token-less runs still build (unsigned), mirroring the APPLE_* gate.
|
|
HAS_SIGNPATH: ${{ secrets.SIGNPATH_API_TOKEN != '' }}
|
|
defaults:
|
|
run:
|
|
shell: bash # desktop-build.sh is bash; windows runners default to pwsh otherwise
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.approved_sha || inputs.tag || github.ref }}
|
|
|
|
- uses: actions/setup-go@v6
|
|
with:
|
|
go-version-file: desktop/go.mod
|
|
cache: true
|
|
cache-dependency-path: desktop/go.sum
|
|
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: "22"
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 10
|
|
|
|
# Linux: WebKitGTK 4.1 toolchain (-tags webkit2_41 in desktop-build.sh).
|
|
# 4.1 ships from ubuntu-22.04 on and is the only one present on 24.04+/Fedora 40+.
|
|
- name: Install Linux build deps
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.1-dev
|
|
|
|
# Linux: nfpm builds the .deb in desktop-build.sh's linux branch. go install
|
|
# drops it in ~/go/bin, already on PATH (same place the wails CLI lands below).
|
|
- name: Install nfpm
|
|
if: runner.os == 'Linux'
|
|
run: go install github.com/goreleaser/nfpm/v2/cmd/[email protected]
|
|
|
|
# Windows: NSIS provides makensis for `wails build -nsis`.
|
|
- name: Install NSIS
|
|
if: runner.os == 'Windows'
|
|
run: pwsh -NoProfile -File scripts/install-nsis.ps1
|
|
|
|
# macOS: create-dmg packages the .app into a drag-to-Applications .dmg.
|
|
- name: Install create-dmg
|
|
if: runner.os == 'macOS'
|
|
run: brew install create-dmg
|
|
|
|
# macOS signing: import the Developer ID cert into a throwaway keychain and
|
|
# stage the notarization key. No-ops (and the build ad-hoc signs) when the
|
|
# APPLE_* secrets aren't set, so forks still build.
|
|
- name: Import Apple signing certificate
|
|
if: runner.os == 'macOS'
|
|
env:
|
|
APPLE_CERT_P12: ${{ secrets.APPLE_CERT_P12 }}
|
|
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
|
|
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
|
run: |
|
|
if [ -z "$APPLE_CERT_P12" ]; then
|
|
echo "APPLE_CERT_P12 unset — desktop build will ad-hoc sign (un-notarized)"
|
|
exit 0
|
|
fi
|
|
KEYCHAIN="$RUNNER_TEMP/signing.keychain-db"
|
|
KEYCHAIN_PASS="$(uuidgen)"
|
|
security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN"
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
|
security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN"
|
|
echo "$APPLE_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
|
|
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" >/dev/null
|
|
# Prepend the signing keychain to the search list so codesign / find-identity see it.
|
|
existing_keychains=()
|
|
while IFS= read -r keychain; do
|
|
[ -n "$keychain" ] && existing_keychains+=("$keychain")
|
|
done < <(security list-keychains -d user | sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//')
|
|
security list-keychains -d user -s "$KEYCHAIN" "${existing_keychains[@]}"
|
|
echo "$APPLE_API_KEY_P8" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
|
rm -f "$RUNNER_TEMP/cert.p12"
|
|
|
|
- name: Install Wails CLI
|
|
run: go install github.com/wailsapp/wails/v2/cmd/[email protected]
|
|
|
|
- name: Resolve version
|
|
id: ver
|
|
run: bash scripts/resolve-desktop-release.sh
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
IN_CHANNEL: ${{ inputs.channel }}
|
|
IN_TAG: ${{ inputs.tag }}
|
|
IN_BASE_VERSION: ${{ inputs.base_version }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
RUN_NUMBER: ${{ github.run_number }}
|
|
|
|
- name: Build and package
|
|
env:
|
|
# macOS Developer ID + notarization path turns on only when all five
|
|
# APPLE_* secrets are present; otherwise desktop-build.sh ad-hoc signs.
|
|
# Harmless on Windows/Linux runners (only the darwin branch reads these).
|
|
HAS_APPLE_CERT: ${{ secrets.APPLE_CERT_P12 != '' && secrets.APPLE_CERT_PASSWORD != '' && secrets.APPLE_API_KEY_P8 != '' && secrets.APPLE_API_KEY_ID != '' && secrets.APPLE_API_ISSUER_ID != '' }}
|
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/notary.p8
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
|
run: scripts/desktop-build.sh "${{ matrix.platform }}" "${{ steps.ver.outputs.version }}" "${{ steps.ver.outputs.channel }}"
|
|
|
|
# Authenticode-sign the Windows installer through SignPath. Runs BEFORE minisign:
|
|
# the signature rewrites the installer's bytes, so the updater's minisign sig and
|
|
# the manifest SHA must be computed over the signed build. SignPath pulls the file
|
|
# from the Actions artifact API, so it must be uploaded first. Canary signs with the
|
|
# test certificate; stable/rc sign with the Foundation release certificate, but only
|
|
# once SIGNPATH_RELEASE_SIGNING_READY is set. The release-signing policy verifies the
|
|
# trusted GitHub origin and intentionally retains its manual approval process as a
|
|
# second-party control. Each Windows architecture can require its own confirmation.
|
|
- name: Upload unsigned installer for SignPath
|
|
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true' && (steps.ver.outputs.channel == 'canary' || vars.SIGNPATH_RELEASE_SIGNING_READY == 'true')
|
|
id: unsigned-installer
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: unsigned-${{ matrix.name }}
|
|
path: dist/*installer*.exe
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
- name: Submit installer for Authenticode signing
|
|
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true' && (steps.ver.outputs.channel == 'canary' || vars.SIGNPATH_RELEASE_SIGNING_READY == 'true')
|
|
uses: signpath/github-action-submit-signing-request@v2
|
|
with:
|
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
|
project-slug: DeepSeek-Reasonix
|
|
signing-policy-slug: ${{ steps.ver.outputs.channel == 'canary' && 'test-signing' || 'release-signing' }}
|
|
artifact-configuration-slug: windows-installer
|
|
github-artifact-id: ${{ steps.unsigned-installer.outputs.artifact-id }}
|
|
github-token: ${{ github.token }}
|
|
wait-for-completion: true
|
|
# Leave headroom for the manual SignPath confirmation, origin verification,
|
|
# malware scanning, and signing.
|
|
wait-for-completion-timeout-in-seconds: 1800
|
|
output-artifact-directory: signed
|
|
|
|
- name: Replace installer with signed build
|
|
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true' && (steps.ver.outputs.channel == 'canary' || vars.SIGNPATH_RELEASE_SIGNING_READY == 'true')
|
|
run: cp signed/*installer*.exe dist/
|
|
|
|
- name: Sign artifacts (minisign)
|
|
working-directory: desktop
|
|
env:
|
|
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
|
|
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
|
|
run: go run ./cmd/sign sign ../dist/*
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: dist-${{ matrix.name }}
|
|
path: dist/*
|
|
if-no-files-found: error
|
|
|
|
publish:
|
|
name: publish release
|
|
needs: build
|
|
if: ${{ always() && !cancelled() && needs.build.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
# The stable caller has already passed the single GitHub release approval.
|
|
# Direct prereleases/manual stable recovery pass release-gate; canaries run
|
|
# free. Windows signing retains its separate SignPath confirmations.
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.approved_sha || inputs.tag || github.ref }}
|
|
|
|
- uses: actions/setup-go@v6
|
|
with:
|
|
go-version-file: desktop/go.mod
|
|
cache: true
|
|
cache-dependency-path: desktop/go.sum
|
|
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: "22"
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: dist
|
|
pattern: dist-*
|
|
merge-multiple: true
|
|
|
|
- name: Resolve version
|
|
id: ver
|
|
run: bash scripts/resolve-desktop-release.sh
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
IN_CHANNEL: ${{ inputs.channel }}
|
|
IN_TAG: ${{ inputs.tag }}
|
|
IN_BASE_VERSION: ${{ inputs.base_version }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
RUN_NUMBER: ${{ github.run_number }}
|
|
|
|
# Generate latest.json with GitHub release download URLs; the mirror step
|
|
# rewrites them to R2 afterwards. GITHUB_REPOSITORY is provided by the runner.
|
|
- name: Generate manifest
|
|
working-directory: desktop
|
|
run: go run ./cmd/sign manifest ../dist "${{ steps.ver.outputs.version }}" "${{ steps.ver.outputs.tag }}"
|
|
|
|
# Canary never appears on the GitHub releases page; the mirror job picks up
|
|
# the signed dist via the canary-dist artifact below. Stable publishes a
|
|
# GitHub release as usual.
|
|
- name: Render reviewed release notes
|
|
if: steps.ver.outputs.channel != 'canary'
|
|
run: node scripts/release-notes.mjs render --version "${{ steps.ver.outputs.version }}" --output /tmp/release-notes.md
|
|
|
|
- name: Revalidate approved release ref
|
|
if: ${{ inputs.orchestrated }}
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
APPROVED_SHA: ${{ inputs.approved_sha }}
|
|
run: bash scripts/verify-release-tag.sh
|
|
|
|
- name: Publish GitHub release
|
|
if: steps.ver.outputs.channel != 'canary'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
# Keep the repository homepage focused on the installable desktop app;
|
|
# the CLI release line is configured not to claim repository-wide latest.
|
|
args=(--title "Reasonix Desktop ${{ steps.ver.outputs.version }}" --notes-file /tmp/release-notes.md)
|
|
if [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then
|
|
args+=(--prerelease --latest=false)
|
|
else
|
|
args+=(--latest)
|
|
fi
|
|
gh release create "${{ steps.ver.outputs.tag }}" dist/* "${args[@]}"
|
|
|
|
- name: Upload canary dist for mirror
|
|
if: steps.ver.outputs.channel == 'canary'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: canary-dist
|
|
path: dist/*
|
|
if-no-files-found: error
|
|
|
|
mirror:
|
|
name: mirror to R2
|
|
needs: publish
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write # gh release download + compatibility manifest upload
|
|
actions: write # dispatch pages.yml to re-bake the site version
|
|
# Skip cleanly when R2 isn't configured; GitHub release still works as fallback.
|
|
if: ${{ always() && !cancelled() && needs.publish.result == 'success' && github.repository_owner == 'esengine' }}
|
|
env:
|
|
HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.approved_sha || inputs.tag || github.ref }}
|
|
|
|
- name: Resolve version
|
|
id: ver
|
|
run: bash scripts/resolve-desktop-release.sh
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
IN_CHANNEL: ${{ inputs.channel }}
|
|
IN_TAG: ${{ inputs.tag }}
|
|
IN_BASE_VERSION: ${{ inputs.base_version }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
RUN_NUMBER: ${{ github.run_number }}
|
|
|
|
- name: Revalidate approved release ref
|
|
if: ${{ inputs.orchestrated }}
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
APPROVED_SHA: ${{ inputs.approved_sha }}
|
|
run: bash scripts/verify-release-tag.sh
|
|
|
|
# Canary has no GitHub release — pull the signed dist from the workflow
|
|
# artifact. Stable pulls from the published release.
|
|
- name: Download canary dist
|
|
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel == 'canary'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: canary-dist
|
|
path: assets
|
|
|
|
- name: Download release assets
|
|
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel != 'canary'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
mkdir -p assets
|
|
gh release download "${{ steps.ver.outputs.tag }}" -R "${{ github.repository }}" -D assets
|
|
|
|
# Rewrite both url and sig inside latest.json from github.com to the R2 CDN,
|
|
# so the updater pulls the manifest AND the heavy artifacts from R2.
|
|
- name: Rewrite latest.json URLs to R2
|
|
if: env.HAS_R2 == 'true'
|
|
env:
|
|
R2_PUBLIC_BASE: https://dl.reasonix.io
|
|
TAG: ${{ steps.ver.outputs.tag }}
|
|
run: |
|
|
f=assets/latest.json
|
|
jq --arg base "$R2_PUBLIC_BASE" --arg tag "$TAG" '
|
|
.platforms |= with_entries(
|
|
.value.url |= sub("https://github.com/[^/]+/[^/]+/releases/download/[^/]+/"; "\($base)/\($tag)/")
|
|
| .value.sig |= sub("https://github.com/[^/]+/[^/]+/releases/download/[^/]+/"; "\($base)/\($tag)/")
|
|
)
|
|
' "$f" > "$f.new"
|
|
mv "$f.new" "$f"
|
|
cat "$f"
|
|
|
|
- name: Configure AWS CLI for R2
|
|
if: env.HAS_R2 == 'true'
|
|
run: |
|
|
aws configure set aws_access_key_id "${{ secrets.R2_ACCESS_KEY_ID }}"
|
|
aws configure set aws_secret_access_key "${{ secrets.R2_SECRET_ACCESS_KEY }}"
|
|
aws configure set region auto
|
|
|
|
- name: Mirror to R2
|
|
if: env.HAS_R2 == 'true'
|
|
env:
|
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
|
TAG: ${{ steps.ver.outputs.tag }}
|
|
run: |
|
|
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
|
|
aws s3 cp assets/ "s3://${R2_BUCKET}/${TAG}/" --recursive --endpoint-url "$ENDPOINT"
|
|
# The pointer a release moves is its channel's alone: canary writes
|
|
# canary/, stable writes latest/. A stable prerelease (rc) moves neither.
|
|
if [ "${{ steps.ver.outputs.channel }}" = "canary" ]; then
|
|
aws s3 cp assets/ "s3://${R2_BUCKET}/canary/" --recursive --endpoint-url "$ENDPOINT"
|
|
elif [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then
|
|
echo "prerelease — leaving R2 latest/ untouched"
|
|
else
|
|
aws s3 cp assets/ "s3://${R2_BUCKET}/latest/" --recursive --endpoint-url "$ENDPOINT"
|
|
fi
|
|
|
|
# dl.reasonix.io serves 403 to GitHub Actions egress IPs (Cloudflare bot
|
|
# protection), so smoke the mirrored objects over the authenticated S3 API
|
|
# instead of the public edge. This verifies the mirror landed; the public
|
|
# edge itself is not reachable from CI and is covered by end users' traffic.
|
|
- name: Smoke desktop release pointers
|
|
if: env.HAS_R2 == 'true'
|
|
env:
|
|
TAG: ${{ steps.ver.outputs.tag }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
CHANNEL: ${{ steps.ver.outputs.channel }}
|
|
PRERELEASE: ${{ steps.ver.outputs.prerelease }}
|
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
|
run: |
|
|
set -euo pipefail
|
|
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
|
|
f=assets/latest.json
|
|
jq -e --arg version "$VERSION" --arg prefix "https://dl.reasonix.io/${TAG}/" '
|
|
.version == $version and
|
|
.download_page == "https://reasonix.io/?download=desktop#start" and
|
|
([.platforms[] | (.url, .sig)] |
|
|
all(type == "string" and startswith($prefix) and (contains("/releases/latest/") | not)))
|
|
' "$f" >/dev/null
|
|
|
|
aws s3 cp "s3://${R2_BUCKET}/${TAG}/latest.json" /tmp/reasonix-desktop-tag-latest.json --endpoint-url "$ENDPOINT"
|
|
jq -e --arg version "$VERSION" '.version == $version' /tmp/reasonix-desktop-tag-latest.json >/dev/null
|
|
if [ "$CHANNEL" = "canary" ]; then
|
|
pointer="canary"
|
|
elif [ "$PRERELEASE" = "true" ]; then
|
|
pointer=""
|
|
else
|
|
pointer="latest"
|
|
fi
|
|
if [ -n "$pointer" ]; then
|
|
aws s3 cp "s3://${R2_BUCKET}/${pointer}/latest.json" /tmp/reasonix-desktop-pointer-latest.json --endpoint-url "$ENDPOINT"
|
|
jq -e --arg version "$VERSION" '.version == $version' /tmp/reasonix-desktop-pointer-latest.json >/dev/null
|
|
fi
|
|
|
|
jq -r '.platforms[] | .url, .sig' "$f" | while IFS= read -r asset; do
|
|
key="${asset#https://dl.reasonix.io/}"
|
|
aws s3api head-object --bucket "$R2_BUCKET" --key "$key" --endpoint-url "$ENDPOINT" >/dev/null
|
|
done
|
|
|
|
# Best-effort probe of the release gateway — the updater's second
|
|
# manifest source — over the same public edge and Go client UA end users
|
|
# hit. A 403 here is the known Cloudflare bot-protection gap (#6005:
|
|
# datacenter/proxy egress gets blocked before the worker runs) and must
|
|
# not fail the release until a WAF skip rule for /v1/desktop/releases/*
|
|
# lands; it is surfaced as a warning so the run shows whether the edge
|
|
# is open. Anything else unexpected (404, 5xx, wrong version) means the
|
|
# gateway route or pointer regressed and fails hard.
|
|
- name: Probe public release gateway
|
|
if: env.HAS_R2 == 'true'
|
|
env:
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
CHANNEL: ${{ steps.ver.outputs.channel }}
|
|
PRERELEASE: ${{ steps.ver.outputs.prerelease }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$PRERELEASE" = "true" ]; then
|
|
echo "prerelease — no pointer moved; skipping gateway probe"
|
|
exit 0
|
|
fi
|
|
chan="stable"
|
|
[ "$CHANNEL" = "canary" ] && chan="canary"
|
|
url="https://crash.reasonix.io/v1/desktop/releases/${chan}/latest.json"
|
|
# curl already prints 000 for a transport failure; || true keeps -e
|
|
# from killing the step so the case below can route it.
|
|
code="$(curl -sS -A "Go-http-client/2.0" -o /tmp/gateway-latest.json -w '%{http_code}' "$url" || true)"
|
|
case "$code" in
|
|
200)
|
|
if jq -e --arg version "$VERSION" '.version == $version' /tmp/gateway-latest.json >/dev/null; then
|
|
echo "gateway serves $VERSION on $chan"
|
|
else
|
|
echo "::error::gateway responded 200 but serves $(jq -r '.version // "<none>"' /tmp/gateway-latest.json), want $VERSION — stale or wrong pointer"
|
|
exit 1
|
|
fi
|
|
;;
|
|
403)
|
|
echo "::warning::gateway returned 403 to CI egress — known bot-protection gap (#6005), not failing the release"
|
|
;;
|
|
000|"")
|
|
echo "::warning::gateway unreachable from CI (transport error), not failing the release"
|
|
;;
|
|
*)
|
|
echo "::error::gateway returned $code for $url — route or pointer regression"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
- name: Attach desktop manifest to matching CLI release
|
|
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel != 'canary' && steps.ver.outputs.prerelease != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
VERSION: ${{ steps.ver.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
if gh release view "$VERSION" >/dev/null 2>&1; then
|
|
gh release upload "$VERSION" assets/latest.json --clobber
|
|
else
|
|
echo "CLI release $VERSION does not exist yet; release.yml will attach the compatibility latest.json when it publishes."
|
|
fi
|
|
|
|
# Stable release moved R2 latest/ — rebuild the site so its build-time baked
|
|
# version + JSON-LD follow (site.js's runtime .rxv refresh can't touch first paint / SEO).
|
|
- name: Refresh site to the new version
|
|
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel != 'canary' && steps.ver.outputs.prerelease != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: gh workflow run pages.yml --ref main-v2
|