Release - Build & Sign Installers #85
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
| # ============================================================================= | |
| # release.yml - Build Row-Bot installers | |
| # | |
| # Triggers: | |
| # - Manual workflow_dispatch builds Windows installer, build macOS installer, | |
| # and sign the macOS outputs | |
| # | |
| # Jobs: | |
| # test - run tests/test_suite.py before any installer build | |
| # build-windows - self-contained unsigned .exe (Inno Setup) | |
| # build-macos - signed .app/.pkg/.dmg (manual notarization later) | |
| # build-linux - self-contained XDG user-install tarball | |
| # | |
| # Notarization is handled by two separate manual workflows: | |
| # notarize-submit.yml - submit DMG to Apple (after testing) | |
| # notarize-check.yml - check status, staple when accepted | |
| # | |
| # Required secrets (set in repo Settings - Secrets - Actions): | |
| # APPLE_APPLICATION_P12 Base64 of Developer ID Application .p12 | |
| # APPLE_INSTALLER_P12 Base64 of Developer ID Installer .p12 | |
| # APPLE_P12_PASSWORD Fallback password for .p12 files | |
| # APPLE_APPLICATION_P12_PASSWORD Optional password override for app .p12 | |
| # APPLE_INSTALLER_P12_PASSWORD Optional password override for installer .p12 | |
| # APPLE_ID Apple ID email (for notarization) | |
| # APPLE_APP_PASSWORD App-specific password (appleid.apple.com) | |
| # APPLE_TEAM_ID Apple Developer Team ID | |
| # | |
| # Windows signing (Certum) is intentionally local-only on the maintainer's | |
| # Windows machine. Do not add Certum credentials to GitHub Actions unless that | |
| # policy changes deliberately. | |
| # ============================================================================= | |
| name: Release - Build & Sign Installers | |
| on: | |
| workflow_dispatch: | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Tracks the version this workflow is currently shipping. Kept in sync with | |
| # `installer/row_bot_setup.iss` (#define MyAppVersion) and `src/row_bot/version.py`. | |
| env: | |
| ROW_BOT_VERSION: "4.1.0" | |
| WINDOWS_PYTHON_VERSION: "3.13.2" | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Run test suite before building installers | |
| # --------------------------------------------------------------------------- | |
| test: | |
| runs-on: macos-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| - name: Install Ollama | |
| run: | | |
| brew install portaudio | |
| brew install ollama | |
| ollama serve & | |
| sleep 5 | |
| ollama pull qwen3:1.7b | |
| - name: Install Python packages | |
| run: python -m pip install -r requirements.txt pytest | |
| - name: Verify required runtime dependencies | |
| run: python scripts/verify_runtime_dependencies.py | |
| - name: Run focused startup hardening tests | |
| run: python -m pytest tests/test_startup_hardening.py tests/test_app_port.py tests/test_linux_support.py | |
| - name: Run focused provider model selection tests | |
| run: python -m pytest tests/test_provider_catalog.py tests/test_provider_selection.py tests/test_provider_runtime.py tests/test_row_bot_status_media.py tests/test_provider_subscription_auth.py tests/test_atlascloud_first_class_provider.py tests/test_openai_compatible_transport.py tests/test_claude_subscription_auth.py tests/test_claude_subscription_transport.py | |
| - name: Run test suite | |
| run: python tests/test_suite.py | |
| # --------------------------------------------------------------------------- | |
| # Build Windows installer (.exe) - self-contained | |
| # --------------------------------------------------------------------------- | |
| build-windows: | |
| needs: test | |
| runs-on: windows-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # System Python (used by build script to find tkinter for bundling) | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ env.WINDOWS_PYTHON_VERSION }} | |
| - name: Install Inno Setup | |
| run: choco install innosetup --yes --no-progress | |
| - name: Build self-contained installer | |
| run: | | |
| Write-Host "Building Windows installer..." | |
| .\installer\build_installer.ps1 -PythonVersion "${{ env.WINDOWS_PYTHON_VERSION }}" | |
| # Windows code signing intentionally disabled. | |
| # The official Windows installer is signed locally by the maintainer with | |
| # Certum/SimplySign, verified with signtool, then uploaded to the release. | |
| - name: Upload Windows installer | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: Row-Bot-Windows | |
| path: dist/Row-Bot-*-Windows-*.exe | |
| retention-days: 90 | |
| # --------------------------------------------------------------------------- | |
| # Build Linux tarball - self-contained, browser/no-tray baseline | |
| # --------------------------------------------------------------------------- | |
| build-linux: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Linux build/runtime libraries | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends rsync binutils libgl1 libegl1 libglib2.0-0 libxcb-cursor0 libportaudio2 | |
| - name: Validate Linux installer scripts | |
| run: bash -n build_linux_app.sh installer/build_linux_app.sh installer/install-linux.sh | |
| - name: Build Linux package | |
| run: | | |
| chmod +x installer/build_linux_app.sh | |
| ./installer/build_linux_app.sh "$ROW_BOT_VERSION" | |
| - name: Smoke Linux package | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$RUNNER_TEMP/row-bot-linux-smoke" | |
| tar -xzf dist/Row-Bot-*-Linux-*.tar.gz -C "$RUNNER_TEMP/row-bot-linux-smoke" | |
| PACKAGE_ROOT="$(find "$RUNNER_TEMP/row-bot-linux-smoke" -maxdepth 1 -type d -name 'Row-Bot-*-Linux-*' | head -n 1)" | |
| if [ -z "$PACKAGE_ROOT" ]; then | |
| echo "::error::Linux package root not found after extraction" | |
| exit 1 | |
| fi | |
| chmod +x "$PACKAGE_ROOT/bin/row-bot" | |
| export HOME="$RUNNER_TEMP/row-bot-linux-home" | |
| export XDG_DATA_HOME="$RUNNER_TEMP/row-bot-linux-xdg" | |
| mkdir -p "$HOME" "$XDG_DATA_HOME" | |
| bash "$PACKAGE_ROOT/install.sh" | |
| python scripts/smoke_app.py \ | |
| --port 8080 \ | |
| --timeout 180 \ | |
| --cwd "$XDG_DATA_HOME/row-bot/current/app" \ | |
| -- "$HOME/.local/bin/row-bot" | |
| python scripts/smoke_app.py \ | |
| --port 8091 \ | |
| --timeout 120 \ | |
| --cwd "$XDG_DATA_HOME/row-bot/current/app" \ | |
| -- "$HOME/.local/bin/row-bot" --server --no-open --port 8091 --no-ollama | |
| - name: Upload Linux package | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: Row-Bot-Linux | |
| path: dist/Row-Bot-*-Linux-*.tar.gz | |
| retention-days: 90 | |
| # --------------------------------------------------------------------------- | |
| # Build macOS installers (.pkg + .dmg) - signed, manual notarization later | |
| # --------------------------------------------------------------------------- | |
| build-macos: | |
| needs: test | |
| runs-on: macos-latest | |
| timeout-minutes: 180 | |
| env: | |
| CODESIGN_IDENTITY: "Developer ID Application: Siddharth Sachar (33PQRTZLNW)" | |
| PKG_SIGN_IDENTITY: "Developer ID Installer: Siddharth Sachar (33PQRTZLNW)" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # Import Apple certificates into a temporary keychain. | |
| - name: Import Apple certificates | |
| env: | |
| APPLICATION_P12: ${{ secrets.APPLE_APPLICATION_P12 }} | |
| INSTALLER_P12: ${{ secrets.APPLE_INSTALLER_P12 }} | |
| P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }} | |
| APPLICATION_P12_PASSWORD: ${{ secrets.APPLE_APPLICATION_P12_PASSWORD }} | |
| INSTALLER_P12_PASSWORD: ${{ secrets.APPLE_INSTALLER_P12_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| fail() { | |
| echo "::error::$1" | |
| exit 1 | |
| } | |
| decode_b64_to_file() { | |
| local secret_value="$1" | |
| local out_file="$2" | |
| local label="$3" | |
| if [ -z "$secret_value" ]; then | |
| fail "$label secret is empty or missing" | |
| fi | |
| if ! printf '%s' "$secret_value" | base64 --decode > "$out_file" 2>"$RUNNER_TEMP/${label}.decode.err"; then | |
| echo "base64 decode stderr for $label:" | |
| cat "$RUNNER_TEMP/${label}.decode.err" || true | |
| fail "$label is not valid base64 (expected raw base64 of .p12 bytes)" | |
| fi | |
| if [ ! -s "$out_file" ]; then | |
| fail "$label decoded to an empty file" | |
| fi | |
| } | |
| verify_pkcs12_password() { | |
| local p12_file="$1" | |
| local password="$2" | |
| local label="$3" | |
| if [ -z "$password" ]; then | |
| fail "No password provided for $label (.p12 import requires its export password)" | |
| fi | |
| if ! openssl pkcs12 -in "$p12_file" -noout -info -passin pass:"$password" >"$RUNNER_TEMP/${label}.openssl.out" 2>"$RUNNER_TEMP/${label}.openssl.err"; then | |
| echo "openssl stderr for $label:" | |
| sed -n '1,80p' "$RUNNER_TEMP/${label}.openssl.err" || true | |
| fail "$label failed PKCS12 validation; likely wrong password for this specific .p12 or corrupted secret data" | |
| fi | |
| } | |
| reencode_pkcs12_for_macos() { | |
| local in_file="$1" | |
| local out_file="$2" | |
| local password="$3" | |
| local label="$4" | |
| local pem_file="$RUNNER_TEMP/${label}.repack.pem" | |
| # Step 1: extract cert+private key from source PKCS12 | |
| if ! openssl pkcs12 -in "$in_file" -passin pass:"$password" \ | |
| -nodes -out "$pem_file" >"$RUNNER_TEMP/${label}.repack.out" 2>"$RUNNER_TEMP/${label}.repack.err"; then | |
| echo "openssl repack stderr for $label:" | |
| sed -n '1,120p' "$RUNNER_TEMP/${label}.repack.err" || true | |
| fail "$label could not be decoded for repack" | |
| fi | |
| # Step 2: re-export with legacy-compatible PBE/MAC expected by macOS tooling | |
| if ! openssl pkcs12 -export -in "$pem_file" \ | |
| -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 \ | |
| -passout pass:"$password" -out "$out_file" >>"$RUNNER_TEMP/${label}.repack.out" 2>>"$RUNNER_TEMP/${label}.repack.err"; then | |
| echo "openssl repack stderr for $label:" | |
| sed -n '1,120p' "$RUNNER_TEMP/${label}.repack.err" || true | |
| fail "$label could not be repacked into macOS-compatible PKCS12" | |
| fi | |
| } | |
| import_pkcs12() { | |
| local p12_file="$1" | |
| local password="$2" | |
| local label="$3" | |
| shift 3 | |
| local trusted_apps=("$@") | |
| local cmd=(security import "$p12_file" -k "$KEYCHAIN_PATH" -f pkcs12 -P "$password") | |
| for app in "${trusted_apps[@]}"; do | |
| cmd+=( -T "$app" ) | |
| done | |
| if ! "${cmd[@]}" >"$RUNNER_TEMP/${label}.security_import.out" 2>"$RUNNER_TEMP/${label}.security_import.err"; then | |
| if grep -qi "already exists in the keychain" "$RUNNER_TEMP/${label}.security_import.err"; then | |
| echo "::warning::$label already exists in keychain, continuing" | |
| return 0 | |
| fi | |
| echo "security import stderr for $label:" | |
| sed -n '1,120p' "$RUNNER_TEMP/${label}.security_import.err" || true | |
| fail "$label failed during security import" | |
| fi | |
| } | |
| # Create a temporary keychain for this build | |
| KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -hex 32)" | |
| APP_P12_PASSWORD="${APPLICATION_P12_PASSWORD:-$P12_PASSWORD}" | |
| INST_P12_PASSWORD="${INSTALLER_P12_PASSWORD:-$P12_PASSWORD}" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Import Developer ID Application certificate | |
| decode_b64_to_file "$APPLICATION_P12" "$RUNNER_TEMP/app.p12" "APPLE_APPLICATION_P12" | |
| verify_pkcs12_password "$RUNNER_TEMP/app.p12" "$APP_P12_PASSWORD" "APPLE_APPLICATION_P12" | |
| reencode_pkcs12_for_macos "$RUNNER_TEMP/app.p12" "$RUNNER_TEMP/app.macos.p12" "$APP_P12_PASSWORD" "APPLE_APPLICATION_P12" | |
| import_pkcs12 "$RUNNER_TEMP/app.macos.p12" "$APP_P12_PASSWORD" "APPLE_APPLICATION_P12" /usr/bin/codesign | |
| # Import Developer ID Installer certificate | |
| decode_b64_to_file "$INSTALLER_P12" "$RUNNER_TEMP/inst.p12" "APPLE_INSTALLER_P12" | |
| verify_pkcs12_password "$RUNNER_TEMP/inst.p12" "$INST_P12_PASSWORD" "APPLE_INSTALLER_P12" | |
| reencode_pkcs12_for_macos "$RUNNER_TEMP/inst.p12" "$RUNNER_TEMP/inst.macos.p12" "$INST_P12_PASSWORD" "APPLE_INSTALLER_P12" | |
| import_pkcs12 "$RUNNER_TEMP/inst.macos.p12" "$INST_P12_PASSWORD" "APPLE_INSTALLER_P12" /usr/bin/codesign /usr/bin/productbuild | |
| # Import Apple Developer ID intermediate certificate (G2) | |
| curl -sL "https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer" \ | |
| -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" | |
| if ! security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$KEYCHAIN_PATH" \ | |
| >"$RUNNER_TEMP/DeveloperIDG2CA.import.out" 2>"$RUNNER_TEMP/DeveloperIDG2CA.import.err"; then | |
| if grep -qi "already exists in the keychain" "$RUNNER_TEMP/DeveloperIDG2CA.import.err"; then | |
| echo "::warning::DeveloperIDG2CA certificate already exists in keychain, continuing" | |
| else | |
| echo "security import stderr for DeveloperIDG2CA:" | |
| sed -n '1,120p' "$RUNNER_TEMP/DeveloperIDG2CA.import.err" || true | |
| fail "DeveloperIDG2CA import failed" | |
| fi | |
| fi | |
| # Allow codesign to access the imported private keys | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: \ | |
| -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Add temp keychain to the search list | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" \ | |
| $(security default-keychain -d user | tr -d '"') | |
| # Clean up temporary artifacts from disk | |
| rm -f "$RUNNER_TEMP/app.p12" "$RUNNER_TEMP/inst.p12" \ | |
| "$RUNNER_TEMP/app.macos.p12" "$RUNNER_TEMP/inst.macos.p12" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.decode.err" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.decode.err" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.openssl.out" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.openssl.err" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.openssl.out" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.openssl.err" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.repack.out" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.repack.err" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.repack.pem" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.repack.out" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.repack.err" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.repack.pem" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.security_import.out" \ | |
| "$RUNNER_TEMP/APPLE_APPLICATION_P12.security_import.err" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.security_import.out" \ | |
| "$RUNNER_TEMP/APPLE_INSTALLER_P12.security_import.err" \ | |
| "$RUNNER_TEMP/DeveloperIDG2CA.import.out" \ | |
| "$RUNNER_TEMP/DeveloperIDG2CA.import.err" | |
| # Build the self-contained .app and .pkg. | |
| - name: Build, sign, and package | |
| run: | | |
| chmod +x installer/build_mac_app.sh | |
| ./installer/build_mac_app.sh "$ROW_BOT_VERSION" | |
| - name: Prepare signed DMG and app zip | |
| run: | | |
| set -euo pipefail | |
| VERSION="$ROW_BOT_VERSION" | |
| ARCH="$(uname -m)" | |
| APP_PATH="installer/build/mac/Row-Bot.app" | |
| DMG_PATH="dist/Row-Bot-${VERSION}-macOS-${ARCH}.dmg" | |
| if [ ! -d "$APP_PATH" ]; then | |
| echo "::error::Expected app bundle not found at $APP_PATH" | |
| exit 1 | |
| fi | |
| echo "Building DMG from app bundle..." | |
| DMG_STAGING="$RUNNER_TEMP/dmg-root" | |
| rm -rf "$DMG_STAGING" | |
| mkdir -p "$DMG_STAGING" | |
| cp -R "$APP_PATH" "$DMG_STAGING/Row-Bot.app" | |
| ln -s /Applications "$DMG_STAGING/Applications" | |
| hdiutil create -volname "Row-Bot" -srcfolder "$DMG_STAGING" \ | |
| -format UDZO -ov "$DMG_PATH" | |
| echo "Signing DMG with: $CODESIGN_IDENTITY" | |
| codesign --force --timestamp --options runtime \ | |
| --sign "$CODESIGN_IDENTITY" "$DMG_PATH" | |
| codesign --verify --strict "$DMG_PATH" | |
| echo "DMG size (bytes):" | |
| stat -f%z "$DMG_PATH" | |
| echo "DMG size (human):" | |
| du -h "$DMG_PATH" | |
| - name: Upload signed macOS DMG | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: Row-Bot-macOS | |
| path: dist/Row-Bot-*-macOS-*.dmg | |
| retention-days: 90 | |
| - name: Upload signed macOS PKG | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: Row-Bot-macOS-pkg | |
| path: dist/Row-Bot-*-macOS-*.pkg | |
| retention-days: 90 |