Nightly Build #18
This file contains hidden or 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
| name: Nightly Build | |
| on: | |
| schedule: | |
| # Run every day at midnight UTC | |
| - cron: '0 0 * * *' | |
| workflow_dispatch: | |
| # Allow manual trigger for testing | |
| inputs: | |
| skip_notarize: | |
| description: 'Skip notarization (faster builds for debugging)' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: nightly-build | |
| cancel-in-progress: true | |
| jobs: | |
| check-for-changes: | |
| name: Check for New Commits | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_changes: ${{ steps.check.outputs.has_changes }} | |
| latest_tag: ${{ steps.check.outputs.latest_tag }} | |
| changelog: ${{ steps.changelog.outputs.changelog }} | |
| commit_count: ${{ steps.check.outputs.commit_count }} | |
| short_sha: ${{ steps.check.outputs.short_sha }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Check for changes since last tag | |
| id: check | |
| run: | | |
| # Find the latest stable release tag (vX.Y.Z format) | |
| LATEST_TAG=$(git tag --sort=-version:refname --list 'v*' | head -1) | |
| echo "latest_tag=${LATEST_TAG}" >> "$GITHUB_OUTPUT" | |
| echo "📌 Latest stable tag: ${LATEST_TAG}" | |
| SHORT_SHA=$(git rev-parse --short HEAD) | |
| echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" | |
| if [ -z "$LATEST_TAG" ]; then | |
| echo "No tags found — treating all commits as new" | |
| COMMIT_COUNT=$(git rev-list --count HEAD) | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "commit_count=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT" | |
| else | |
| # Count commits since last tag | |
| COMMIT_COUNT=$(git rev-list --count "${LATEST_TAG}..HEAD") | |
| echo "commit_count=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT" | |
| if [ "$COMMIT_COUNT" -eq 0 ]; then | |
| echo "✅ No new commits since ${LATEST_TAG} — skipping nightly build" | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "🔄 Found ${COMMIT_COUNT} new commit(s) since ${LATEST_TAG}" | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| - name: Generate changelog | |
| id: changelog | |
| if: steps.check.outputs.has_changes == 'true' | |
| run: | | |
| LATEST_TAG="${{ steps.check.outputs.latest_tag }}" | |
| # Build changelog from commits since last tag | |
| if [ -z "$LATEST_TAG" ]; then | |
| RANGE="HEAD" | |
| else | |
| RANGE="${LATEST_TAG}..HEAD" | |
| fi | |
| # Generate categorized changelog | |
| { | |
| echo "changelog<<CHANGELOG_EOF" | |
| # Features | |
| FEATS=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^feat" --no-merges 2>/dev/null || true) | |
| if [ -n "$FEATS" ]; then | |
| echo "### ✨ New Features" | |
| echo "$FEATS" | |
| echo "" | |
| fi | |
| # Fixes | |
| FIXES=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^fix" --no-merges 2>/dev/null || true) | |
| if [ -n "$FIXES" ]; then | |
| echo "### 🐛 Bug Fixes" | |
| echo "$FIXES" | |
| echo "" | |
| fi | |
| # UI changes | |
| UI=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^ui" --no-merges 2>/dev/null || true) | |
| if [ -n "$UI" ]; then | |
| echo "### 🎨 UI Improvements" | |
| echo "$UI" | |
| echo "" | |
| fi | |
| # CI/Chore/Docs | |
| OTHER=$(git log "$RANGE" --pretty=format:"- %s (%h)" --grep="^chore\|^ci\|^docs\|^refactor\|^perf\|^test" --no-merges 2>/dev/null || true) | |
| if [ -n "$OTHER" ]; then | |
| echo "### 🔧 Maintenance & Other" | |
| echo "$OTHER" | |
| echo "" | |
| fi | |
| # Uncategorized (commits not matching conventional commit prefixes) | |
| ALL_CATEGORIZED=$(git log "$RANGE" --pretty=format:"%h" --grep="^feat\|^fix\|^ui\|^chore\|^ci\|^docs\|^refactor\|^perf\|^test" --no-merges 2>/dev/null | sort || true) | |
| ALL_COMMITS=$(git log "$RANGE" --pretty=format:"%h" --no-merges 2>/dev/null | sort || true) | |
| UNCATEGORIZED_HASHES=$(comm -23 <(echo "$ALL_COMMITS") <(echo "$ALL_CATEGORIZED") 2>/dev/null || true) | |
| if [ -n "$UNCATEGORIZED_HASHES" ]; then | |
| UNCATEGORIZED="" | |
| while IFS= read -r hash; do | |
| [ -z "$hash" ] && continue | |
| MSG=$(git log -1 --pretty=format:"- %s (%h)" "$hash" 2>/dev/null || true) | |
| UNCATEGORIZED="${UNCATEGORIZED}${MSG}"$'\n' | |
| done <<< "$UNCATEGORIZED_HASHES" | |
| if [ -n "$(echo "$UNCATEGORIZED" | tr -d '[:space:]')" ]; then | |
| echo "### 📝 Other Changes" | |
| echo "$UNCATEGORIZED" | |
| echo "" | |
| fi | |
| fi | |
| echo "CHANGELOG_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| build-nightly: | |
| name: Build Nightly | |
| needs: check-for-changes | |
| if: needs.check-for-changes.outputs.has_changes == 'true' | |
| runs-on: macos-15 | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Cache SPM dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: .build | |
| key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }} | |
| restore-keys: | | |
| spm-${{ runner.os }}- | |
| - name: Run tests | |
| run: swift test | |
| - name: Initialize Xcode tools | |
| run: sudo xcodebuild -runFirstLaunch | |
| # ── Code Signing Setup ────────────────────────────────────────────────── | |
| - name: Import Developer ID certificate | |
| env: | |
| DEVELOPER_ID_CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }} | |
| DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }} | |
| run: | | |
| # Create a temporary keychain so the cert is isolated to this job | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain" | |
| KEYCHAIN_PASSWORD="$(openssl rand -hex 16)" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Decode and import the p12 | |
| echo "$DEVELOPER_ID_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" | |
| security import "$RUNNER_TEMP/cert.p12" \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -P "$DEVELOPER_ID_CERT_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| # Allow codesign to access the key without a passphrase prompt | |
| security set-key-partition-list \ | |
| -S apple-tool:,apple:,codesign: \ | |
| -s -k "$KEYCHAIN_PASSWORD" \ | |
| "$KEYCHAIN_PATH" | |
| # Add to keychain search list | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | xargs) | |
| # Store the keychain path for later steps | |
| echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" | |
| echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV" | |
| # Verify the identity | |
| IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" \ | |
| | grep "Developer ID Application" | head -1 | sed 's/.*"\(.*\)"/\1/') | |
| if [ -z "$IDENTITY" ]; then | |
| echo "❌ Developer ID certificate not found after import" | |
| exit 1 | |
| fi | |
| echo "✅ Imported: $IDENTITY" | |
| echo "CODE_SIGN_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" | |
| - name: Store notarization credentials | |
| if: ${{ !inputs.skip_notarize }} | |
| env: | |
| NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} | |
| NOTARIZE_TEAM_ID: ${{ secrets.NOTARIZE_TEAM_ID }} | |
| NOTARIZE_PASSWORD: ${{ secrets.NOTARIZE_PASSWORD }} | |
| run: | | |
| xcrun notarytool store-credentials "AC_PASSWORD" \ | |
| --apple-id "$NOTARIZE_APPLE_ID" \ | |
| --team-id "$NOTARIZE_TEAM_ID" \ | |
| --password "$NOTARIZE_PASSWORD" | |
| # ── Nightly Version ──────────────────────────────────────────────────── | |
| - name: Set nightly version | |
| run: | | |
| NIGHTLY_DATE=$(date -u +%Y%m%d) | |
| SHORT_SHA="${{ needs.check-for-changes.outputs.short_sha }}" | |
| # Extract the default version from APP_VERSION="${APP_VERSION:-X.Y.Z}" in build.sh | |
| BASE_VERSION=$(grep '^APP_VERSION=' scripts/build.sh | sed 's/.*:-\(.*\)}.*/\1/') | |
| if [ -z "$BASE_VERSION" ]; then | |
| BASE_VERSION="0.5.0" | |
| fi | |
| APP_VERSION="${BASE_VERSION}-nightly.${NIGHTLY_DATE}+${SHORT_SHA}" | |
| echo "APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV" | |
| echo "📌 Nightly version: ${APP_VERSION}" | |
| # ── Bundled Tiny Model ───────────────────────────────────────────────── | |
| - name: Set up Python for huggingface-cli | |
| run: | | |
| python3 -m venv "$RUNNER_TEMP/hf-venv" | |
| source "$RUNNER_TEMP/hf-venv/bin/activate" | |
| pip install -U huggingface_hub | |
| echo "$RUNNER_TEMP/hf-venv/bin" >> "$GITHUB_PATH" | |
| - name: Fetch bundled tiny model from Hugging Face | |
| run: | | |
| MODEL_STAGE="$RUNNER_TEMP/whisperkit-coreml" | |
| hf download argmaxinc/whisperkit-coreml \ | |
| --include "openai_whisper-tiny/*" \ | |
| --local-dir "$MODEL_STAGE" | |
| hf download openai/whisper-tiny \ | |
| tokenizer.json tokenizer_config.json \ | |
| --local-dir "$MODEL_STAGE/openai_whisper-tiny" | |
| rm -rf "$MODEL_STAGE/.cache" "$MODEL_STAGE/openai_whisper-tiny/.cache" | |
| test -d "$MODEL_STAGE/openai_whisper-tiny" | |
| test -f "$MODEL_STAGE/openai_whisper-tiny/tokenizer.json" | |
| test -f "$MODEL_STAGE/openai_whisper-tiny/tokenizer_config.json" | |
| test -d "$MODEL_STAGE/openai_whisper-tiny/MelSpectrogram.mlmodelc" | |
| test -d "$MODEL_STAGE/openai_whisper-tiny/AudioEncoder.mlmodelc" | |
| test -d "$MODEL_STAGE/openai_whisper-tiny/TextDecoder.mlmodelc" | |
| echo "VOCAMAC_BUNDLED_MODEL_SOURCE=$MODEL_STAGE" >> "$GITHUB_ENV" | |
| # ── Build & Package ───────────────────────────────────────────────────── | |
| - name: Build signed, notarized DMG via dist.sh | |
| run: | | |
| DIST_FLAGS="" | |
| if [ "${{ inputs.skip_notarize }}" = "true" ]; then | |
| DIST_FLAGS="--skip-notarize" | |
| fi | |
| ./scripts/dist.sh $DIST_FLAGS | |
| env: | |
| APP_VERSION: ${{ env.APP_VERSION }} | |
| VOCAMAC_BUNDLED_MODEL_SOURCE: ${{ env.VOCAMAC_BUNDLED_MODEL_SOURCE }} | |
| - name: Verify code signature | |
| run: codesign -v --deep --strict VocaMac.app | |
| - name: Rename artifacts for nightly | |
| run: | | |
| NIGHTLY_DATE=$(date -u +%Y%m%d) | |
| SHORT_SHA="${{ needs.check-for-changes.outputs.short_sha }}" | |
| ARCH=$(uname -m) | |
| # Rename the DMG from dist/ to nightly naming convention | |
| DIST_DMG=$(ls dist/VocaMac-*.dmg | head -1) | |
| DMG_NAME="VocaMac-nightly-${NIGHTLY_DATE}-${SHORT_SHA}-${ARCH}.dmg" | |
| cp "$DIST_DMG" "$DMG_NAME" | |
| echo "DMG_NAME=${DMG_NAME}" >> "$GITHUB_ENV" | |
| # Create ZIP from the signed .app | |
| ZIP_NAME="VocaMac-nightly-${NIGHTLY_DATE}-${SHORT_SHA}-${ARCH}.zip" | |
| ditto -c -k --sequesterRsrc --keepParent VocaMac.app "$ZIP_NAME" | |
| echo "ZIP_NAME=${ZIP_NAME}" >> "$GITHUB_ENV" | |
| echo "Artifacts created:" | |
| ls -lh "$DMG_NAME" "$ZIP_NAME" | |
| - name: Generate checksums | |
| run: | | |
| shasum -a 256 VocaMac-nightly-*.dmg VocaMac-nightly-*.zip > checksums.txt | |
| cat checksums.txt | |
| # ── Cleanup ───────────────────────────────────────────────────────────── | |
| - name: Delete temporary keychain | |
| if: always() | |
| run: | | |
| security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true | |
| # ── Release ───────────────────────────────────────────────────────────── | |
| - name: Delete existing nightly release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Delete the existing nightly release (if any) to keep only one active | |
| echo "🗑️ Checking for existing nightly release..." | |
| if gh release view nightly &>/dev/null; then | |
| echo " Found existing nightly release — deleting..." | |
| gh release delete nightly --yes --cleanup-tag | |
| echo " ✅ Old nightly release deleted" | |
| else | |
| echo " No existing nightly release found" | |
| fi | |
| - name: Create nightly release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CHANGELOG: ${{ needs.check-for-changes.outputs.changelog }} | |
| run: | | |
| LATEST_TAG="${{ needs.check-for-changes.outputs.latest_tag }}" | |
| COMMIT_COUNT="${{ needs.check-for-changes.outputs.commit_count }}" | |
| SHORT_SHA="${{ needs.check-for-changes.outputs.short_sha }}" | |
| NIGHTLY_DATE=$(date -u +%Y-%m-%d) | |
| REPO_URL="${{ github.server_url }}/${{ github.repository }}" | |
| # Build the stable release link | |
| if [ -n "$LATEST_TAG" ]; then | |
| STABLE_LINK="[**${LATEST_TAG}**](${REPO_URL}/releases/tag/${LATEST_TAG})" | |
| else | |
| STABLE_LINK="_No stable release available yet._" | |
| fi | |
| # Determine signing status for release notes | |
| if [ "${{ inputs.skip_notarize }}" = "true" ]; then | |
| SIGNING_NOTE="> **Note:** This nightly build is **Developer ID signed** but not notarized." | |
| SIGNING_NOTE="${SIGNING_NOTE}"$'\n'"> macOS may show a Gatekeeper warning on first open." | |
| SIGNING_NOTE="${SIGNING_NOTE}"$'\n'"> To bypass: right-click VocaMac.app → Open → click Open in the dialog." | |
| else | |
| SIGNING_NOTE="> This nightly build is **Developer ID signed and notarized** by Apple." | |
| SIGNING_NOTE="${SIGNING_NOTE}"$'\n'"> macOS will open it without any security warnings." | |
| fi | |
| # Write release notes to a file (avoids heredoc indentation issues) | |
| { | |
| echo "## ⚠️ Nightly Build — Not for Production Use" | |
| echo "" | |
| echo "> **This is an automated nightly build from the latest \`main\` branch.**" | |
| echo "> It is intended for **developers and testers** who want to try the latest changes" | |
| echo "> before they are included in a stable release." | |
| echo ">" | |
| echo "> **This build may be unstable, contain bugs, or include incomplete features.**" | |
| echo "> Do not use it as your daily driver. If you encounter issues, please" | |
| echo "> [report them](${REPO_URL}/issues/new) — your feedback helps us improve!" | |
| echo "" | |
| echo "### 🏷️ Latest Stable Release" | |
| echo "" | |
| echo "👉 For a reliable experience, use the latest stable release: ${STABLE_LINK}" | |
| echo "" | |
| echo "---" | |
| echo "" | |
| echo "### 📋 What's Changed Since ${LATEST_TAG:-the beginning}" | |
| echo "" | |
| echo "_${COMMIT_COUNT} commit(s) since the last stable release (${LATEST_TAG:-N/A}) • built from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${SHORT_SHA})_" | |
| echo "" | |
| echo "$CHANGELOG" | |
| echo "" | |
| echo "---" | |
| echo "" | |
| echo "### 📥 Installation" | |
| echo "" | |
| echo "1. Download \`${DMG_NAME}\`" | |
| echo "2. Open the DMG and drag VocaMac to Applications" | |
| echo "3. Open VocaMac from Applications" | |
| echo "4. Grant Microphone, Accessibility, and Input Monitoring permissions when prompted" | |
| echo "" | |
| echo "$SIGNING_NOTE" | |
| echo "" | |
| echo "### Checksums (SHA-256)" | |
| echo "\`\`\`" | |
| cat checksums.txt | |
| echo "\`\`\`" | |
| echo "" | |
| echo "### Requirements" | |
| echo "- macOS 13 (Ventura) or later" | |
| echo "- Apple Silicon (arm64)" | |
| } > release-notes.md | |
| gh release create nightly \ | |
| --title "🌙 Nightly Build — ${NIGHTLY_DATE}" \ | |
| --notes-file release-notes.md \ | |
| --prerelease \ | |
| --target main \ | |
| VocaMac-nightly-*.dmg \ | |
| VocaMac-nightly-*.zip \ | |
| checksums.txt | |
| echo "✅ Nightly release created!" |