name: Release on: push: tags: - 'v*' workflow_dispatch: permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm - name: Install dependencies run: pnpm install --no-frozen-lockfile - name: Set version from tag if: startsWith(github.ref, 'refs/tags/v') run: | VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" - name: Build app env: NODE_OPTIONS: '--max-old-space-size=8192' SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: quant-jump-pro SENTRY_PROJECT: electron run: pnpm build - name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" gh release create "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --title "$TAG" \ --generate-notes \ --draft=false 2>/dev/null || echo "Release $TAG already exists, skipping creation" - name: Upload dist artifact uses: actions/upload-artifact@v6 with: name: dist path: | out/renderer dist-electron retention-days: 1 prepare-runtime: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout uses: actions/checkout@v6 - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" gh release create "$TAG" \ --repo "$GITHUB_REPOSITORY" \ --title "$TAG" \ --generate-notes \ --draft=false 2>/dev/null || echo "Release $TAG already exists, skipping creation" - name: Check runtime assets id: runtime-assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/}" existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)" missing=0 while IFS= read -r asset; do [ -n "$asset" ] || continue if ! grep -Fqx "$asset" <<<"$existing"; then echo "Missing runtime asset: $asset" missing=1 fi done < <(node ./scripts/runtime-lock.mjs asset-list) echo "missing=$missing" >> "$GITHUB_OUTPUT" - name: Dispatch private runtime build if: steps.runtime-assets.outputs.missing == '1' env: GH_TOKEN: ${{ secrets.RUNTIME_BUILD_DISPATCH_TOKEN }} run: | set -euo pipefail TARGET_TAG="${GITHUB_REF#refs/tags/}" SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)" SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)" RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)" gh api \ --method POST \ "repos/${SOURCE_REPO}/actions/workflows/release-runtime.yml/dispatches" \ -f ref=main \ -f inputs[source_ref]="$SOURCE_REF" \ -f inputs[runtime_version]="$RUNTIME_VERSION" \ -f inputs[target_release_repo]="$GITHUB_REPOSITORY" \ -f inputs[target_release_tag]="$TARGET_TAG" - name: Wait for runtime assets if: steps.runtime-assets.outputs.missing == '1' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/}" for attempt in $(seq 1 60); do existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)" all_found=1 while IFS= read -r asset; do [ -n "$asset" ] || continue if ! grep -Fqx "$asset" <<<"$existing"; then all_found=0 break fi done < <(node ./scripts/runtime-lock.mjs asset-list) if [ "$all_found" -eq 1 ]; then echo "Runtime assets are ready" exit 0 fi echo "Waiting for runtime assets - attempt $attempt/60" sleep 10 done echo "Timed out waiting for runtime assets in release $TAG" >&2 exit 1 release-mac: needs: [build, prepare-runtime] strategy: fail-fast: false matrix: include: - arch: arm64 runner: macos-14 dist_command: pnpm dist:mac:arm64 - arch: x64 runner: macos-15-intel dist_command: pnpm dist:mac:x64 runs-on: ${{ matrix.runner }} steps: - name: Checkout uses: actions/checkout@v6 - name: Download dist artifact uses: actions/download-artifact@v8 with: name: dist - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm - name: Setup Python for node-gyp uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: pnpm install --no-frozen-lockfile - name: Set version from tag if: startsWith(github.ref, 'refs/tags/v') run: | VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" - name: Resolve runtime asset name (macOS ${{ matrix.arch }}) if: startsWith(github.ref, 'refs/tags/v') id: runtime-asset shell: bash run: | set -euo pipefail if [[ "${{ matrix.arch }}" == "arm64" ]]; then platform="darwin-arm64" else platform="darwin-x64" fi echo "asset_name=$(node ./scripts/runtime-lock.mjs asset-name "$platform")" >> "$GITHUB_OUTPUT" - name: Stage bundled runtime (macOS ${{ matrix.arch }}) if: startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/}" rm -rf .runtime-download resources/runtime mkdir -p .runtime-download resources/runtime gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "${{ steps.runtime-asset.outputs.asset_name }}" --dir .runtime-download tar -xzf ".runtime-download/${{ steps.runtime-asset.outputs.asset_name }}" -C .runtime-download cp -R .runtime-download/runtime/. resources/runtime/ - name: Build app (macOS ${{ matrix.arch }}) env: NODE_OPTIONS: '--max-old-space-size=8192' SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: quant-jump-pro SENTRY_PROJECT: electron run: pnpm build - name: Verify packaged inputs (macOS ${{ matrix.arch }}) run: | test -f dist-electron/main/index.cjs test -f dist-electron/preload/index.js test -f out/renderer/index.html test -f mcp-server/dist/index.js if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then test -f resources/runtime/VERSION fi - name: Package (macOS ${{ matrix.arch }}) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: ${{ matrix.dist_command }} --publish never - name: Validate packaged bundle (macOS ${{ matrix.arch }}) run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Claude Agent Teams UI.app" darwin ${{ matrix.arch }} - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" for f in release/*.dmg release/*.zip release/*.blockmap; do [ -f "$f" ] || continue echo "Uploading: $f" gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1 || \ (sleep 5 && gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1) || \ echo "WARNING: failed to upload $f, continuing..." done release-win: needs: [build, prepare-runtime] runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Download dist artifact uses: actions/download-artifact@v8 with: name: dist - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm - name: Setup Python for node-gyp uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: pnpm install --no-frozen-lockfile - name: Set version from tag if: startsWith(github.ref, 'refs/tags/v') shell: bash run: | VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" - name: Resolve runtime asset name (Windows) if: startsWith(github.ref, 'refs/tags/v') id: runtime-asset shell: bash run: | echo "asset_name=$(node ./scripts/runtime-lock.mjs asset-name win32-x64)" >> "$GITHUB_OUTPUT" - name: Stage bundled runtime (Windows) if: startsWith(github.ref, 'refs/tags/v') shell: pwsh env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | $ErrorActionPreference = "Stop" $tag = $env:GITHUB_REF.Replace('refs/tags/', '') Remove-Item .runtime-download -Recurse -Force -ErrorAction SilentlyContinue Remove-Item resources/runtime/* -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path .runtime-download | Out-Null New-Item -ItemType Directory -Force -Path resources/runtime | Out-Null gh release download $tag --repo $env:GITHUB_REPOSITORY --pattern "${{ steps.runtime-asset.outputs.asset_name }}" --dir .runtime-download Expand-Archive -Path ".runtime-download/${{ steps.runtime-asset.outputs.asset_name }}" -DestinationPath .runtime-download/unpacked -Force Copy-Item .runtime-download/unpacked/runtime/* resources/runtime -Recurse -Force - name: Build app (Windows) env: NODE_OPTIONS: '--max-old-space-size=8192' SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: quant-jump-pro SENTRY_PROJECT: electron run: pnpm build - name: Verify packaged inputs (Windows) shell: bash run: | test -f dist-electron/main/index.cjs test -f dist-electron/preload/index.js test -f out/renderer/index.html test -f mcp-server/dist/index.js if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then test -f resources/runtime/VERSION fi - name: Package (Windows) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm dist:win --publish never - name: Validate packaged bundle (Windows) shell: bash run: node ./scripts/electron-builder/verifyBundle.cjs "release/win-unpacked" win32 x64 - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" for f in release/*.exe release/*.blockmap; do [ -f "$f" ] || continue echo "Uploading: $f" gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1 || \ (sleep 5 && gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1) || \ echo "WARNING: failed to upload $f, continuing..." done release-linux: needs: [build, prepare-runtime] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Download dist artifact uses: actions/download-artifact@v8 with: name: dist - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm - name: Setup Python for node-gyp uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install Linux packaging dependencies run: | sudo apt-get update sudo apt-get install -y libarchive-tools rpm - name: Install dependencies run: pnpm install --no-frozen-lockfile - name: Set version from tag if: startsWith(github.ref, 'refs/tags/v') run: | VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" - name: Resolve runtime asset name (Linux) if: startsWith(github.ref, 'refs/tags/v') id: runtime-asset shell: bash run: | echo "asset_name=$(node ./scripts/runtime-lock.mjs asset-name linux-x64)" >> "$GITHUB_OUTPUT" - name: Stage bundled runtime (Linux) if: startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/}" rm -rf .runtime-download resources/runtime mkdir -p .runtime-download resources/runtime gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "${{ steps.runtime-asset.outputs.asset_name }}" --dir .runtime-download tar -xzf ".runtime-download/${{ steps.runtime-asset.outputs.asset_name }}" -C .runtime-download cp -R .runtime-download/runtime/. resources/runtime/ - name: Build app (Linux) env: NODE_OPTIONS: '--max-old-space-size=8192' SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: quant-jump-pro SENTRY_PROJECT: electron run: pnpm build - name: Verify packaged inputs (Linux) run: | test -f dist-electron/main/index.cjs test -f dist-electron/preload/index.js test -f out/renderer/index.html test -f mcp-server/dist/index.js if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then test -f resources/runtime/VERSION fi - name: Package (Linux) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: pnpm dist:linux --publish never - name: Validate packaged bundle (Linux) run: node ./scripts/electron-builder/verifyBundle.cjs "release/linux-unpacked" linux x64 - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -x TAG="${GITHUB_REF#refs/tags/}" ls -la release/ || true for f in release/*.AppImage release/*.deb release/*.rpm release/*.pacman release/*.blockmap; do [ -f "$f" ] || continue echo "Uploading: $f" gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1 || \ (sleep 5 && gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1) || \ echo "WARNING: failed to upload $f, continuing..." done upload-stable-links: needs: [release-mac, release-win, release-linux] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - name: Upload stable-named assets for /latest/download links env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail VERSION="${GITHUB_REF#refs/tags/v}" REPO="${GITHUB_REPOSITORY}" DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/v${VERSION}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT declare -A FILES=( ["Claude-Agent-Teams-UI-arm64.dmg"]="Claude.Agent.Teams.UI-${VERSION}-arm64.dmg" ["Claude-Agent-Teams-UI-x64.dmg"]="Claude.Agent.Teams.UI-${VERSION}.dmg" ["Claude-Agent-Teams-UI-Setup.exe"]="Claude.Agent.Teams.UI.Setup.${VERSION}.exe" ["Claude-Agent-Teams-UI.AppImage"]="Claude.Agent.Teams.UI-${VERSION}.AppImage" ["Claude-Agent-Teams-UI-amd64.deb"]="claude-agent-teams-ui_${VERSION}_amd64.deb" ["Claude-Agent-Teams-UI-x86_64.rpm"]="claude-agent-teams-ui-${VERSION}.x86_64.rpm" ["Claude-Agent-Teams-UI.pacman"]="claude-agent-teams-ui-${VERSION}.pacman" ) # Download versioned files and re-upload with stable names for STABLE_NAME in "${!FILES[@]}"; do VERSIONED_NAME="${FILES[$STABLE_NAME]}" echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}" curl -fSL -o "${TMP_DIR}/${VERSIONED_NAME}" "${DOWNLOAD_BASE}/${VERSIONED_NAME}" cp "${TMP_DIR}/${VERSIONED_NAME}" "${TMP_DIR}/${STABLE_NAME}" gh release upload "v${VERSION}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber done - name: Publish canonical updater metadata env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail VERSION="${GITHUB_REF#refs/tags/v}" TAG="v${VERSION}" REPO="${GITHUB_REPOSITORY}" RELEASE_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" TMP_DIR="$(mktemp -d)" cd "$TMP_DIR" sha512_base64() { openssl dgst -sha512 -binary "$1" | openssl base64 -A } file_size() { wc -c < "$1" | tr -d '[:space:]' } download_asset() { local name="$1" curl -fSL -o "$name" "https://github.com/${REPO}/releases/download/${TAG}/${name}" } # Canonical Windows feed download_asset "Claude-Agent-Teams-UI-Setup.exe" WIN_SHA="$(sha512_base64 Claude-Agent-Teams-UI-Setup.exe)" WIN_SIZE="$(file_size Claude-Agent-Teams-UI-Setup.exe)" cat > latest.yml < latest-linux.yml < latest-mac.yml <