name: release-beta

on:
  workflow_dispatch:
    inputs:
      signed:
        description: "Build signed/notarized mac artifacts. Disable only for explicit unsigned validation releases."
        required: true
        type: boolean
        default: true
      enable_mac:
        description: "Build and publish mac arm64 beta artifacts."
        required: true
        type: boolean
        default: true
      enable_win:
        description: "Build and publish Windows x64 beta artifacts."
        required: true
        type: boolean
        default: true
      enable_linux:
        description: "Build and publish Linux x64 AppImage beta artifacts."
        required: true
        type: boolean
        default: false

permissions:
  contents: write

concurrency:
  group: open-design-release-beta
  cancel-in-progress: false

jobs:
  metadata:
    name: Prepare beta metadata
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ github.token }}
      GITHUB_REPOSITORY: ${{ github.repository }}
      OPEN_DESIGN_RELEASE_SIGNED: ${{ inputs.signed }}
    outputs:
      asset_version_suffix: ${{ steps.beta.outputs.asset_version_suffix }}
      base_version: ${{ steps.beta.outputs.base_version }}
      beta_tag: ${{ steps.beta.outputs.beta_tag }}
      beta_version: ${{ steps.beta.outputs.beta_version }}
      branch: ${{ steps.beta.outputs.branch }}
      commit: ${{ steps.beta.outputs.commit }}
      release_name: ${{ steps.beta.outputs.release_name }}
      signed: ${{ steps.beta.outputs.signed }}
      version_tag: ${{ steps.beta.outputs.version_tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24

      - name: Prepare beta release metadata
        id: beta
        run: node --experimental-strip-types ./scripts/release-beta.ts

  build_mac:
    name: Build beta mac arm64
    needs: metadata
    if: ${{ inputs.enable_mac }}
    runs-on: macos-14
    env:
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6.0.2
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v5
        with:
          version: 10.33.2

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Apply beta package version
        run: npm pkg set "version=${{ needs.metadata.outputs.beta_version }}" --prefix apps/packaged

      - name: Prepare Apple signing certificate
        if: ${{ inputs.signed }}
        env:
          APPLE_SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }}
          APPLE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
        run: |
          set -euo pipefail
          cert_path="$RUNNER_TEMP/open-design-signing.p12"
          if ! printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 --decode > "$cert_path" 2>/dev/null; then
            printf '%s' "$APPLE_SIGNING_CERTIFICATE_BASE64" | base64 -D > "$cert_path"
          fi
          {
            echo "CSC_LINK=$cert_path"
            echo "CSC_KEY_PASSWORD=$APPLE_SIGNING_CERTIFICATE_PASSWORD"
          } >> "$GITHUB_ENV"

      - name: Build beta mac artifacts
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: |
          set -euo pipefail
          signed_flag=""
          if [ "${{ inputs.signed }}" = "true" ]; then
            signed_flag="--signed"
          fi
          pnpm exec tools-pack mac build \
            --dir "$RUNNER_TEMP/tools-pack" \
            --namespace release-beta \
            --portable \
            --mac-compression normal \
            --to all \
            --json \
            $signed_flag

      - name: Smoke beta mac packaged runtime
        working-directory: e2e
        env:
          OD_PACKAGED_E2E_MAC: "1"
          OD_PACKAGED_E2E_NAMESPACE: release-beta
          OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack
        run: pnpm test specs/mac.spec.ts

      - name: Prepare beta assets
        id: assets
        run: |
          set -euo pipefail
          release_dir="$RUNNER_TEMP/release-assets"
          mkdir -p "$release_dir"

          source_dmg="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-beta/dmg/Open Design-release-beta.dmg"
          source_zip="$RUNNER_TEMP/tools-pack/out/mac/namespaces/release-beta/zip/Open Design-release-beta.zip"
          if [ ! -f "$source_dmg" ]; then
            echo "expected dmg not found at $source_dmg" >&2
            exit 1
          fi
          if [ ! -f "$source_zip" ]; then
            echo "expected zip not found at $source_zip" >&2
            exit 1
          fi

          asset_suffix="${{ needs.metadata.outputs.asset_version_suffix }}"
          versioned_dmg="open-design-${{ needs.metadata.outputs.beta_version }}${asset_suffix}-mac-arm64.dmg"
          versioned_zip="open-design-${{ needs.metadata.outputs.beta_version }}${asset_suffix}-mac-arm64.zip"
          dmg_checksum_file="$versioned_dmg.sha256"
          zip_checksum_file="$versioned_zip.sha256"

          cp "$source_dmg" "$release_dir/$versioned_dmg"
          cp "$source_zip" "$release_dir/$versioned_zip"
          (
            cd "$release_dir"
            shasum -a 256 "$versioned_dmg" > "$dmg_checksum_file"
            shasum -a 256 "$versioned_zip" > "$zip_checksum_file"
          )

          zip_sha512="$(openssl dgst -sha512 -binary "$release_dir/$versioned_zip" | openssl base64 -A)"
          zip_size="$(stat -f%z "$release_dir/$versioned_zip")"
          zip_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ needs.metadata.outputs.version_tag }}/$versioned_zip"
          release_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
          cat > "$release_dir/latest-mac.yml" <<EOF
          version: "${{ needs.metadata.outputs.beta_version }}"
          files:
            - url: "$zip_url"
              sha512: "$zip_sha512"
              size: $zip_size
          path: "$zip_url"
          sha512: "$zip_sha512"
          releaseDate: "$release_date"
          releaseNotes: "Open Design beta ${{ needs.metadata.outputs.beta_version }}${asset_suffix}"
          EOF

      - name: Upload mac release bundle
        uses: actions/upload-artifact@v7
        with:
          name: open-design-beta-mac-release-assets
          path: ${{ runner.temp }}/release-assets

  build_win:
    name: Build beta win x64
    needs: metadata
    if: ${{ inputs.enable_win }}
    runs-on: windows-latest
    env:
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6.0.2
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v5
        with:
          version: 10.33.2

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Apply beta package version
        run: npm pkg set "version=${{ needs.metadata.outputs.beta_version }}" --prefix apps/packaged

      - name: Build beta windows artifacts
        shell: pwsh
        run: >-
          pnpm exec tools-pack win build
          --dir "${{ runner.temp }}/tools-pack"
          --namespace release-beta-win
          --portable
          --to nsis
          --json

      - name: Prepare windows beta assets
        shell: pwsh
        run: |
          $releaseDir = Join-Path $env:RUNNER_TEMP "release-assets"
          New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null

          $sourceInstaller = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-beta-win/builder/Open Design-release-beta-win-setup.exe"
          $sourceBlockmap = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/release-beta-win/builder/Open Design-release-beta-win-setup.exe.blockmap"
          if (!(Test-Path $sourceInstaller)) {
            throw "expected installer not found at $sourceInstaller"
          }
          if (!(Test-Path $sourceBlockmap)) {
            throw "expected blockmap not found at $sourceBlockmap"
          }

          $windowsAssetSuffix = ".unsigned"
          $versionedInstaller = "open-design-${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix-win-x64-setup.exe"
          $versionedBlockmap = "open-design-${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix-win-x64-setup.exe.blockmap"
          $checksumFile = "$versionedInstaller.sha256"
          Copy-Item $sourceInstaller (Join-Path $releaseDir $versionedInstaller)
          Copy-Item $sourceBlockmap (Join-Path $releaseDir $versionedBlockmap)

          $installerPath = Join-Path $releaseDir $versionedInstaller
          $hash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLowerInvariant()
          "$hash  $versionedInstaller" | Set-Content -Path (Join-Path $releaseDir $checksumFile)
          $installerBytes = [System.IO.File]::ReadAllBytes($installerPath)
          $installerSha512 = [System.Convert]::ToBase64String([System.Security.Cryptography.SHA512]::Create().ComputeHash($installerBytes))
          $installerSize = (Get-Item $installerPath).Length
          $installerUrl = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/${{ needs.metadata.outputs.version_tag }}/$versionedInstaller"
          $releaseDate = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
          @(
            'version: "${{ needs.metadata.outputs.beta_version }}"'
            'files:'
            "  - url: `"$installerUrl`""
            "    sha512: `"$installerSha512`""
            "    size: $installerSize"
            "path: `"$installerUrl`""
            "sha512: `"$installerSha512`""
            "releaseDate: `"$releaseDate`""
            "releaseNotes: `"Open Design beta ${{ needs.metadata.outputs.beta_version }}$windowsAssetSuffix`""
          ) | Set-Content -Path (Join-Path $releaseDir "latest.yml")

      - name: Upload windows release bundle
        uses: actions/upload-artifact@v7
        with:
          name: open-design-beta-win-release-assets
          path: ${{ runner.temp }}/release-assets

  build_linux:
    name: Build beta linux x64
    needs: metadata
    if: ${{ inputs.enable_linux }}
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6.0.2
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v5
        with:
          version: 10.33.2

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Apply beta package version
        env:
          BETA_VERSION: ${{ needs.metadata.outputs.beta_version }}
        run: npm pkg set "version=$BETA_VERSION" --prefix apps/packaged

      # `--containerized` builds the AppImage inside the electronuserland/builder
      # Docker image (glibc 2.27 baseline) so the resulting binary runs on older
      # distros than ubuntu-latest's glibc 2.39. Docker is preinstalled on the
      # GitHub-hosted ubuntu-latest runner, so no extra setup is required.
      - name: Build beta linux artifacts
        run: |
          set -euo pipefail
          pnpm exec tools-pack linux build \
            --dir "$RUNNER_TEMP/tools-pack" \
            --namespace release-beta-linux \
            --portable \
            --to appimage \
            --containerized \
            --json

      - name: Prepare linux beta assets
        env:
          BETA_VERSION: ${{ needs.metadata.outputs.beta_version }}
        run: |
          set -euo pipefail
          release_dir="$RUNNER_TEMP/release-assets"
          mkdir -p "$release_dir"

          source_appimage="$RUNNER_TEMP/tools-pack/out/linux/namespaces/release-beta-linux/builder/Open Design-release-beta-linux.AppImage"
          if [ ! -f "$source_appimage" ]; then
            echo "expected AppImage not found at $source_appimage" >&2
            exit 1
          fi

          # Linux currently has no signing path in tools-pack, so the suffix is
          # hardcoded to .unsigned (matching the windows convention above).
          linux_asset_suffix=".unsigned"
          versioned_appimage="open-design-${BETA_VERSION}${linux_asset_suffix}-linux-x64.AppImage"
          checksum_file="$versioned_appimage.sha256"

          cp "$source_appimage" "$release_dir/$versioned_appimage"
          (
            cd "$release_dir"
            sha256sum "$versioned_appimage" > "$checksum_file"
          )

      - name: Upload linux release bundle
        uses: actions/upload-artifact@v7
        with:
          name: open-design-beta-linux-release-assets
          path: ${{ runner.temp }}/release-assets

  publish:
    name: Publish beta release
    needs:
      - metadata
      - build_mac
      - build_win
      - build_linux
    if: >-
      ${{
        always() &&
        !cancelled() &&
        needs.metadata.result == 'success' &&
        (inputs.enable_mac || inputs.enable_win || inputs.enable_linux) &&
        (!inputs.enable_mac || needs.build_mac.result == 'success') &&
        (!inputs.enable_win || needs.build_win.result == 'success') &&
        (!inputs.enable_linux || needs.build_linux.result == 'success')
      }}
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ github.token }}
      ENABLE_MAC: ${{ inputs.enable_mac }}
      ENABLE_WIN: ${{ inputs.enable_win }}
      ENABLE_LINUX: ${{ inputs.enable_linux }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6.0.2
        with:
          fetch-depth: 0

      - name: Download mac release bundle
        if: ${{ inputs.enable_mac }}
        uses: actions/download-artifact@v8
        with:
          name: open-design-beta-mac-release-assets
          path: ${{ runner.temp }}/release-assets/mac

      - name: Download windows release bundle
        if: ${{ inputs.enable_win }}
        uses: actions/download-artifact@v8
        with:
          name: open-design-beta-win-release-assets
          path: ${{ runner.temp }}/release-assets/win

      - name: Download linux release bundle
        if: ${{ inputs.enable_linux }}
        uses: actions/download-artifact@v8
        with:
          name: open-design-beta-linux-release-assets
          path: ${{ runner.temp }}/release-assets/linux

      - name: Move beta tags to current commit
        run: |
          set -euo pipefail
          git tag -f "${{ needs.metadata.outputs.version_tag }}" "$GITHUB_SHA"
          git push origin "refs/tags/${{ needs.metadata.outputs.version_tag }}" --force
          git tag -f "${{ needs.metadata.outputs.beta_tag }}" "$GITHUB_SHA"
          git push origin "refs/tags/${{ needs.metadata.outputs.beta_tag }}" --force

      - name: Write release notes
        id: notes
        run: |
          set -euo pipefail
          version_notes_file="$RUNNER_TEMP/open-design-beta-version-notes.md"
          latest_notes_file="$RUNNER_TEMP/open-design-beta-latest-notes.md"
          cat > "$version_notes_file" <<EOF
          ## Summary
          - channel: beta
          - version: ${{ needs.metadata.outputs.beta_version }}
          - base version: ${{ needs.metadata.outputs.base_version }}
          - mac enabled: ${{ inputs.enable_mac }}
          - mac signed/notarized: ${{ needs.metadata.outputs.signed }}
          - windows enabled: ${{ inputs.enable_win }}
          - windows signed: false
          - linux enabled: ${{ inputs.enable_linux }}
          - branch: ${{ needs.metadata.outputs.branch }}
          - commit: ${{ needs.metadata.outputs.commit }}

          This beta release ships the enabled platform artifacts, checksums, and updater feed files for enabled auto-update platforms. Linux AppImage has no auto-update feed yet.
          EOF
          cat > "$latest_notes_file" <<EOF
          ## Summary
          - channel: beta
          - latest version: ${{ needs.metadata.outputs.beta_version }}
          - latest tag: ${{ needs.metadata.outputs.version_tag }}

          This release is the mutable beta channel feed carrier. It should contain enabled feed assets only: latest-mac.yml and/or latest.yml. If neither mac nor Windows is enabled, no feed assets are expected.
          EOF
          {
            echo "version_notes_file=$version_notes_file"
            echo "latest_notes_file=$latest_notes_file"
          } >> "$GITHUB_OUTPUT"

      - name: Create or update immutable beta prerelease
        run: |
          set -euo pipefail
          all_release_dir="$RUNNER_TEMP/release-assets/all"
          mkdir -p "$all_release_dir"
          for asset_dir in "$RUNNER_TEMP/release-assets/mac" "$RUNNER_TEMP/release-assets/win" "$RUNNER_TEMP/release-assets/linux"; do
            if [ -d "$asset_dir" ] && compgen -G "$asset_dir/*" > /dev/null; then
              cp "$asset_dir"/* "$all_release_dir/"
            fi
          done
          if ! compgen -G "$all_release_dir/*" > /dev/null; then
            echo "no enabled beta release assets were found" >&2
            exit 1
          fi
          declare -A current_release_assets=()
          for asset_path in "$all_release_dir"/*; do
            current_release_assets["$(basename "$asset_path")"]=1
          done
          if gh release view "${{ needs.metadata.outputs.version_tag }}" >/dev/null 2>&1; then
            gh release edit "${{ needs.metadata.outputs.version_tag }}" \
              --title "${{ needs.metadata.outputs.release_name }}" \
              --notes-file "${{ steps.notes.outputs.version_notes_file }}" \
              --prerelease
          else
            gh release create "${{ needs.metadata.outputs.version_tag }}" \
              --target "$GITHUB_SHA" \
              --title "${{ needs.metadata.outputs.release_name }}" \
              --notes-file "${{ steps.notes.outputs.version_notes_file }}" \
              --prerelease
          fi
          gh release upload "${{ needs.metadata.outputs.version_tag }}" "$all_release_dir"/* --clobber
          while IFS= read -r asset_name; do
            if [ -n "$asset_name" ] && [ -z "${current_release_assets[$asset_name]+x}" ]; then
              gh release delete-asset "${{ needs.metadata.outputs.version_tag }}" "$asset_name" --yes
            fi
          done < <(gh release view "${{ needs.metadata.outputs.version_tag }}" --json assets --jq '.assets[].name')

      - name: Create or update beta channel feed
        run: |
          set -euo pipefail
          latest_mac_path="$RUNNER_TEMP/release-assets/mac/latest-mac.yml"
          latest_win_path="$RUNNER_TEMP/release-assets/win/latest.yml"
          feed_assets=()
          if [ "$ENABLE_MAC" = "true" ]; then
            if [ ! -f "$latest_mac_path" ]; then
              echo "expected mac feed not found at $latest_mac_path" >&2
              exit 1
            fi
            feed_assets+=("$latest_mac_path")
          fi
          if [ "$ENABLE_WIN" = "true" ]; then
            if [ ! -f "$latest_win_path" ]; then
              echo "expected windows feed not found at $latest_win_path" >&2
              exit 1
            fi
            feed_assets+=("$latest_win_path")
          fi
          declare -A current_feed_assets=()
          for feed_asset in "${feed_assets[@]}"; do
            current_feed_assets["$(basename "$feed_asset")"]=1
          done
          if gh release view "${{ needs.metadata.outputs.beta_tag }}" >/dev/null 2>&1; then
            gh release edit "${{ needs.metadata.outputs.beta_tag }}" \
              --title "Open Design Beta Latest" \
              --notes-file "${{ steps.notes.outputs.latest_notes_file }}" \
              --prerelease
          else
            gh release create "${{ needs.metadata.outputs.beta_tag }}" \
              --target "$GITHUB_SHA" \
              --title "Open Design Beta Latest" \
              --notes-file "${{ steps.notes.outputs.latest_notes_file }}" \
              --prerelease
          fi
          if [ "${#feed_assets[@]}" -gt 0 ]; then
            gh release upload "${{ needs.metadata.outputs.beta_tag }}" "${feed_assets[@]}" --clobber
          fi
          while IFS= read -r asset_name; do
            if [ -n "$asset_name" ] && [ -z "${current_feed_assets[$asset_name]+x}" ]; then
              gh release delete-asset "${{ needs.metadata.outputs.beta_tag }}" "$asset_name" --yes
            fi
          done < <(gh release view "${{ needs.metadata.outputs.beta_tag }}" --json assets --jq '.assets[].name')

      - name: Publish summary
        run: |
          {
            echo "## Beta release"
            echo "- Channel: beta"
            echo "- Version: ${{ needs.metadata.outputs.beta_version }}"
            echo "- Version tag: ${{ needs.metadata.outputs.version_tag }}"
            echo "- Channel feed tag: ${{ needs.metadata.outputs.beta_tag }}"
            echo "- mac enabled: $ENABLE_MAC"
            echo "- mac signed/notarized: ${{ needs.metadata.outputs.signed }}"
            echo "- windows enabled: $ENABLE_WIN"
            echo "- windows signed: false"
            echo "- linux enabled: $ENABLE_LINUX"
            if [ "$ENABLE_MAC" = "true" ]; then
              echo "- mac assets: open-design-${{ needs.metadata.outputs.beta_version }}${{ needs.metadata.outputs.asset_version_suffix }}-mac-arm64.dmg, open-design-${{ needs.metadata.outputs.beta_version }}${{ needs.metadata.outputs.asset_version_suffix }}-mac-arm64.zip"
            fi
            if [ "$ENABLE_WIN" = "true" ]; then
              echo "- win assets: open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-win-x64-setup.exe, open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-win-x64-setup.exe.blockmap"
            fi
            if [ "$ENABLE_LINUX" = "true" ]; then
              echo "- linux assets: open-design-${{ needs.metadata.outputs.beta_version }}.unsigned-linux-x64.AppImage"
            fi
            echo "- Feeds: enabled mac/win feeds only (no latest-linux.yml; AppImage updater not yet wired)"
          } >> "$GITHUB_STEP_SUMMARY"
