Skip to content

feat: Use release version and run number in app bundle (#10) #33

feat: Use release version and run number in app bundle (#10)

feat: Use release version and run number in app bundle (#10) #33

Workflow file for this run

# =============================================================================
# build.yml — CI/CD pipeline for Insomnia macOS app
#
# Triggers:
# - Push to main → build + test (validates code compiles and passes)
# - Pull request to main → build + test (gate for merge)
# - Published release → build + test + sign + notarize + package + upload
#
# Secrets required (set in GitHub repo Settings → Secrets → Actions):
# DEVELOPER_ID_CERTIFICATE — Base64-encoded .p12 file containing the
# "Developer ID Application" signing certificate
# DEVELOPER_ID_PASSWORD — Password used to protect the .p12 file
# APPLE_ID — Apple Developer account email address
# APPLE_TEAM_ID — 10-character Apple Developer Team ID
# APPLE_APP_PASSWORD — App-specific password generated at
# appleid.apple.com for notarytool auth
# =============================================================================
name: Build and Test
# --- Trigger conditions ------------------------------------------------------
on:
# Run on every push to the main branch
push:
branches: [main]
# Run on every pull request targeting main
pull_request:
branches: [main]
# Run when a GitHub Release is published (triggers the release job)
release:
types: [published]
# =============================================================================
# Job 1: Build and Test
# Compiles all targets and runs the test suite on every trigger.
# =============================================================================
jobs:
build-and-test:
# Use macOS 14 runner — provides Apple Silicon (M1) hardware for arm64 builds
runs-on: macos-14
# Human-readable name shown in the GitHub Actions UI
name: Build & Test
steps:
# --- Checkout the repository -------------------------------------------
# Fetches the full repo so swift build has access to all source files
- name: Checkout repository
uses: actions/checkout@v4
# --- Display Swift version ---------------------------------------------
# Logs the Swift toolchain version for build reproducibility and debugging
- name: Print Swift version
run: swift --version
# --- Build all targets --------------------------------------------------
# Compiles every target defined in Package.swift (CLI, GUI, Core, Tests)
# This catches compilation errors before running tests
- name: Build all targets
run: swift build --build-tests
# --- Run tests ----------------------------------------------------------
# Executes all test targets (unit + integration tests)
# --parallel runs test cases concurrently for faster feedback
- name: Run tests
run: swift test --parallel
# =============================================================================
# Job 2: Release
# Only runs when a GitHub Release is published. Builds, signs, notarizes,
# packages, and uploads the release artifacts.
# =============================================================================
release:
# Only run after build-and-test passes
needs: build-and-test
# Only trigger on release events (not on push or PR)
if: github.event_name == 'release'
# Apple Silicon runner for native arm64 builds
runs-on: macos-14
name: Sign, Notarize & Release
# Grant write access to upload assets to the GitHub Release
permissions:
contents: write
steps:
# --- Checkout -----------------------------------------------------------
- name: Checkout repository
uses: actions/checkout@v4
# --- Build release artifacts -------------------------------------------
# Runs the build script that produces the CLI binary and .app bundle
- name: Build release artifacts
run: |
# Extract version from the release tag (e.g., "v0.2" → "0.2")
VERSION="${GITHUB_REF_NAME#v}"
# Make the script executable (git may not preserve the +x bit)
chmod +x Scripts/build-release.sh
# Run the build with version and a unique run+attempt number for the bundle
# run_attempt ensures reruns of the same workflow produce unique bundle versions
./Scripts/build-release.sh "${VERSION}" "${{ github.run_number }}.${{ github.run_attempt }}"
# --- Import signing certificate ----------------------------------------
# The Developer ID certificate is stored as a base64-encoded secret.
# We decode it, import it into a temporary keychain, and set it as the
# default so codesign can find it automatically.
- name: Import signing certificate
env:
# Base64-encoded .p12 certificate file
CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE }}
# Password to unlock the .p12 file
CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_PASSWORD }}
run: |
# Decode the certificate from base64 into a .p12 file
echo "${CERTIFICATE_BASE64}" | base64 --decode > certificate.p12
# Create a temporary keychain to hold the signing certificate
# Using a temporary keychain avoids polluting the system keychain
KEYCHAIN_PATH="${RUNNER_TEMP}/signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"
# Create the keychain with a random password
security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
# Set it as the default keychain so codesign finds the certificate
security default-keychain -s "${KEYCHAIN_PATH}"
# Unlock the keychain (required before importing)
security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
# Import the .p12 into the keychain
# -T /usr/bin/codesign grants codesign access to use this certificate
security import certificate.p12 \
-k "${KEYCHAIN_PATH}" \
-P "${CERTIFICATE_PASSWORD}" \
-T /usr/bin/codesign
# Allow codesign to access the keychain without GUI prompts
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "${KEYCHAIN_PASSWORD}" \
"${KEYCHAIN_PATH}"
# Clean up the decoded certificate file
rm certificate.p12
# --- Sign the .app bundle ----------------------------------------------
# codesign applies the Developer ID signature required for distribution
# outside the Mac App Store (Gatekeeper checks this).
- name: Sign application
run: |
# --deep → sign all nested binaries and frameworks
# --force → replace any existing signature
# --options runtime → enable hardened runtime (required for notarization)
# --timestamp → include a secure timestamp (required for notarization)
codesign --deep --force \
--options runtime \
--timestamp \
--sign "Developer ID Application" \
Distribution/Insomnia.app
# Also sign the standalone CLI binary
codesign --force \
--options runtime \
--timestamp \
--sign "Developer ID Application" \
Distribution/insomnia
# --- Notarize the .app --------------------------------------------------
# Apple notarization scans the binary for malware and issues a ticket.
# Without notarization, macOS Gatekeeper blocks the app on first launch.
- name: Notarize application
env:
# Apple Developer account email
APPLE_ID: ${{ secrets.APPLE_ID }}
# Apple Developer Team ID (10-char alphanumeric)
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# App-specific password for notarytool authentication
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
# Create a zip of the .app for upload to Apple's notary service
# (notarytool accepts .zip, .dmg, or .pkg)
ditto -c -k --keepParent Distribution/Insomnia.app Distribution/Insomnia-notarize.zip
# Submit to Apple's notary service and wait for the result
# --wait blocks until Apple finishes scanning (usually 2-10 minutes)
xcrun notarytool submit Distribution/Insomnia-notarize.zip \
--apple-id "${APPLE_ID}" \
--team-id "${APPLE_TEAM_ID}" \
--password "${APPLE_APP_PASSWORD}" \
--wait
# Clean up the temporary zip — we'll create a DMG for distribution
rm Distribution/Insomnia-notarize.zip
# --- Staple the notarization ticket ------------------------------------
# Stapling embeds the notarization ticket directly in the .app bundle.
# This allows Gatekeeper to verify offline without contacting Apple's servers.
- name: Staple notarization ticket
run: |
xcrun stapler staple Distribution/Insomnia.app
# --- Create DMG ---------------------------------------------------------
# Package the .app into a DMG disk image for end-user distribution.
# DMGs are the standard macOS distribution format for non-App Store apps.
- name: Create DMG
run: |
# Extract the version from the release tag (e.g., "v1.2.0" → "1.2.0")
VERSION="${GITHUB_REF_NAME#v}"
# hdiutil creates the disk image:
# -volname → name shown when the DMG is mounted in Finder
# -srcfolder → directory containing the .app to include
# -ov → overwrite if the DMG already exists
# -format UDZO → compressed read-only image (standard for distribution)
hdiutil create \
-volname "Insomnia" \
-srcfolder Distribution/Insomnia.app \
-ov \
-format UDZO \
"Distribution/Insomnia-${VERSION}.dmg"
# --- Package CLI for Homebrew -------------------------------------------
# Create a tar.gz archive of the CLI binary for Homebrew formula distribution
- name: Package CLI archive
run: |
# Extract version from the release tag
VERSION="${GITHUB_REF_NAME#v}"
# Make the packaging script executable
chmod +x Scripts/package-for-homebrew.sh
# Run the Homebrew packaging script (creates archive + prints formula)
./Scripts/package-for-homebrew.sh "${VERSION}"
# --- Upload artifacts to GitHub Release --------------------------------
# Attaches the DMG and CLI archive to the GitHub Release page so users
# can download them directly from the repository's Releases tab.
- name: Upload release assets
env:
# GitHub token with write access to the release (auto-provided)
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Extract version from the release tag
VERSION="${GITHUB_REF_NAME#v}"
# Upload the DMG (GUI app installer)
gh release upload "${GITHUB_REF_NAME}" \
"Distribution/Insomnia-${VERSION}.dmg" \
--clobber
# Upload the CLI tar.gz (Homebrew / manual install)
gh release upload "${GITHUB_REF_NAME}" \
"Distribution/insomnia-${VERSION}-arm64-apple-macosx.tar.gz" \
--clobber
# Upload the standalone CLI binary (direct download)
gh release upload "${GITHUB_REF_NAME}" \
"Distribution/insomnia" \
--clobber
# --- Update Homebrew tap ---------------------------------------------------
# Pushes an updated cask definition to gordonbeeming/homebrew-tap so users
# can install/upgrade via `brew install --cask gordonbeeming/tap/insomnia`.
- name: Update Homebrew tap
env:
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
# Compute SHA256 of the DMG for the cask definition
DMG_SHA256="$(shasum -a 256 "Distribution/Insomnia-${VERSION}.dmg" | awk '{print $1}')"
# Configure SSH with the deploy key for push access to the tap repo
mkdir -p ~/.ssh
echo "${HOMEBREW_TAP_DEPLOY_KEY}" > ~/.ssh/homebrew_tap_key
chmod 600 ~/.ssh/homebrew_tap_key
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
export GIT_SSH_COMMAND="ssh -i ~/.ssh/homebrew_tap_key -o StrictHostKeyChecking=no"
# Clone the tap repo
git clone git@github.com:GordonBeeming/homebrew-tap.git /tmp/homebrew-tap
# Generate the cask definition with the new version and SHA
mkdir -p /tmp/homebrew-tap/Casks
cat > /tmp/homebrew-tap/Casks/insomnia.rb << 'CASK_EOF'
cask "insomnia" do
version "${VERSION}"
sha256 "${DMG_SHA256}"
url "https://github.com/gordonbeeming/insomnia/releases/download/v#{version}/Insomnia-#{version}.dmg"
name "Insomnia"
desc "Caffeinate utility — the tool that never sleeps"
homepage "https://github.com/gordonbeeming/insomnia"
depends_on macos: ">= :sonoma"
app "Insomnia.app"
zap trash: [
"~/Library/Application Support/Insomnia",
"~/Library/Preferences/com.gordonbeeming.insomnia.plist",
]
end
CASK_EOF
# Remove the leading whitespace from the heredoc
sed -i '' 's/^ //' /tmp/homebrew-tap/Casks/insomnia.rb
# Substitute shell variables (heredoc used single quotes to avoid YAML issues)
sed -i '' "s/\${VERSION}/${VERSION}/g" /tmp/homebrew-tap/Casks/insomnia.rb
sed -i '' "s/\${DMG_SHA256}/${DMG_SHA256}/g" /tmp/homebrew-tap/Casks/insomnia.rb
# Commit and push the updated cask
cd /tmp/homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/insomnia.rb
git commit -m "Update insomnia to ${VERSION}" || echo "No changes to commit"
git push
# Clean up the deploy key
rm -f ~/.ssh/homebrew_tap_key