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: Extract 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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 }} ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix /zig - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # 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/ghostty-${GHOSTTY_VERSION}.tar.gz . cp zig-out/dist/ghostty-${GHOSTTY_VERSION}.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-${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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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-tahoe 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select run: sudo xcode-select -s /Applications/Xcode_26.0.app - name: Xcode Version run: xcodebuild -version - name: Setup Sparkle env: SPARKLE_VERSION: 2.7.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 \ -Demit-macos-app=false \ -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: 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/ - name: Upload artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 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-tahoe 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download macOS Artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: macos - name: Setup Sparkle env: SPARKLE_VERSION: 2.7.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: macos - name: Download Sparkle Artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: sparkle - name: Download Source Tarball Artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 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 mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml - name: Upload to R2 uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 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: ./