486 lines
22 KiB
YAML
486 lines
22 KiB
YAML
name: Release desktop
|
|
|
|
# Desktop (Wails) release line. Tag namespace `desktop-v<semver>` triggers this —
|
|
# plain `v<semver>` tags are the CLI release (release.yml) and intentionally do NOT
|
|
# trigger here (GitHub glob `v*` does not match `desktop-v*`). Bump with:
|
|
# git tag desktop-vX.Y.Z && git push origin desktop-vX.Y.Z
|
|
#
|
|
# 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. A `desktop-v*` tag
|
|
# (or manual stable dispatch) 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
|
|
|
|
permissions:
|
|
contents: write # create the release and upload artifacts
|
|
|
|
jobs:
|
|
cache-guard:
|
|
name: cache hit guard
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- 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
|
|
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
|
|
|
|
- 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/nfpm@v2.46.3
|
|
|
|
# Windows: NSIS provides makensis for `wails build -nsis`.
|
|
- name: Install NSIS
|
|
if: runner.os == 'Windows'
|
|
run: |
|
|
choco install nsis -y --no-progress
|
|
echo "C:\Program Files (x86)\NSIS" >> "$GITHUB_PATH"
|
|
|
|
# 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.
|
|
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
|
|
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/wails@v2.12.0
|
|
|
|
- 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 — until that cert clears CSR-pending,
|
|
# stable ships unsigned instead of failing the whole release on an invalid policy.
|
|
- 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
|
|
# The policy uses an approval process — leave headroom for a human to approve
|
|
# the request (the approver gets an email on submit) before the job gives up.
|
|
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
|
|
runs-on: ubuntu-latest
|
|
# Stable publishes go through the `release` environment (esengine must
|
|
# approve); canary uses the open `canary` environment so maintainers self-serve.
|
|
environment: ${{ (github.event_name == 'workflow_dispatch' && inputs.channel == 'canary') && 'canary' || 'release' }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-go@v6
|
|
with:
|
|
go-version-file: desktop/go.mod
|
|
cache: true
|
|
cache-dependency-path: desktop/go.sum
|
|
|
|
- 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: 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 }}" --generate-notes)
|
|
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: ${{ 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
|
|
|
|
- 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 }}
|
|
|
|
# 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/#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
|