Skip to content

Fix Ghostty make targets for paths with spaces (#210) #152

Fix Ghostty make targets for paths with spaces (#210)

Fix Ghostty make targets for paths with spaces (#210) #152

Workflow file for this run

name: Release Tip
on:
push:
branches: [main]
workflow_dispatch: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
check:
if: github.event_name == 'workflow_dispatch' || github.ref_name == 'main'
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.check.outputs.should_skip }}
steps:
- name: Check if tip release already exists for this commit
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
TIP_SHA=$(gh api repos/${{ github.repository }}/git/ref/tags/tip --jq '.object.sha' 2>/dev/null || echo "")
ASSET_COUNT=$(gh release view tip -R "${{ github.repository }}" --json assets --jq '.assets | length' 2>/dev/null || echo "0")
if [ "$TIP_SHA" = "${{ github.sha }}" ] && [ "$ASSET_COUNT" -gt 0 ]; then
echo "Tip already points at ${{ github.sha }} with $ASSET_COUNT assets, skipping"
echo "should_skip=true" >> "$GITHUB_OUTPUT"
else
echo "should_skip=false" >> "$GITHUB_OUTPUT"
fi
build:
runs-on: macos-26
needs: [check]
if: needs.check.outputs.should_skip != 'true'
permissions:
contents: read
env:
MISE_HTTP_TIMEOUT: 120
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEVELOPER_ID_CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }}
DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
DEVELOPER_ID_IDENTITY: ${{ secrets.DEVELOPER_ID_IDENTITY }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: ./.github/actions/setup-macos
- name: Override build number for tip
run: |
BASE=$(awk -F' = ' '/CURRENT_PROJECT_VERSION = [0-9]+/{gsub(/;/,""); print $2; exit}' supacode.xcodeproj/project.pbxproj)
OFFSET=${{ github.run_number }}
if [ "$OFFSET" -gt 999 ]; then
echo "::error::Tip run_number ($OFFSET) exceeds 999. Bump CURRENT_PROJECT_VERSION before the next tip release."
exit 1
fi
BUILD_NUMBER=$((BASE * 1000 + OFFSET))
sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]*/CURRENT_PROJECT_VERSION = $BUILD_NUMBER/g" supacode.xcodeproj/project.pbxproj
echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
- name: Setup keychain
run: |
echo "$DEVELOPER_ID_CERT_P12" | base64 --decode > build-cert.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import build-cert.p12 -k build.keychain -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/xcodebuild > /dev/null
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain > /dev/null
security list-keychains -d user -s build.keychain $(security list-keychains -d user | tr -d '"')
security default-keychain -s build.keychain
DEVELOPER_ID_IDENTITY_SHA=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | awk '{print $2}')
if [ -z "$DEVELOPER_ID_IDENTITY_SHA" ]; then
echo "::error::Developer ID Application identity not found in keychain"
exit 1
fi
echo "DEVELOPER_ID_IDENTITY_SHA=$DEVELOPER_ID_IDENTITY_SHA" >> "$GITHUB_ENV"
- name: Inject secrets
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
run: |
set -euo pipefail
: "${SENTRY_DSN:?secret SENTRY_DSN is not set}"
: "${POSTHOG_API_KEY:?secret POSTHOG_API_KEY is not set}"
: "${POSTHOG_HOST:?secret POSTHOG_HOST is not set}"
sed -i '' "s|__SENTRY_DSN__|${SENTRY_DSN}|g" supacode/App/supacodeApp.swift
sed -i '' "s|__POSTHOG_API_KEY__|${POSTHOG_API_KEY}|g" supacode/App/supacodeApp.swift
sed -i '' "s|__POSTHOG_HOST__|${POSTHOG_HOST}|g" supacode/App/supacodeApp.swift
- name: Archive Xcode project
run: make archive
- name: Upload dSYMs
uses: actions/upload-artifact@v6
with:
name: dsyms
path: build/supacode.xcarchive/dSYMs
- run: |
cat > build/ExportOptions.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>$DEVELOPER_ID_IDENTITY</string>
<key>teamID</key>
<string>$APPLE_TEAM_ID</string>
</dict>
</plist>
EOF
make export-archive
- name: Re-sign frameworks
run: |
set -ex
APP_PATH="$(find build/export -name "supacode.app" -maxdepth 3 -print -quit)"
bash ./.github/scripts/resign_exported_app.sh build/export
codesign -d --entitlements - "$APP_PATH/Contents/MacOS/supacode" 2>&1 | tee /tmp/supacode-entitlements.txt
grep -q "com.apple.security.device.audio-input" /tmp/supacode-entitlements.txt
- name: Store notarization credentials
run: |
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
- name: Build DMG
run: |
APP_PATH="$(find build/export -name "supacode.app" -maxdepth 3 -print -quit)"
mise exec -- create-dmg "$APP_PATH" build/ \
--overwrite \
--dmg-title="Supacode" \
--identity="$DEVELOPER_ID_IDENTITY_SHA"
DMG_OUTPUT=$(find build -name "*.dmg" -maxdepth 1 | head -1)
if [ "$DMG_OUTPUT" != "build/supacode.dmg" ]; then
mv "$DMG_OUTPUT" build/supacode.dmg
fi
- name: Notarize and staple
run: |
APP_PATH="$(find build/export -name "supacode.app" -maxdepth 3 -print -quit)"
for attempt in 1 2 3; do
echo "Notarization attempt $attempt..."
xcrun notarytool submit build/supacode.dmg --keychain-profile "notarytool-profile" --wait && break
echo "Attempt $attempt failed, retrying in 30s..."
sleep 30
done
xcrun stapler staple build/supacode.dmg
xcrun stapler staple "$APP_PATH"
- name: Create app zip for Sparkle
run: |
APP_PATH="$(find build/export -name "supacode.app" -maxdepth 3 -print -quit)"
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" build/supacode.app.zip
- name: Generate tip appcast
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
MAX_DELTAS=10
STAGING=$(mktemp -d)
cp build/supacode.app.zip "$STAGING/"
# Download previous tip builds stored as history assets
ASSETS=$(gh release view tip -R "${{ github.repository }}" --json assets --jq '.assets[].name' 2>/dev/null || true)
for asset in $ASSETS; do
case "$asset" in
supacode-history-*.app.zip)
gh release download tip -p "$asset" -O "$STAGING/$asset" -R "${{ github.repository }}" 2>/dev/null || true
;;
esac
done
printf "%s" "$SPARKLE_PRIVATE_KEY" | tr -d '\r\n\t ' | ./bins/generate_appcast \
--download-url-prefix "https://supacode.sh/download/tip/" \
--maximum-versions $((MAX_DELTAS + 1)) \
--maximum-deltas $MAX_DELTAS \
--delta-compression lzma \
--ed-key-file - "$STAGING"
# Inject sparkle:channel tag into the tip items
sed -i '' 's|<enclosure |<sparkle:channel>tip</sparkle:channel>\n <enclosure |' "$STAGING/appcast.xml"
cp "$STAGING/appcast.xml" build/appcast.xml
find "$STAGING" -name "*.delta" -exec cp {} build/ \; 2>/dev/null || true
- uses: actions/upload-artifact@v6
with:
name: build-artifacts
path: |
build/supacode.dmg
build/supacode.app.zip
build/appcast.xml
build/*.delta
tag:
runs-on: ubuntu-latest
needs: [check, build]
if: needs.check.outputs.should_skip != 'true'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
token: ${{ secrets.GH_RELEASE_TOKEN }}
- name: Force-move 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:
runs-on: ubuntu-latest
needs: [check, build]
if: needs.check.outputs.should_skip != 'true'
steps:
- name: Install sentry-cli
run: curl -sL https://sentry.io/get-cli/ | sh
- uses: actions/download-artifact@v7
with:
name: dsyms
path: dsyms
- name: Upload dSYMs to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: supabit
SENTRY_PROJECT: supacode
run: sentry-cli debug-files upload --include-sources dsyms
publish:
runs-on: ubuntu-latest
needs: [check, build, tag]
if: needs.check.outputs.should_skip != 'true'
permissions:
contents: write
steps:
- uses: actions/download-artifact@v7
with:
name: build-artifacts
path: build
- name: Update tip release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create tip --prerelease --title 'Supacode Tip ("Nightly")' --target "${{ github.sha }}" --notes "" -R "${{ github.repository }}" 2>/dev/null ||
gh release edit tip --prerelease --title 'Supacode Tip ("Nightly")' --target "${{ github.sha }}" -R "${{ github.repository }}"
# Delete stale delta files from previous tip
STALE_DELTAS=$(gh release view tip -R "${{ github.repository }}" --json assets --jq '.assets[].name | select(endswith(".delta"))' 2>/dev/null || true)
for d in $STALE_DELTAS; do
gh release delete-asset tip "$d" -R "${{ github.repository }}" -y 2>/dev/null || true
done
DELTA_FILES=$(find build -name "*.delta" -type f 2>/dev/null | tr '\n' ' ' || true)
gh release upload tip build/supacode.dmg build/supacode.app.zip build/appcast.xml $DELTA_FILES --clobber -R "${{ github.repository }}"
# Store current build as history asset for future delta generation
MAX_DELTAS=10
cp build/supacode.app.zip "build/supacode-history-${{ github.run_number }}.app.zip"
gh release upload tip "build/supacode-history-${{ github.run_number }}.app.zip" --clobber -R "${{ github.repository }}"
# Prune oldest history assets beyond MAX_DELTAS
HISTORY=$(gh release view tip -R "${{ github.repository }}" --json assets --jq '[.assets[].name | select(startswith("supacode-history-"))] | sort | reverse' 2>/dev/null || echo "[]")
PRUNE=$(echo "$HISTORY" | jq -r ".[$MAX_DELTAS:][]" 2>/dev/null || true)
for old in $PRUNE; do
gh release delete-asset tip "$old" -R "${{ github.repository }}" -y 2>/dev/null || true
done
- name: Merge tip item into production appcast
env:
GH_TOKEN: ${{ github.token }}
run: |
LATEST_TAG=$(gh release list --exclude-drafts --exclude-pre-releases -R "${{ github.repository }}" --json tagName --jq '.[0].tagName' 2>/dev/null || echo "")
if [ -z "$LATEST_TAG" ]; then
echo "No stable release found, skipping appcast merge"
exit 0
fi
gh release download "$LATEST_TAG" -p "appcast.xml" -O stable_appcast.xml -R "${{ github.repository }}" 2>/dev/null || exit 0
python3 - stable_appcast.xml build/appcast.xml merged_appcast.xml <<'PYEOF'
import sys
import xml.etree.ElementTree as ET
ET.register_namespace('sparkle', 'http://www.andymatuschak.org/xml-namespaces/sparkle')
ET.register_namespace('dc', 'http://purl.org/dc/elements/1.1/')
stable_path, tip_path, out_path = sys.argv[1], sys.argv[2], sys.argv[3]
stable_tree = ET.parse(stable_path)
stable_root = stable_tree.getroot()
tip_tree = ET.parse(tip_path)
tip_root = tip_tree.getroot()
ns = {'sparkle': 'http://www.andymatuschak.org/xml-namespaces/sparkle'}
stable_channel = stable_root.find('.//channel')
# Remove existing tip items from stable appcast
for item in stable_channel.findall('item'):
ch = item.find('sparkle:channel', ns)
if ch is not None and ch.text == 'tip':
stable_channel.remove(item)
# Add new tip items from tip appcast
tip_channel = tip_root.find('.//channel')
for item in tip_channel.findall('item'):
stable_channel.append(item)
stable_tree.write(out_path, xml_declaration=True, encoding='utf-8')
PYEOF
mv merged_appcast.xml appcast.xml
gh release upload "$LATEST_TAG" appcast.xml --clobber -R "${{ github.repository }}"