on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version to deploy (format: vX.Y.Z)"
        required: true
      upload:
        description: "Upload final artifacts to R2"
        default: false
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"

name: Release Tag

# We must only run one release workflow at a time to prevent corrupting
# our release artifacts.
concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

jobs:
  setup:
    runs-on: namespace-profile-ghostty-sm
    outputs:
      version: ${{ steps.extract_version.outputs.version }}
      build: ${{ steps.extract_build_info.outputs.build }}
      commit: ${{ steps.extract_build_info.outputs.commit }}
      commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
    steps:
      - name: Validate Version Input
        if: github.event_name == 'workflow_dispatch'
        run: |
          if [[ ! "${{ github.event.inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Error: Version must follow the format vX.Y.Z (e.g., v1.0.0)."
            exit 1
          fi

          echo "Version is valid: ${{ github.event.inputs.version }}"

      - name: Exract the Version
        id: extract_version
        run: |
          if [[ "${{ github.event_name }}" == "push" ]]; then
            # Remove the leading 'v' from the tag
            VERSION=${GITHUB_REF#refs/tags/v}
            echo "version=$VERSION" >> $GITHUB_OUTPUT
          elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            VERSION=${{ github.event.inputs.version }}
            VERSION=${VERSION#v}
            echo "version=$VERSION" >> $GITHUB_OUTPUT
          else
            echo "Error: Unsupported event type."
            exit 1
          fi

      - name: Checkout code
        uses: actions/checkout@v4
        with:
          # Important so that build number generation works
          fetch-depth: 0

      - name: Extract build info
        id: extract_build_info
        run: |
          GHOSTTY_BUILD=$(git rev-list --count HEAD)
          GHOSTTY_COMMIT=$(git rev-parse --short HEAD)
          GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
          echo "build=$GHOSTTY_BUILD" >> $GITHUB_OUTPUT
          echo "commit=$GHOSTTY_COMMIT" >> $GITHUB_OUTPUT
          echo "commit_long=$GHOSTTY_COMMIT_LONG" >> $GITHUB_OUTPUT
          cat $GITHUB_OUTPUT

  source-tarball:
    runs-on: namespace-profile-ghostty-md
    needs: [setup]
    env:
      GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - uses: cachix/install-nix-action@v30
        with:
          nix_path: nixpkgs=channel:nixos-unstable

      - uses: cachix/cachix-action@v15
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

      - name: Create Tarball
        run: |
          git archive --format=tgz --prefix="ghostty-${GHOSTTY_VERSION}/" -o "ghostty-${GHOSTTY_VERSION}.tar.gz" HEAD
          git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD

      - name: Sign Tarball
        run: |
          echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
          echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password
          nix develop -c minisign -S -m "ghostty-${GHOSTTY_VERSION}.tar.gz" -s minisign.key < minisign.password
          nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: source-tarball
          path: |-
            ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz
            ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz.minisig
            ghostty-source.tar.gz
            ghostty-source.tar.gz.minisig

  build-macos:
    needs: [setup]
    runs-on: namespace-profile-ghostty-macos
    timeout-minutes: 90
    env:
      GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
      GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
      GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - uses: cachix/install-nix-action@v30
        with:
          nix_path: nixpkgs=channel:nixos-unstable
      - uses: cachix/cachix-action@v15
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

      - name: XCode Select
        run: sudo xcode-select -s /Applications/Xcode_16.0.app

      - name: Setup Sparkle
        env:
          SPARKLE_VERSION: 2.6.3
        run: |
          mkdir -p .action/sparkle
          cd .action/sparkle
          curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip
          unzip sparkle.zip
          echo "$(pwd)/bin" >> $GITHUB_PATH

      # GhosttyKit is the framework that is built from Zig for our native
      # Mac app to access. Build this in release mode.
      - name: Build GhosttyKit
        run: |
          nix develop -c \
            zig build \
            -Doptimize=ReleaseFast \
            -Dversion-string=${GHOSTTY_VERSION}

      # The native app is built with native XCode tooling. This also does
      # codesigning. IMPORTANT: this must NOT run in a Nix environment.
      # Nix breaks xcodebuild so this has to be run outside.
      - name: Build Ghostty.app
        run: |
          cd macos
          xcodebuild -target Ghostty -configuration Release

      # Add all our metadata to Info.plist so we can reference it later.
      - name: Update Info.plist
        env:
          SPARKLE_KEY_PUB: ${{ secrets.PROD_MACOS_SPARKLE_KEY_PUB }}
        run: |
          # Version Info
          /usr/libexec/PlistBuddy -c "Set :GhosttyCommit $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GHOSTTY_BUILD" "macos/build/Release/Ghostty.app/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $GHOSTTY_VERSION" "macos/build/Release/Ghostty.app/Contents/Info.plist"

          # Updater
          /usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"

      - name: Codesign app bundle
        env:
          MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
          MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
          MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
        run: |
          # Turn our base64-encoded certificate back to a regular .p12 file
          echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12

          # We need to create a new keychain, otherwise using the certificate will prompt
          # with a UI dialog asking for the certificate password, which we can't
          # use in a headless CI environment
          security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
          security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain

          # Codesign Sparkle. Some notes here:
          #   - The XPC services aren't used since we don't sandbox Ghostty,
          #     but since they're part of the build, they still need to be
          #     codesigned.
          #   - The binaries in the "Versions" folders need to NOT be symlinks.
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"

          # Codesign the app bundle
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app

      - name: Create DMG
        env:
          MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
        run: |
          npm install --global create-dmg
          create-dmg \
            --identity="$MACOS_CERTIFICATE_NAME" \
            ./macos/build/Release/Ghostty.app \
            ./
          mv ./Ghostty*.dmg ./Ghostty.dmg

      - name: "Notarize DMG"
        env:
          PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
          PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
          PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
        run: |
          # Store the notarization credentials so that we can prevent a UI password dialog
          # from blocking the CI
          echo "Create keychain profile"
          xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"

          # Here we send the notarization request to the Apple's Notarization service, waiting for the result.
          # This typically takes a few seconds inside a CI environment, but it might take more depending on the App
          # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
          # you're curious
          echo "Notarize dmg"
          xcrun notarytool submit "Ghostty.dmg" --keychain-profile "notarytool-profile" --wait

          # Finally, we need to "attach the staple" to our executable, which will allow our app to be
          # validated by macOS even when an internet connection is not available. We do this to
          # both the app and the dmg
          echo "Attach staple"
          xcrun stapler staple "Ghostty.dmg"
          xcrun stapler staple "macos/build/Release/Ghostty.app"

      # Zip up the app and symbols
      - name: Zip App
        run: |
          cd macos/build/Release
          zip -9 -r --symlinks ../../../ghostty-macos-universal.zip Ghostty.app
          zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: macos
          path: |-
            Ghostty.dmg
            ghostty-macos-universal.zip
            ghostty-macos-universal-dsym.zip

  sentry-dsym:
    runs-on: namespace-profile-ghostty-sm
    needs: [build-macos]
    steps:
      - name: Install sentry-cli
        run: |
          curl -sL https://sentry.io/get-cli/ | bash

      - name: Download macOS Artifacts
        uses: actions/download-artifact@v4
        with:
          name: macos

      - name: Upload dSYM to Sentry
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
        run: |
          sentry-cli dif upload --project ghostty --wait ghostty-macos-universal-dsym.zip

  appcast:
    needs: [setup, build-macos]
    runs-on: namespace-profile-ghostty-macos
    env:
      GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
      GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
      GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Download macOS Artifacts
        uses: actions/download-artifact@v4
        with:
          name: macos

      - name: Setup Sparkle
        env:
          SPARKLE_VERSION: 2.6.3
        run: |
          mkdir -p .action/sparkle
          cd .action/sparkle
          curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip
          unzip sparkle.zip
          echo "$(pwd)/bin" >> $GITHUB_PATH

      - name: Generate Appcast
        env:
          SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }}
        run: |
          echo "GHOSTTY_VERSION=$GHOSTTY_VERSION"
          echo "GHOSTTY_BUILD=$GHOSTTY_BUILD"
          echo "GHOSTTY_COMMIT=$GHOSTTY_COMMIT"
          echo "GHOSTTY_COMMIT_LONG=$GHOSTTY_COMMIT_LONG"

          echo $SPARKLE_KEY > signing.key
          sign_update -f signing.key Ghostty.dmg > sign_update.txt
          curl -L https://release.files.ghostty.org/appcast.xml > appcast.xml
          python3 ./dist/macos/update_appcast_tag.py
          test -f appcast_new.xml
          mv appcast_new.xml appcast.xml

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: sparkle
          path: |-
            appcast.xml

  upload:
    if: |-
      (github.event_name == 'workflow_dispatch' &&
        github.event.inputs.upload == 'true') ||
      github.event_name == 'push'
    needs: [setup, source-tarball, build-macos, appcast]
    runs-on: namespace-profile-ghostty-sm
    env:
      GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
    steps:
      - name: Download macOS Artifacts
        uses: actions/download-artifact@v4
        with:
          name: macos

      - name: Download Sparkle Artifacts
        uses: actions/download-artifact@v4
        with:
          name: sparkle

      - name: Download Source Tarball Artifacts
        uses: actions/download-artifact@v4
        with:
          name: source-tarball

      # Upload all of our files EXCEPT the appcast. The appcast triggers
      # updates in clients and we don't want to do that until we're
      # sure these are uploaded.
      - name: Prep Files
        run: |
          mkdir blob
          mkdir -p blob/${GHOSTTY_VERSION}
          mv "ghostty-${GHOSTTY_VERSION}.tar.gz" blob/${GHOSTTY_VERSION}/ghostty-${GHOSTTY_VERSION}.tar.gz
          mv "ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" blob/${GHOSTTY_VERSION}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig
          mv ghostty-source.tar.gz blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz
          mv ghostty-source.tar.gz.minisig blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz.minisig
          mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip
          mv ghostty-macos-universal-dsym.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal-dsym.zip
          mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
      - name: Upload to R2
        uses: ryand56/r2-upload-action@latest
        with:
          r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
          r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
          r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
          r2-bucket: ghostty-release
          source-dir: blob
          destination-dir: ./

      - name: Prep Appcast
        run: |
          rm -rf blob
          mkdir blob
          mv appcast.xml blob/appcast.xml
      - name: Upload Appcast to R2
        uses: ryand56/r2-upload-action@latest
        with:
          r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
          r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
          r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
          r2-bucket: ghostty-release
          source-dir: blob
          destination-dir: ./
