Skip to content

Build & Release

Build & Release #48

Workflow file for this run

name: Build & Release
on:
workflow_dispatch:
push:
tags: ['v*.*.*']
permissions:
contents: write
jobs:
build:
runs-on: macos-26
outputs:
marketing_version: ${{ steps.version.outputs.marketing_version }}
tag: ${{ steps.version.outputs.tag }}
is_release: ${{ steps.version.outputs.is_release }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install signing certificate
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
CERTIFICATE_BASE64_PATH="$RUNNER_TEMP/build_certificate.base64"
printf '%s' "$BUILD_CERTIFICATE_BASE64" > "$CERTIFICATE_BASE64_PATH"
base64 -D -i "$CERTIFICATE_BASE64_PATH" -o "$CERTIFICATE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
- name: Set version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "marketing_version=${TAG#v}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
SHORT_SHA="${GITHUB_SHA:0:7}"
echo "tag=dev-${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "marketing_version=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Archive
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
xcodebuild \
-project Wave.xcodeproj \
-scheme Wave \
-configuration Release \
-archivePath build/Wave.xcarchive \
-derivedDataPath build \
archive \
MARKETING_VERSION="${{ steps.version.outputs.marketing_version }}" \
CURRENT_PROJECT_VERSION="${{ steps.version.outputs.marketing_version }}" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \
PROVISIONING_PROFILE_SPECIFIER="" \
PROVISIONING_PROFILE="" \
STRIP_INSTALLED_PRODUCT=YES \
DEAD_CODE_STRIPPING=YES \
DEBUG_INFORMATION_FORMAT=dwarf-with-dsym \
SWIFT_OPTIMIZATION_LEVEL=-Osize \
SWIFT_COMPILATION_MODE=wholemodule \
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO
- name: Export signed app
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
cat <<EOF > "$RUNNER_TEMP/ExportOptions.plist"
<?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 Application</string>
<key>teamID</key>
<string>${APPLE_TEAM_ID}</string>
</dict>
</plist>
EOF
xcodebuild \
-exportArchive \
-archivePath build/Wave.xcarchive \
-exportPath build/export \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \
PROVISIONING_PROFILE_SPECIFIER="" \
PROVISIONING_PROFILE=""
- name: Debug app size
run: |
echo "=== App bundle total ==="
du -sh build/export/Wave.app
echo "=== Contents breakdown ==="
du -sh build/export/Wave.app/Contents/*
echo "=== Frameworks ==="
du -sh build/export/Wave.app/Contents/Frameworks/* 2>/dev/null || echo "No frameworks"
echo "=== Binary ==="
du -sh build/export/Wave.app/Contents/MacOS/*
echo "=== Plugins/XPC ==="
du -sh build/export/Wave.app/Contents/PlugIns/* 2>/dev/null || echo "No plugins"
echo "=== Resources (top level) ==="
du -sh build/export/Wave.app/Contents/Resources/* 2>/dev/null | sort -rh | head -20
- name: Verify signed app
run: |
codesign --verify --deep --strict --verbose=2 build/export/Wave.app
- name: Upload app
run: ditto -c -k --sequesterRsrc --keepParent build/export/Wave.app Wave.app.zip
- name: Upload app artifact
uses: actions/upload-artifact@v4
with:
name: Wave.app.zip
path: Wave.app.zip
package:
runs-on: macos-26
needs: build
outputs:
dmg_name: ${{ steps.dmg.outputs.dmg_name }}
steps:
- name: Download app
uses: actions/download-artifact@v4
with:
name: Wave.app.zip
- name: Extract app
run: ditto -x -k Wave.app.zip .
- name: Install signing certificate
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
CERTIFICATE_BASE64_PATH="$RUNNER_TEMP/build_certificate.base64"
printf '%s' "$BUILD_CERTIFICATE_BASE64" > "$CERTIFICATE_BASE64_PATH"
base64 -D -i "$CERTIFICATE_BASE64_PATH" -o "$CERTIFICATE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
- name: Install create-dmg
run: brew install create-dmg
- name: Create DMG
id: dmg
run: |
TAG="${{ needs.build.outputs.tag }}"
DMG_NAME="Wave-Installer-${TAG}.dmg"
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
create-dmg \
--volname "Wave" \
--window-pos 200 120 \
--window-size 640 360 \
--icon-size 96 \
--icon "Wave.app" 180 170 \
--hide-extension "Wave.app" \
--app-drop-link 460 170 \
"$DMG_NAME" \
Wave.app/
- name: Sign DMG
run: |
DMG_NAME="${{ steps.dmg.outputs.dmg_name }}"
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$RUNNER_TEMP/app-signing.keychain-db" | awk -F '"' '/Developer ID Application/ { print $2; exit }')
test -n "$SIGNING_IDENTITY"
codesign --force --sign "$SIGNING_IDENTITY" --keychain "$RUNNER_TEMP/app-signing.keychain-db" "$DMG_NAME"
codesign --verify --verbose=2 "$DMG_NAME"
- name: Notarize and staple DMG
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
DMG_NAME="${{ steps.dmg.outputs.dmg_name }}"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
xcrun notarytool store-credentials wave-notary \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--keychain "$KEYCHAIN_PATH"
xcrun notarytool submit "$DMG_NAME" \
--keychain-profile wave-notary \
--keychain "$KEYCHAIN_PATH" \
--wait
xcrun stapler staple "$DMG_NAME"
spctl -a -t open --context context:primary-signature -vv "$DMG_NAME"
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: ${{ steps.dmg.outputs.dmg_name }}
path: ${{ steps.dmg.outputs.dmg_name }}
release:
runs-on: macos-26
needs: [build, package]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download DMG
uses: actions/download-artifact@v4
with:
name: ${{ needs.package.outputs.dmg_name }}
path: release-dir
- name: Install Sparkle tools
if: needs.build.outputs.is_release == 'true'
run: |
curl -L https://github.com/sparkle-project/Sparkle/releases/download/2.9.0/Sparkle-2.9.0.tar.xz -o sparkle.tar.xz
mkdir sparkle-tools
tar -xf sparkle.tar.xz -C sparkle-tools
SPARKLE_BIN=$(find sparkle-tools -name generate_appcast -type f | head -1 | xargs dirname)
echo "$SPARKLE_BIN" >> $GITHUB_PATH
- name: Generate appcast
if: needs.build.outputs.is_release == 'true'
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
TAG="${{ needs.build.outputs.tag }}"
DMG="${{ needs.package.outputs.dmg_name }}"
echo "$SPARKLE_PRIVATE_KEY" > /tmp/sparkle.key
generate_appcast \
--ed-key-file /tmp/sparkle.key \
--download-url-prefix "https://github.com/mxvsh/wave/releases/download/${TAG}/" \
--link "https://github.com/mxvsh/wave" \
release-dir/
rm /tmp/sparkle.key
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ needs.build.outputs.tag }}"
DMG="${{ needs.package.outputs.dmg_name }}"
IS_RELEASE="${{ needs.build.outputs.is_release }}"
if [[ "$IS_RELEASE" == "true" ]]; then
VERSION="${{ needs.build.outputs.marketing_version }}"
awk "/^## \[${VERSION}\]/{found=1; next} found && /^## /{exit} found{print}" CHANGELOG.md > release-notes.md
gh release create "$TAG" \
--title "$TAG" \
--notes-file release-notes.md \
"release-dir/$DMG"
else
gh release create "$TAG" \
--title "$TAG" \
--prerelease \
--notes "Development build from commit ${GITHUB_SHA}" \
"release-dir/$DMG"
fi
- name: Push appcast to gh-pages
if: needs.build.outputs.is_release == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin gh-pages
git checkout gh-pages
cp release-dir/appcast.xml appcast.xml
git add appcast.xml
git commit -m "Update appcast for ${{ needs.build.outputs.tag }}"
git push origin gh-pages