on:
  workflow_run:
    workflows: [Test]
    types: [completed]
    branches: [main]

  workflow_dispatch:
    inputs:
      pr:
        type: number
        required: false

name: Release Tip

# 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:
    if: |
      github.event_name == 'workflow_dispatch' ||
      (
        github.repository_owner == 'ghostty-org' &&
        github.ref_name == 'main'
      )
    runs-on: namespace-profile-ghostty-sm
    outputs:
      should_skip: ${{ steps.check.outputs.should_skip }}
      build: ${{ steps.extract_build_info.outputs.build }}
      commit: ${{ steps.extract_build_info.outputs.commit }}
      commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          # Important so that build number generation works
          fetch-depth: 0
      - name: Setup Cache
        uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
        with:
          path: |
            /nix
      - uses: cachix/install-nix-action@19effe9fe722874e6d46dd7182e4b8b7a43c4a99 # v31.10.0
        with:
          nix_path: nixpkgs=channel:nixos-unstable
      - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
      - 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
      - name: Check if tip already exists
        id: check
        run: |
          GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
          if nix develop -c nu .github/scripts/ghostty-tip $GHOSTTY_COMMIT_LONG; then
            echo "Tip release already exists for commit $GHOSTTY_COMMIT_LONG"
            echo "should_skip=true" >> $GITHUB_OUTPUT
          else
            echo "No tip release found for commit $GHOSTTY_COMMIT_LONG"
            echo "should_skip=false" >> $GITHUB_OUTPUT
          fi

  tag:
    runs-on: namespace-profile-ghostty-sm
    needs: [setup, build-macos]
    if: needs.setup.outputs.should_skip != 'true'
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Tip Tag
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git tag -fa tip -m "Latest Continuous Release" ${GITHUB_SHA}
          git push --force origin tip

  sentry-dsym-debug-slow:
    runs-on: namespace-profile-ghostty-sm
    needs: [setup, build-macos-debug-slow]
    if: needs.setup.outputs.should_skip != 'true'
    env:
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install sentry-cli
        run: |
          curl -sL https://sentry.io/get-cli/ | bash

      - name: Download dSYM
        run: |
          curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip > dsym.zip

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

  sentry-dsym-debug-fast:
    runs-on: namespace-profile-ghostty-sm
    needs: [setup, build-macos-debug-fast]
    if: needs.setup.outputs.should_skip != 'true'
    env:
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install sentry-cli
        run: |
          curl -sL https://sentry.io/get-cli/ | bash

      - name: Download dSYM
        run: |
          curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip > dsym.zip

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

  sentry-dsym:
    runs-on: namespace-profile-ghostty-sm
    needs: [setup, build-macos]
    if: needs.setup.outputs.should_skip != 'true'
    env:
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install sentry-cli
        run: |
          curl -sL https://sentry.io/get-cli/ | bash

      - name: Download dSYM
        run: |
          curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip > dsym.zip

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

  source-tarball:
    needs: [setup]
    if: |
      needs.setup.outputs.should_skip != 'true' &&
      (
        github.event_name == 'workflow_dispatch' ||
        (
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        )
      )
    runs-on: namespace-profile-ghostty-md
    env:
      ZIG_LOCAL_CACHE_DIR: /zig/local-cache
      ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Setup Cache
        uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
        with:
          path: |
            /nix
            /zig
      - uses: cachix/install-nix-action@19effe9fe722874e6d46dd7182e4b8b7a43c4a99 # v31.10.0
        with:
          nix_path: nixpkgs=channel:nixos-unstable
      - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
      - name: Create Tarball
        run: |
          rm -rf zig-out/dist
          nix develop -c zig build distcheck
          cp zig-out/dist/*.tar.gz ghostty-source.tar.gz

      - 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-source.tar.gz -s minisign.key < minisign.password

      - name: Update Release
        uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
        with:
          name: 'Ghostty Tip ("Nightly")'
          prerelease: true
          tag_name: tip
          target_commitish: ${{ github.sha }}
          files: |
            ghostty-source.tar.gz
            ghostty-source.tar.gz.minisig
          token: ${{ secrets.GH_RELEASE_TOKEN }}

  build-macos:
    needs: [setup]
    if: |
      needs.setup.outputs.should_skip != 'true' &&
      (
        github.event_name == 'workflow_dispatch' ||
        (
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        )
      )

    runs-on: namespace-profile-ghostty-macos-tahoe
    timeout-minutes: 90
    env:
      GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
      GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
      ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
      ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          # Important so that build number generation works
          fetch-depth: 0

      - name: Setup Cache
        uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
        with:
          cache: |
            xcode
          path: |
            /Users/runner/zig

      # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
      - uses: DeterminateSystems/nix-installer-action@main
        with:
          determinate: true
      - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

      - name: XCode Select
        run: sudo xcode-select -s /Applications/Xcode_26.2.app

      - name: Xcode Version
        run: xcodebuild -version

      # Setup Sparkle
      - name: Setup Sparkle
        env:
          SPARKLE_VERSION: 2.9.0
        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 -Demit-macos-app=false

      # 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 \
            COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
            COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES

      # We inject the "build number" as simply the number of commits since HEAD.
      # This will be a monotonically always increasing build number that we use.
      - 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_COMMIT" "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"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"

          # 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:
          APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }}
          APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
          APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
        run: |
          # Store the notarization credentials so that we can prevent a UI password dialog
          # from blocking the CI
          echo "Create keychain profile"
          echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8
          xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER"
          rm notarization_key.p8

          # 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/

      # Update Release
      - name: Release
        uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
        with:
          name: 'Ghostty Tip ("Nightly")'
          prerelease: true
          tag_name: tip
          target_commitish: ${{ github.sha }}
          files: |
            ghostty-macos-universal.zip
            Ghostty.dmg
          token: ${{ secrets.GH_RELEASE_TOKEN }}

      # Create our appcast for Sparkle
      - name: Generate Appcast
        if: |
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        env:
          SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }}
        run: |
          echo $SPARKLE_KEY > signing.key
          sign_update -f signing.key Ghostty.dmg > sign_update.txt
          curl -L https://tip.files.ghostty.org/appcast.xml > appcast.xml
          python3 ./dist/macos/update_appcast_tip.py
          test -f appcast_new.xml

      # Upload our binaries first
      - name: Prep R2 Storage
        run: |
          mkdir blob
          mkdir -p blob/${GHOSTTY_COMMIT_LONG}
          cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
          cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
          cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg

      - name: Upload to R2
        uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
        with:
          r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
          r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
          r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
          r2-bucket: ghostty-tip
          source-dir: blob
          destination-dir: ./

      # Now upload our appcast. This ensures that the appcast never
      # gets out of sync with the binaries.
      - name: Prep R2 Storage for Appcast
        if: |
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        run: |
          rm -r blob
          mkdir blob
          cp appcast_new.xml blob/appcast.xml

      - name: Upload Appcast to R2
        if: |
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
        with:
          r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
          r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
          r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
          r2-bucket: ghostty-tip
          source-dir: blob
          destination-dir: ./

      - name: Show and Save Release URLs
        run: |
          cat << EOF | tee release-urls.txt
          Release URLs:
            App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
            Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
            DMG: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
          EOF

      - name: Upload Release URLs
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0
        with:
          name: release-urls-${{ inputs.pr || '0' }}
          path: release-urls.txt
          retention-days: 2

  build-macos-debug-slow:
    needs: [setup]
    if: |
      needs.setup.outputs.should_skip != 'true' &&
      (
        github.event_name == 'workflow_dispatch' ||
        (
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        )
      )

    runs-on: namespace-profile-ghostty-macos-tahoe
    timeout-minutes: 90
    env:
      GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
      GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
      ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
      ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          # Important so that build number generation works
          fetch-depth: 0

      - name: Setup Cache
        uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
        with:
          cache: |
            xcode
          path: |
            /Users/runner/zig

      # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
      - uses: DeterminateSystems/nix-installer-action@main
        with:
          determinate: true
      - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

      - name: XCode Select
        run: sudo xcode-select -s /Applications/Xcode_26.2.app

      - name: Xcode Version
        run: xcodebuild -version

      # Setup Sparkle
      - name: Setup Sparkle
        env:
          SPARKLE_VERSION: 2.9.0
        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=Debug -Demit-macos-app=false

      # 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 \
            COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
            COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES

      # We inject the "build number" as simply the number of commits since HEAD.
      # This will be a monotonically always increasing build number that we use.
      - 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_COMMIT" "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"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"

          # 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: "Notarize app bundle"
        env:
          APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }}
          APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
          APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
        run: |
          # Store the notarization credentials so that we can prevent a UI password dialog
          # from blocking the CI
          echo "Create keychain profile"
          echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8
          xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER"
          rm notarization_key.p8

          # We can't notarize an app bundle directly, but we need to compress it as an archive.
          # Therefore, we create a zip file containing our app bundle, so that we can send it to the
          # notarization service
          echo "Creating temp notarization archive"
          ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip"

          # 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 app"
          xcrun notarytool submit "notarization.zip" --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.
          echo "Attach staple"
          xcrun stapler staple "macos/build/Release/Ghostty.app"

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

      # Update Release
      - name: Release
        uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
        with:
          name: 'Ghostty Tip ("Nightly")'
          prerelease: true
          tag_name: tip
          target_commitish: ${{ github.sha }}
          files: ghostty-macos-universal-debug-slow.zip
          token: ${{ secrets.GH_RELEASE_TOKEN }}

      # Update Blob Storage
      - name: Prep R2 Storage
        run: |
          mkdir blob
          mkdir -p blob/${GHOSTTY_COMMIT_LONG}
          cp ghostty-macos-universal-debug-slow.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow.zip
          cp ghostty-macos-universal-debug-slow-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip
      - name: Upload to R2
        uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
        with:
          r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
          r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
          r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
          r2-bucket: ghostty-tip
          source-dir: blob
          destination-dir: ./

      - name: Echo Release URLs
        run: |
          echo "Release URLs:"
          echo "  App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow.zip"
          echo "  Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip"

  build-macos-debug-fast:
    needs: [setup]
    if: |
      needs.setup.outputs.should_skip != 'true' &&
      (
        github.event_name == 'workflow_dispatch' ||
        (
          github.repository_owner == 'ghostty-org' &&
          github.ref_name == 'main'
        )
      )

    runs-on: namespace-profile-ghostty-macos-tahoe
    timeout-minutes: 90
    env:
      GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
      GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
      GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
      ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
      ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          # Important so that build number generation works
          fetch-depth: 0

      - name: Setup Cache
        uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
        with:
          cache: |
            xcode
          path: |
            /Users/runner/zig

      # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
      - uses: DeterminateSystems/nix-installer-action@main
        with:
          determinate: true
      - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
        with:
          name: ghostty
          authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

      - name: XCode Select
        run: sudo xcode-select -s /Applications/Xcode_26.2.app

      - name: Xcode Version
        run: xcodebuild -version

      # Setup Sparkle
      - name: Setup Sparkle
        env:
          SPARKLE_VERSION: 2.9.0
        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=ReleaseSafe -Demit-macos-app=false

      # 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 \
            COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
            COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES

      # We inject the "build number" as simply the number of commits since HEAD.
      # This will be a monotonically always increasing build number that we use.
      - 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_COMMIT" "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"
          /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"

          # 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: "Notarize app bundle"
        env:
          APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }}
          APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
          APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
        run: |
          # Store the notarization credentials so that we can prevent a UI password dialog
          # from blocking the CI
          echo "Create keychain profile"
          echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8
          xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER"
          rm notarization_key.p8

          # We can't notarize an app bundle directly, but we need to compress it as an archive.
          # Therefore, we create a zip file containing our app bundle, so that we can send it to the
          # notarization service
          echo "Creating temp notarization archive"
          ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip"

          # 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 app"
          xcrun notarytool submit "notarization.zip" --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.
          echo "Attach staple"
          xcrun stapler staple "macos/build/Release/Ghostty.app"

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

      # Update Release
      - name: Release
        uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
        with:
          name: 'Ghostty Tip ("Nightly")'
          prerelease: true
          tag_name: tip
          target_commitish: ${{ github.sha }}
          files: ghostty-macos-universal-debug-fast.zip
          token: ${{ secrets.GH_RELEASE_TOKEN }}

      # Update Blob Storage
      - name: Prep R2 Storage
        run: |
          mkdir blob
          mkdir -p blob/${GHOSTTY_COMMIT_LONG}
          cp ghostty-macos-universal-debug-fast.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast.zip
          cp ghostty-macos-universal-debug-fast-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip
      - name: Upload to R2
        uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
        with:
          r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
          r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
          r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
          r2-bucket: ghostty-tip
          source-dir: blob
          destination-dir: ./

      - name: Echo Release URLs
        run: |
          echo "Release URLs:"
          echo "  App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast.zip"
          echo "  Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip"
