Refine Verge Slim title interactions #78
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: CI | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| tags: | |
| - "*" | |
| pull_request: | |
| workflow_dispatch: | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| linux-build: | |
| name: Linux Build | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 18.x | |
| cache: npm | |
| cache-dependency-path: npm-shrinkwrap.json | |
| - name: Cache Electron build tools | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/electron | |
| ~/.cache/electron-builder | |
| ~/.npm/_prebuilds | |
| key: ${{ runner.os }}-${{ runner.arch }}-electron-tools-linux-${{ hashFiles('package.json', 'npm-shrinkwrap.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-electron-tools-linux- | |
| - name: Install Linux build dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y pkg-config libsecret-1-dev | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Ensure Electron binary is installed | |
| run: node node_modules/electron/install.js | |
| - name: Build AppImage | |
| run: npm run electron:build:linux -- --publish never | |
| - name: Upload Linux artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-build | |
| path: | | |
| dist_electron/*.AppImage | |
| dist_electron/latest-linux.yml | |
| dist_electron/linux-unpacked/** | |
| if-no-files-found: error | |
| windows-build: | |
| name: Windows Build | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 18.x | |
| cache: npm | |
| cache-dependency-path: npm-shrinkwrap.json | |
| - name: Cache Electron build tools | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/electron | |
| ~/.cache/electron-builder | |
| ~/.npm/_prebuilds | |
| key: ${{ runner.os }}-${{ runner.arch }}-electron-tools-win-${{ hashFiles('package.json', 'npm-shrinkwrap.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-electron-tools-win- | |
| - name: Install Linux build dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y pkg-config libsecret-1-dev | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Ensure Electron binary is installed | |
| run: node node_modules/electron/install.js | |
| - name: Build Windows exe from Linux | |
| run: npm run electron:build:win -- --publish never | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: "false" | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-build | |
| path: | | |
| dist_electron/*.exe | |
| if-no-files-found: error | |
| macos-build: | |
| name: macOS Build | |
| runs-on: macos-15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 18.x | |
| architecture: x64 | |
| cache: npm | |
| cache-dependency-path: npm-shrinkwrap.json | |
| - name: Cache Electron build tools | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/Library/Caches/electron | |
| ~/Library/Caches/electron-builder | |
| ~/.npm/_prebuilds | |
| key: ${{ runner.os }}-${{ runner.arch }}-electron-tools-macos-${{ hashFiles('package.json', 'npm-shrinkwrap.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-electron-tools-macos- | |
| - name: Install dependencies | |
| run: npm ci | |
| env: | |
| npm_config_arch: x64 | |
| npm_config_target_arch: x64 | |
| - name: Ensure Electron binary is installed | |
| run: node node_modules/electron/install.js | |
| env: | |
| npm_config_arch: x64 | |
| npm_config_target_arch: x64 | |
| - name: Import Developer ID certificate | |
| run: | | |
| set -euo pipefail | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -hex 16)" | |
| if ! echo "$MACOS_CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12" 2>/dev/null; then | |
| echo "$MACOS_CERT_P12_BASE64" | base64 -D > "$RUNNER_TEMP/cert.p12" | |
| fi | |
| 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 list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| security import "$RUNNER_TEMP/cert.p12" \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -P "$MACOS_CERT_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| SIGN_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n1)" | |
| if [ -z "${SIGN_IDENTITY:-}" ]; then | |
| echo "Developer ID Application identity not found in imported certificate" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true | |
| exit 1 | |
| fi | |
| SIGN_IDENTITY_NO_PREFIX="${SIGN_IDENTITY#Developer ID Application: }" | |
| echo "SIGN_IDENTITY=$SIGN_IDENTITY" >> "$GITHUB_ENV" | |
| echo "SIGN_IDENTITY_NO_PREFIX=$SIGN_IDENTITY_NO_PREFIX" >> "$GITHUB_ENV" | |
| echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV" | |
| env: | |
| MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} | |
| MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} | |
| - name: Build macOS app bundle | |
| run: npm run electron:build -- --mac dir --x64 --publish never | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: "false" | |
| CSC_NAME: ${{ env.SIGN_IDENTITY_NO_PREFIX }} | |
| CSC_KEYCHAIN: ${{ env.CSC_KEYCHAIN }} | |
| NOTARIZE: "false" | |
| npm_config_arch: x64 | |
| npm_config_target_arch: x64 | |
| - name: Sign nested binaries and app bundle | |
| run: | | |
| set -euo pipefail | |
| APP_PATH="$(find dist_electron/mac -maxdepth 1 -name '*.app' -type d | head -n1)" | |
| ENTITLEMENTS_PATH="dist_electron/entitlements.mac.plist" | |
| if [ -z "${APP_PATH:-}" ]; then | |
| echo "No .app found in dist_electron/mac" | |
| exit 1 | |
| fi | |
| if [ ! -f "$ENTITLEMENTS_PATH" ]; then | |
| echo "Missing entitlements file at $ENTITLEMENTS_PATH" | |
| exit 1 | |
| fi | |
| xattr -cr "$APP_PATH" || true | |
| # Sign every Mach-O file (executables, dylibs, helper binaries, bundled Tor files). | |
| while IFS= read -r -d '' f; do | |
| if file "$f" | grep -q "Mach-O"; then | |
| codesign --force --timestamp --options runtime --sign "$SIGN_IDENTITY" "$f" | |
| fi | |
| done < <(find "$APP_PATH" -type f -print0) | |
| # Sign nested code bundles from deepest to shallowest, then the root app. | |
| while IFS= read -r -d '' bundle; do | |
| codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGN_IDENTITY" "$bundle" | |
| done < <(find "$APP_PATH" -depth -type d \( -name "*.framework" -o -name "*.app" -o -name "*.xpc" -o -name "*.appex" \) -print0) | |
| codesign --force --timestamp --options runtime --entitlements "$ENTITLEMENTS_PATH" --sign "$SIGN_IDENTITY" "$APP_PATH" | |
| codesign --verify --deep --strict "$APP_PATH" | |
| codesign --display --verbose=2 "$APP_PATH" | |
| codesign -d --entitlements :- "$APP_PATH" | |
| env: | |
| SIGN_IDENTITY: ${{ env.SIGN_IDENTITY }} | |
| - name: Create DMG from app bundle | |
| run: | | |
| set -euo pipefail | |
| APP_PATH="$(find dist_electron/mac -maxdepth 1 -name '*.app' -type d | head -n1)" | |
| APP_NAME="$(basename "$APP_PATH" .app)" | |
| APP_VERSION="$(node -p "require('./package.json').version")" | |
| DMG_PATH="dist_electron/${APP_NAME}-${APP_VERSION}.dmg" | |
| VOL_PATH="/Volumes/${APP_NAME}" | |
| if mount | grep -Fq "on ${VOL_PATH} "; then | |
| hdiutil detach "$VOL_PATH" -force || true | |
| sleep 2 | |
| fi | |
| SRC_DIR="$RUNNER_TEMP/dmg-src" | |
| TMP_DMG="$RUNNER_TEMP/${APP_NAME}-${APP_VERSION}.dmg" | |
| rm -rf "$SRC_DIR" | |
| mkdir -p "$SRC_DIR" | |
| cp -R "$APP_PATH" "$SRC_DIR/" | |
| for attempt in 1 2 3; do | |
| if hdiutil create "$TMP_DMG" -srcfolder "$SRC_DIR" -volname "$APP_NAME" -ov -format UDZO; then | |
| break | |
| fi | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "Failed to create DMG after 3 attempts" | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| mv -f "$TMP_DMG" "$DMG_PATH" | |
| echo "Created $DMG_PATH" | |
| - name: Sign DMG | |
| run: | | |
| set -euo pipefail | |
| DMG_PATH="$(find dist_electron -maxdepth 1 -name '*.dmg' -type f | head -n1)" | |
| if [ -z "${DMG_PATH:-}" ]; then | |
| echo "No DMG found to sign" | |
| exit 1 | |
| fi | |
| codesign --force --sign "$SIGN_IDENTITY" "$DMG_PATH" | |
| codesign --verify --verbose "$DMG_PATH" | |
| codesign --display --verbose=2 "$DMG_PATH" | |
| env: | |
| SIGN_IDENTITY: ${{ env.SIGN_IDENTITY }} | |
| - name: Smoke test app launch from DMG (30s) | |
| run: | | |
| set -euo pipefail | |
| DMG_PATH="$(find dist_electron -maxdepth 1 -name '*.dmg' -type f | head -n1)" | |
| if [ -z "${DMG_PATH:-}" ]; then | |
| echo "No DMG found to test" | |
| exit 1 | |
| fi | |
| MOUNT_OUTPUT="$(hdiutil attach "$DMG_PATH" -nobrowse -readonly)" | |
| MOUNT_DEVICE="$(echo "$MOUNT_OUTPUT" | awk '/^\/dev\// {print $1; exit}')" | |
| MOUNT_POINT="$(echo "$MOUNT_OUTPUT" | awk '/\/Volumes\// {print substr($0, index($0, "/Volumes/")); exit}')" | |
| APP_PATH="$(find "$MOUNT_POINT" -maxdepth 1 -name '*.app' -type d | head -n1)" | |
| if [ -z "${APP_PATH:-}" ]; then | |
| echo "No .app found in mounted DMG" | |
| echo "$MOUNT_OUTPUT" | |
| exit 1 | |
| fi | |
| cleanup() { | |
| pkill -f "$APP_PATH/Contents/MacOS" || true | |
| if [ -n "${APP_PID:-}" ]; then | |
| kill "$APP_PID" >/dev/null 2>&1 || true | |
| sleep 2 | |
| kill -9 "$APP_PID" >/dev/null 2>&1 || true | |
| fi | |
| hdiutil detach "$MOUNT_DEVICE" -force >/dev/null 2>&1 || true | |
| } | |
| trap cleanup EXIT | |
| APP_EXECUTABLE="$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$APP_PATH/Contents/Info.plist")" | |
| APP_BIN="$APP_PATH/Contents/MacOS/$APP_EXECUTABLE" | |
| if [ ! -x "$APP_BIN" ]; then | |
| echo "App binary not executable at $APP_BIN" | |
| exit 1 | |
| fi | |
| "$APP_BIN" & | |
| APP_PID=$! | |
| sleep 30 | |
| if ! kill -0 "$APP_PID" >/dev/null 2>&1; then | |
| echo "App exited before 30-second smoke window" | |
| exit 1 | |
| fi | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-build | |
| path: | | |
| dist_electron/*.dmg | |
| dist_electron/mac/** | |
| if-no-files-found: error | |
| macos-notarize: | |
| name: macOS Notarize | |
| runs-on: macos-15 | |
| needs: | |
| - macos-build | |
| steps: | |
| - name: Download previous notary state (if present) | |
| uses: actions/download-artifact@v4 | |
| continue-on-error: true | |
| with: | |
| name: macos-notary-state | |
| path: notary_state | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: macos-build | |
| path: notarize_input | |
| - name: Notarize and staple DMG | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then | |
| echo "Missing one or more required Apple notarization secrets: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID" | |
| exit 1 | |
| fi | |
| DMG_PATH="$(find notarize_input -name '*.dmg' -type f | head -n1)" | |
| if [ -z "${DMG_PATH:-}" ]; then | |
| echo "No DMG found to notarize" | |
| exit 1 | |
| fi | |
| if [ -f "notary_state/notary_submission_id.txt" ]; then | |
| SUBMISSION_ID="$(cat notary_state/notary_submission_id.txt)" | |
| echo "Reusing prior notary submission id: $SUBMISSION_ID" | |
| else | |
| SUBMIT_OUTPUT="$(xcrun notarytool submit "$DMG_PATH" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --output-format json)" | |
| echo "$SUBMIT_OUTPUT" | |
| SUBMISSION_ID="$(echo "$SUBMIT_OUTPUT" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" | |
| if [ -z "${SUBMISSION_ID:-}" ]; then | |
| echo "Failed to parse notarization submission id" | |
| exit 1 | |
| fi | |
| fi | |
| echo "Notary submission id: $SUBMISSION_ID" | |
| mkdir -p notary_state | |
| echo "$SUBMISSION_ID" > notary_state/notary_submission_id.txt | |
| set +e | |
| xcrun notarytool wait "$SUBMISSION_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --timeout 90m | |
| WAIT_RC=$? | |
| set -e | |
| INFO_OUTPUT="$(xcrun notarytool info "$SUBMISSION_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --output-format json || true)" | |
| echo "$INFO_OUTPUT" | |
| STATUS="$(echo "$INFO_OUTPUT" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" | |
| if [ "$WAIT_RC" -ne 0 ]; then | |
| echo "Notarization failed or timed out. Submission id: $SUBMISSION_ID" | |
| echo "Printing Apple notary log for diagnostics" | |
| xcrun notarytool log "$SUBMISSION_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" || true | |
| exit "$WAIT_RC" | |
| fi | |
| if [ "${STATUS:-}" != "Accepted" ]; then | |
| echo "Notarization did not return Accepted status (status=${STATUS:-unknown}); printing Apple notary log" | |
| xcrun notarytool log "$SUBMISSION_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" || true | |
| exit 1 | |
| fi | |
| xcrun stapler staple "$DMG_PATH" | |
| xcrun stapler validate "$DMG_PATH" | |
| set +e | |
| spctl --assess --type open --verbose "$DMG_PATH" | |
| SPCTL_RC=$? | |
| set -e | |
| if [ "$SPCTL_RC" -ne 0 ]; then | |
| echo "::warning::spctl assessment rejected the DMG in CI (often \"source=Insufficient Context\") despite successful notarization/stapling." | |
| fi | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| - name: Upload notary state | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-notary-state | |
| path: notary_state/notary_submission_id.txt | |
| if-no-files-found: ignore | |
| - name: Upload notarized macOS DMG | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-notarized | |
| path: notarize_input/*.dmg | |
| if-no-files-found: error |