name: Release App

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag to release (e.g., v0.1.2). Leave empty to use current ref."
        required: false
        type: string
      release_name:
        description: "Release title (defaults to OpenWork <tag>)"
        required: false
        type: string
      release_body:
        description: "Release notes body in Markdown (defaults to a short placeholder)"
        required: false
        type: string
      draft:
        description: "Create the GitHub Release as a draft"
        required: false
        type: boolean
        default: false
      prerelease:
        description: "Mark the GitHub Release as a prerelease"
        required: false
        type: boolean
        default: false
      notarize:
        description: "Notarize macOS builds (requires Apple team configured)"
        required: false
        type: boolean
        default: false
      publish_sidecars:
        description: "Build + upload openwrk sidecar release assets"
        required: false
        type: boolean
        default: true
      publish_npm:
        description: "Publish openwrk/openwork-server/owpenwork to npm if versions changed"
        required: false
        type: boolean
        default: true

permissions:
  contents: write
  pull-requests: write

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  resolve-release:
    name: Resolve Release Metadata
    runs-on: ubuntu-latest
    outputs:
      release_tag: ${{ steps.resolve.outputs.release_tag }}
      release_name: ${{ steps.resolve.outputs.release_name }}
      release_body: ${{ steps.resolve.outputs.release_body }}
      draft: ${{ steps.resolve.outputs.draft }}
      prerelease: ${{ steps.resolve.outputs.prerelease }}
      notarize: ${{ steps.resolve.outputs.notarize }}
      publish_sidecars: ${{ steps.resolve.outputs.publish_sidecars }}
      publish_npm: ${{ steps.resolve.outputs.publish_npm }}
    steps:
      - name: Resolve metadata
        id: resolve
        shell: bash
        env:
          INPUT_TAG: ${{ github.event.inputs.tag }}
          INPUT_RELEASE_NAME: ${{ github.event.inputs.release_name }}
          INPUT_RELEASE_BODY: ${{ github.event.inputs.release_body }}
          INPUT_DRAFT: ${{ github.event.inputs.draft }}
          INPUT_PRERELEASE: ${{ github.event.inputs.prerelease }}
          INPUT_NOTARIZE: ${{ github.event.inputs.notarize }}
          INPUT_PUBLISH_SIDECARS: ${{ github.event.inputs.publish_sidecars }}
          INPUT_PUBLISH_NPM: ${{ github.event.inputs.publish_npm }}
          DEFAULT_PUBLISH_SIDECARS: ${{ vars.RELEASE_PUBLISH_SIDECARS }}
          DEFAULT_PUBLISH_NPM: ${{ vars.RELEASE_PUBLISH_NPM }}
          DEFAULT_NOTARIZE: ${{ vars.MACOS_NOTARIZE }}
        run: |
          set -euo pipefail

          TAG_INPUT="${INPUT_TAG:-}"
          if [ -n "$TAG_INPUT" ]; then
            if [[ "$TAG_INPUT" == v* ]]; then
              TAG="$TAG_INPUT"
            else
              TAG="v$TAG_INPUT"
            fi
          else
            TAG="${GITHUB_REF_NAME}"
          fi

          if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
            echo "Invalid release tag: $TAG (expected vX.Y.Z)" >&2
            exit 1
          fi

          RELEASE_NAME_INPUT="${INPUT_RELEASE_NAME:-}"
          if [ -n "$RELEASE_NAME_INPUT" ]; then
            RELEASE_NAME="$RELEASE_NAME_INPUT"
          else
            RELEASE_NAME="OpenWork $TAG"
          fi

          RELEASE_BODY_INPUT="${INPUT_RELEASE_BODY:-}"
          if [ -n "$RELEASE_BODY_INPUT" ]; then
            RELEASE_BODY="$RELEASE_BODY_INPUT"
          else
            RELEASE_BODY="See the assets to download this version and install."
          fi

          draft="${INPUT_DRAFT:-false}"
          prerelease="${INPUT_PRERELEASE:-false}"
          notarize="${INPUT_NOTARIZE:-}"
          if [ -z "$notarize" ]; then
            notarize="${DEFAULT_NOTARIZE:-true}"
          fi

          publish_sidecars="${INPUT_PUBLISH_SIDECARS:-}"
          if [ -z "$publish_sidecars" ]; then
            publish_sidecars="${DEFAULT_PUBLISH_SIDECARS:-true}"
          fi
          publish_npm="${INPUT_PUBLISH_NPM:-}"
          if [ -z "$publish_npm" ]; then
            publish_npm="${DEFAULT_PUBLISH_NPM:-true}"
          fi

          TAG="${TAG//$'\n'/}"
          TAG="${TAG//$'\r'/}"
          RELEASE_NAME="${RELEASE_NAME//$'\n'/ }"
          RELEASE_NAME="${RELEASE_NAME//$'\r'/ }"

          echo "release_tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "release_name=$RELEASE_NAME" >> "$GITHUB_OUTPUT"
          echo "draft=$draft" >> "$GITHUB_OUTPUT"
          echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
          echo "notarize=$notarize" >> "$GITHUB_OUTPUT"
          echo "publish_sidecars=$publish_sidecars" >> "$GITHUB_OUTPUT"
          echo "publish_npm=$publish_npm" >> "$GITHUB_OUTPUT"
          {
            echo "release_body<<__OPENWORK_RELEASE_BODY_EOF__"
            printf '%s\n' "$RELEASE_BODY"
            echo "__OPENWORK_RELEASE_BODY_EOF__"
          } >> "$GITHUB_OUTPUT"

      - name: Create release if missing
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail

          BODY_FILE="$RUNNER_TEMP/release_body.md"
          printf '%s\n' "${{ steps.resolve.outputs.release_body }}" > "$BODY_FILE"

          if gh release view "${{ steps.resolve.outputs.release_tag }}" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
            echo "Release ${{ steps.resolve.outputs.release_tag }} already exists; skipping create."
            exit 0
          fi

          DRAFT_FLAG=""
          PRERELEASE_FLAG=""
          if [ "${{ steps.resolve.outputs.draft }}" = "true" ]; then
            DRAFT_FLAG="--draft"
          fi
          if [ "${{ steps.resolve.outputs.prerelease }}" = "true" ]; then
            PRERELEASE_FLAG="--prerelease"
          fi

          gh release create "${{ steps.resolve.outputs.release_tag }}" \
            --repo "$GITHUB_REPOSITORY" \
            --title "${{ steps.resolve.outputs.release_name }}" \
            --notes-file "$BODY_FILE" \
            $DRAFT_FLAG $PRERELEASE_FLAG

  verify-release:
    name: Verify Release Versions
    needs: resolve-release
    runs-on: ubuntu-latest
    env:
      RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ env.RELEASE_TAG }}
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Verify tag matches app versions
        run: node scripts/release/verify-tag.mjs --tag "$RELEASE_TAG"

      - name: Release review (strict)
        run: node scripts/release/review.mjs --strict

  publish-tauri:
    name: Build + Publish (${{ matrix.target }})
    needs: [resolve-release, verify-release]
    runs-on: ${{ matrix.platform }}
    timeout-minutes: 360
    env:
      RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
      RELEASE_NAME: ${{ needs.resolve-release.outputs.release_name }}
      RELEASE_BODY: ${{ needs.resolve-release.outputs.release_body }}
      RELEASE_DRAFT: ${{ needs.resolve-release.outputs.draft }}
      RELEASE_PRERELEASE: ${{ needs.resolve-release.outputs.prerelease }}
      MACOS_NOTARIZE: ${{ needs.resolve-release.outputs.notarize }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: macos-14
            os_type: macos
            target: aarch64-apple-darwin
            args: "--target aarch64-apple-darwin --bundles dmg,app"
          - platform: macos-14
            os_type: macos
            target: x86_64-apple-darwin
            args: "--target x86_64-apple-darwin --bundles dmg,app"
          - platform: ubuntu-22.04
            os_type: linux
            target: x86_64-unknown-linux-gnu
            args: "--target x86_64-unknown-linux-gnu --bundles deb,rpm"
          - platform: windows-2022
            os_type: windows
            target: x86_64-pc-windows-msvc
            args: "--target x86_64-pc-windows-msvc --bundles msi"

    steps:
      - name: Enable git long paths (Windows)
        if: matrix.os_type == 'windows'
        shell: pwsh
        run: git config --global core.longpaths true

      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ env.RELEASE_TAG }}
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10.27.0

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: "1.3.6"

      - name: Get pnpm store path
        id: pnpm-store
        shell: bash
        run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

      - name: Cache pnpm store
        uses: actions/cache@v4
        continue-on-error: true
        with:
          path: ${{ steps.pnpm-store.outputs.path }}
          key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - name: Cache cargo
        uses: actions/cache@v4
        continue-on-error: true
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            packages/desktop/src-tauri/target
          key: ${{ runner.os }}-cargo-${{ hashFiles('packages/desktop/src-tauri/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

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

      - name: Install OpenTUI x64 core (macOS x86_64)
        if: matrix.os_type == 'macos' && matrix.target == 'x86_64-apple-darwin'
        run: pnpm add -w --ignore-workspace-root-check @opentui/core-darwin-x64@0.1.77

      - name: Install Linux build dependencies
        if: matrix.os_type == 'linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libgtk-3-dev \
            libglib2.0-dev \
            libayatana-appindicator3-dev \
            libsoup-3.0-dev \
            libwebkit2gtk-4.1-dev \
            libssl-dev \
            rpm \
            libdbus-1-dev \
            librsvg2-dev

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Resolve OpenCode version
        id: opencode-version
        shell: bash
        run: |
          node -e "const pkg=require('./packages/desktop/package.json'); if (!pkg.opencodeVersion) { throw new Error('opencodeVersion missing'); } console.log('version=' + pkg.opencodeVersion);" >> "$GITHUB_OUTPUT"

      - name: Download OpenCode sidecar
        shell: bash
        env:
          OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
        run: |
          set -euo pipefail

          case "${{ matrix.target }}" in
            aarch64-apple-darwin)
              opencode_asset="opencode-darwin-arm64.zip"
              ;;
            x86_64-apple-darwin)
              opencode_asset="opencode-darwin-x64-baseline.zip"
              ;;
            x86_64-unknown-linux-gnu)
              opencode_asset="opencode-linux-x64-baseline.tar.gz"
              ;;
            x86_64-pc-windows-msvc)
              opencode_asset="opencode-windows-x64-baseline.zip"
              ;;
            *)
              echo "Unsupported target: ${{ matrix.target }}" >&2
              exit 1
              ;;
          esac

          url="https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/${opencode_asset}"
          tmp_dir="$RUNNER_TEMP/opencode"
          extract_dir="$tmp_dir/extracted"
          rm -rf "$tmp_dir"
          mkdir -p "$extract_dir"
          curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 -o "$tmp_dir/$opencode_asset" "$url"

          if [[ "$opencode_asset" == *.tar.gz ]]; then
            tar -xzf "$tmp_dir/$opencode_asset" -C "$extract_dir"
          else
            if command -v unzip >/dev/null 2>&1; then
              unzip -q "$tmp_dir/$opencode_asset" -d "$extract_dir"
            elif command -v 7z >/dev/null 2>&1; then
              7z x "$tmp_dir/$opencode_asset" -o"$extract_dir" >/dev/null
            else
              echo "No unzip utility available" >&2
              exit 1
            fi
          fi

          if [ -f "$extract_dir/opencode" ]; then
            bin_path="$extract_dir/opencode"
          elif [ -f "$extract_dir/opencode.exe" ]; then
            bin_path="$extract_dir/opencode.exe"
          else
            echo "OpenCode binary not found in archive" >&2
            ls -la "$extract_dir"
            exit 1
          fi

          target_name="opencode-${{ matrix.target }}"
          if [ "${{ matrix.os_type }}" = "windows" ]; then
            target_name="${target_name}.exe"
          fi

          mkdir -p packages/desktop/src-tauri/sidecars
          cp "$bin_path" "packages/desktop/src-tauri/sidecars/${target_name}"
          chmod 755 "packages/desktop/src-tauri/sidecars/${target_name}"

      - name: Write notary API key
        if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE == 'true'
        env:
          APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}
        run: |
          set -euo pipefail

          NOTARY_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
          printf '%s' "$APPLE_NOTARY_API_KEY_P8_BASE64" | base64 --decode > "$NOTARY_KEY_PATH"
          chmod 600 "$NOTARY_KEY_PATH"

          echo "NOTARY_KEY_PATH=$NOTARY_KEY_PATH" >> "$GITHUB_ENV"

      - name: Build + upload (notarized)
        if: matrix.os_type == 'macos' && env.MACOS_NOTARIZE == 'true'
        uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
        env:
          CI: true
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

          # Tauri updater signing
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}

          # macOS signing
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}

          # macOS notarization (App Store Connect API key)
          APPLE_API_KEY: ${{ secrets.APPLE_NOTARY_API_KEY_ID }}
          APPLE_API_ISSUER: ${{ secrets.APPLE_NOTARY_API_ISSUER_ID }}
          APPLE_API_KEY_PATH: ${{ env.NOTARY_KEY_PATH }}
        with:
          tagName: ${{ env.RELEASE_TAG }}
          releaseName: ${{ env.RELEASE_NAME }}
          releaseBody: ${{ env.RELEASE_BODY }}
          releaseDraft: ${{ env.RELEASE_DRAFT == 'true' }}
          prerelease: ${{ env.RELEASE_PRERELEASE == 'true' }}
          projectPath: packages/desktop
          tauriScript: pnpm exec tauri -vvv
          args: ${{ matrix.args }}
          retryAttempts: 3
          uploadUpdaterJson: true
          updaterJsonPreferNsis: true
          releaseAssetNamePattern: openwork-desktop-[platform]-[arch][ext]

      - name: Build + upload
        if: matrix.os_type != 'macos' || env.MACOS_NOTARIZE != 'true'
        uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
        env:
          CI: true
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

          # Tauri updater signing
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}

          # macOS signing
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
        with:
          tagName: ${{ env.RELEASE_TAG }}
          releaseName: ${{ env.RELEASE_NAME }}
          releaseBody: ${{ env.RELEASE_BODY }}
          releaseDraft: ${{ env.RELEASE_DRAFT == 'true' }}
          prerelease: ${{ env.RELEASE_PRERELEASE == 'true' }}
          projectPath: packages/desktop
          tauriScript: pnpm exec tauri -vvv
          args: ${{ matrix.args }}
          retryAttempts: 3
          uploadUpdaterJson: true
          updaterJsonPreferNsis: true
          releaseAssetNamePattern: openwork-desktop-[platform]-[arch][ext]

  release-openwrk-sidecars:
    name: Build + Upload openwrk Sidecars
    needs: [resolve-release, verify-release]
    if: needs.resolve-release.outputs.publish_sidecars == 'true'
    runs-on: ubuntu-latest
    env:
      RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ env.RELEASE_TAG }}
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10.27.0

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: "1.3.6"

      - name: Get pnpm store path
        id: pnpm-store
        shell: bash
        run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

      - name: Cache pnpm store
        uses: actions/cache@v4
        continue-on-error: true
        with:
          path: ${{ steps.pnpm-store.outputs.path }}
          key: ubuntu-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            ubuntu-pnpm-

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

      - name: Resolve sidecar versions
        id: sidecar-versions
        shell: bash
        run: |
          node -e "const fs=require('fs'); const headless=JSON.parse(fs.readFileSync('packages/headless/package.json','utf8')); const server=JSON.parse(fs.readFileSync('packages/server/package.json','utf8')); const owpenbot=JSON.parse(fs.readFileSync('packages/owpenbot/package.json','utf8')); console.log('openwrk=' + headless.version); console.log('server=' + server.version); console.log('owpenbot=' + owpenbot.version);" >> "$GITHUB_OUTPUT"

      - name: Resolve SOURCE_DATE_EPOCH
        id: source-date
        shell: bash
        run: |
          epoch=$(git show -s --format=%ct "${RELEASE_TAG}")
          echo "epoch=$epoch" >> "$GITHUB_OUTPUT"

      - name: Check openwrk release
        id: openwrk-release
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          tag="openwrk-v${{ steps.sidecar-versions.outputs.openwrk }}"
          if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
            echo "exists=true" >> "$GITHUB_OUTPUT"
          else
            echo "exists=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Build sidecar artifacts
        env:
          SOURCE_DATE_EPOCH: ${{ steps.source-date.outputs.epoch }}
        run: pnpm --filter openwrk build:sidecars

      - name: Release review (strict)
        env:
          SOURCE_DATE_EPOCH: ${{ steps.source-date.outputs.epoch }}
        run: node scripts/release/review.mjs --strict

      - name: Create openwrk release
        if: steps.openwrk-release.outputs.exists != 'true'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          version="${{ steps.sidecar-versions.outputs.openwrk }}"
          tag="openwrk-v${version}"
          notes="Sidecar bundle for openwrk v${version}.\n\nopenwork-server: ${{ steps.sidecar-versions.outputs.server }}\nowpenbot: ${{ steps.sidecar-versions.outputs.owpenbot }}"
          gh release create "$tag" \
            --repo "$GITHUB_REPOSITORY" \
            --title "openwrk v${version}" \
            --notes "$notes"

      - name: Upload sidecar assets
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          tag="openwrk-v${{ steps.sidecar-versions.outputs.openwrk }}"
          gh release upload "$tag" packages/headless/dist/sidecars/* --repo "$GITHUB_REPOSITORY" --clobber

  publish-npm:
    name: Publish npm packages
    needs: [resolve-release, verify-release]
    if: needs.resolve-release.outputs.publish_npm == 'true'
    runs-on: ubuntu-latest
    env:
      RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ env.RELEASE_TAG }}
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10.27.0

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: "1.3.6"

      - name: Get pnpm store path
        id: pnpm-store
        shell: bash
        run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

      - name: Cache pnpm store
        uses: actions/cache@v4
        continue-on-error: true
        with:
          path: ${{ steps.pnpm-store.outputs.path }}
          key: ubuntu-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            ubuntu-pnpm-

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

      - name: Resolve package versions
        id: package-versions
        shell: bash
        run: |
          node -e "const fs=require('fs'); const headless=JSON.parse(fs.readFileSync('packages/headless/package.json','utf8')); const server=JSON.parse(fs.readFileSync('packages/server/package.json','utf8')); const owpenbot=JSON.parse(fs.readFileSync('packages/owpenbot/package.json','utf8')); console.log('openwrk=' + headless.version); console.log('server=' + server.version); console.log('owpenbot=' + owpenbot.version);" >> "$GITHUB_OUTPUT"

      - name: Check npm versions
        id: npm-versions
        shell: bash
        env:
          OPENWRK_VERSION: ${{ steps.package-versions.outputs.openwrk }}
          SERVER_VERSION: ${{ steps.package-versions.outputs.server }}
          OWPENBOT_VERSION: ${{ steps.package-versions.outputs.owpenbot }}
        run: |
          set -euo pipefail
          openwrk_current=$(npm view openwrk version)
          server_current=$(npm view openwork-server version)
          owpenbot_current=$(npm view owpenwork version)

          if [ "$openwrk_current" = "$OPENWRK_VERSION" ]; then
            echo "publish_openwrk=false" >> "$GITHUB_OUTPUT"
          else
            echo "publish_openwrk=true" >> "$GITHUB_OUTPUT"
          fi

          if [ "$server_current" = "$SERVER_VERSION" ]; then
            echo "publish_server=false" >> "$GITHUB_OUTPUT"
          else
            echo "publish_server=true" >> "$GITHUB_OUTPUT"
          fi

          if [ "$owpenbot_current" = "$OWPENBOT_VERSION" ]; then
            echo "publish_owpenbot=false" >> "$GITHUB_OUTPUT"
          else
            echo "publish_owpenbot=true" >> "$GITHUB_OUTPUT"
          fi

          publish_any=false
          if [ "$openwrk_current" != "$OPENWRK_VERSION" ] || [ "$server_current" != "$SERVER_VERSION" ] || [ "$owpenbot_current" != "$OWPENBOT_VERSION" ]; then
            publish_any=true
          fi
          echo "publish_any=$publish_any" >> "$GITHUB_OUTPUT"

      - name: Ensure npm auth
        if: steps.npm-versions.outputs.publish_any == 'true'
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          set -euo pipefail
          if [ -z "${NPM_TOKEN:-}" ]; then
            echo "NPM_TOKEN is required to publish packages." >&2
            exit 1
          fi
          npm config set //registry.npmjs.org/:_authToken "$NPM_TOKEN"

      - name: Publish openwork-server
        if: steps.npm-versions.outputs.publish_server == 'true'
        run: pnpm --filter openwork-server publish --access public --no-git-checks

      - name: Publish owpenwork
        if: steps.npm-versions.outputs.publish_owpenbot == 'true'
        run: pnpm --filter owpenwork publish --access public --no-git-checks

      - name: Publish openwrk
        if: steps.npm-versions.outputs.publish_openwrk == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
          OPENWRK_VERSION: ${{ steps.package-versions.outputs.openwrk }}
        run: |
          set -euo pipefail
          tag="openwrk-v${OPENWRK_VERSION}"
          if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
            echo "openwrk sidecar release $tag not found. Publish sidecars before openwrk." >&2
            exit 1
          fi
          pnpm --filter openwrk publish --access public --no-git-checks

  aur-pr:
    name: Open AUR update PR
    needs: [resolve-release, publish-tauri]
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    env:
      RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ env.RELEASE_TAG }}
          fetch-depth: 0

      - name: Open AUR update PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: scripts/aur/open-pr.sh "$RELEASE_TAG"
