Release Tag #21
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | |
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-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-source.tar.gz -s minisign.key < minisign.password | |
- name: Upload artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: source-tarball | |
path: |- | |
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" | |
- 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-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: ./ |