Build Electron App #26
Workflow file for this run
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: Build Electron App | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| arch: | |
| description: "macOS architecture to build" | |
| required: true | |
| default: "both" | |
| type: choice | |
| options: | |
| - arm64 | |
| - x64 | |
| - both | |
| release_tag: | |
| description: "Optional release tag to create or update, e.g. v1.5.0" | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: build-${{ github.ref_name }}-${{ github.event.inputs.release_tag || 'artifacts' }} | |
| cancel-in-progress: false | |
| jobs: | |
| build-windows: | |
| name: Windows installer | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: ./.github/actions/setup | |
| # STT is the bundled whisper-stt-server (whisper.cpp with native DTW token | |
| # timestamps); no VAD model is fetched here. The binary is built by | |
| # build-whisper-stt.yml and staged below — without that step the installer | |
| # ships without speech-to-text. See | |
| # technical-documentation/architecture/transcription-and-captions.md. | |
| - name: Stage whisper-stt binaries | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bash scripts/stage-whisper-stt.sh win32-x64 | |
| - name: Build Windows app | |
| run: npm run build:win -- --publish never | |
| - name: Upload Windows installer | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openscreen-windows | |
| path: release/**/Openscreen.Setup.*.exe | |
| if-no-files-found: error | |
| retention-days: 30 | |
| build-windows-store: | |
| name: Windows Store package | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: ./.github/actions/setup | |
| - name: Stage whisper-stt binaries | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bash scripts/stage-whisper-stt.sh win32-x64 | |
| - name: Build Windows Store package | |
| run: npm run build:win:store -- --publish never | |
| - name: Upload Windows Store package | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openscreen-windows-store | |
| path: release/**/Openscreen.Setup.*.appx | |
| if-no-files-found: error | |
| retention-days: 30 | |
| build-macos: | |
| name: macOS ${{ matrix.arch }} DMG | |
| # RELEASE-BRANCH-ONLY: 1.8.0 ships Windows-only. Do NOT let this `if: false` | |
| # reach main when promoting, or every later release becomes Windows-only too. | |
| if: false | |
| runs-on: macos-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: ${{ fromJSON((github.event_name == 'workflow_dispatch' && github.event.inputs.arch != 'both') && format('["{0}"]', github.event.inputs.arch) || '["arm64", "x64"]') }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: ./.github/actions/setup | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Ensure sharp prebuilt | |
| run: npm rebuild sharp | |
| env: | |
| npm_config_build_from_source: "false" | |
| - name: Stage whisper-stt binaries | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bash scripts/stage-whisper-stt.sh darwin-${{ matrix.arch }} | |
| - name: Resolve macOS signing | |
| id: signing | |
| env: | |
| MAC_CERTIFICATE_P12: ${{ secrets.MAC_CERTIFICATE_P12 }} | |
| MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} | |
| MAC_CSC_NAME: ${{ secrets.MAC_CSC_NAME }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| run: | | |
| if [[ -n "$MAC_CERTIFICATE_P12" && -n "$MAC_CERTIFICATE_PASSWORD" && -n "$MAC_CSC_NAME" && -n "$APPLE_ID" && -n "$APPLE_TEAM_ID" && -n "$APPLE_APP_SPECIFIC_PASSWORD" ]]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Import code signing certificate | |
| if: steps.signing.outputs.enabled == 'true' | |
| env: | |
| MAC_CERTIFICATE_P12: ${{ secrets.MAC_CERTIFICATE_P12 }} | |
| MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} | |
| run: | | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| echo "$MAC_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12" | |
| security import "$RUNNER_TEMP/certificate.p12" \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -P "$MAC_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | |
| rm -f "$RUNNER_TEMP/certificate.p12" | |
| - name: Build Vite + Electron | |
| run: npx tsc && npx vite build | |
| - name: Build native macOS helpers | |
| run: npm run build:native:mac | |
| env: | |
| OPENSCREEN_MAC_HELPER_ARCHS: ${{ matrix.arch }} | |
| - name: Package .app bundle | |
| run: npx electron-builder --mac --${{ matrix.arch }} --dir --publish never | |
| env: | |
| CSC_NAME: ${{ secrets.MAC_CSC_NAME }} | |
| CSC_IDENTITY_AUTO_DISCOVERY: ${{ steps.signing.outputs.enabled == 'true' && 'true' || 'false' }} | |
| - name: Get version | |
| id: version | |
| run: | | |
| VERSION="$(node -e "console.log(require('./package.json').version)")" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Find .app bundle | |
| id: find_app | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| APP_BUNDLE="$(find "release/${VERSION}" -maxdepth 4 -name "*.app" -type d | head -n1)" | |
| if [[ -z "$APP_BUNDLE" ]]; then | |
| echo "::error::No .app bundle found in release/${VERSION}/" | |
| find "release/${VERSION}" -maxdepth 4 -print || true | |
| exit 1 | |
| fi | |
| echo "app_bundle=$APP_BUNDLE" >> "$GITHUB_OUTPUT" | |
| - name: Verify .app code signature | |
| if: steps.signing.outputs.enabled == 'true' | |
| run: codesign --verify --deep --strict "${{ steps.find_app.outputs.app_bundle }}" | |
| - name: Create DMG | |
| id: dmg | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| ARCH="${{ matrix.arch }}" | |
| DMG_NAME="Openscreen-Mac-${ARCH}-${VERSION}.dmg" | |
| RELEASE_DIR="release/${VERSION}" | |
| DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}" | |
| STAGING="${RELEASE_DIR}/dmg-staging" | |
| rm -rf "$STAGING" | |
| rm -f "$DMG_OUTPUT" | |
| mkdir -p "$STAGING" | |
| cp -R "${{ steps.find_app.outputs.app_bundle }}" "$STAGING/" | |
| ln -s /Applications "$STAGING/Applications" | |
| hdiutil create \ | |
| -srcfolder "$STAGING" \ | |
| -volname "Openscreen" \ | |
| -fs HFS+ \ | |
| -fsargs "-c c=64,a=16,e=16" \ | |
| -format UDBZ \ | |
| "$DMG_OUTPUT" | |
| rm -rf "$STAGING" | |
| echo "dmg_path=$DMG_OUTPUT" >> "$GITHUB_OUTPUT" | |
| - name: Sign DMG | |
| if: steps.signing.outputs.enabled == 'true' && !contains(github.ref_name, '-') | |
| run: | | |
| codesign --force \ | |
| --sign "${{ secrets.MAC_CSC_NAME }}" \ | |
| --timestamp \ | |
| "${{ steps.dmg.outputs.dmg_path }}" | |
| - name: Notarize DMG | |
| if: steps.signing.outputs.enabled == 'true' && !contains(github.ref_name, '-') | |
| run: | | |
| xcrun notarytool submit "${{ steps.dmg.outputs.dmg_path }}" \ | |
| --apple-id "${{ secrets.APPLE_ID }}" \ | |
| --team-id "${{ secrets.APPLE_TEAM_ID }}" \ | |
| --password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" \ | |
| --wait | |
| timeout-minutes: 15 | |
| - name: Staple notarization ticket | |
| if: steps.signing.outputs.enabled == 'true' && !contains(github.ref_name, '-') | |
| run: xcrun stapler staple "${{ steps.dmg.outputs.dmg_path }}" | |
| - name: Validate stapled DMG | |
| if: steps.signing.outputs.enabled == 'true' && !contains(github.ref_name, '-') | |
| run: | | |
| xcrun stapler validate "${{ steps.dmg.outputs.dmg_path }}" | |
| spctl -a -vv -t install "${{ steps.dmg.outputs.dmg_path }}" | |
| - name: Upload macOS DMG | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openscreen-mac-${{ matrix.arch }} | |
| path: ${{ steps.dmg.outputs.dmg_path }} | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - name: Cleanup keychain | |
| if: always() && steps.signing.outputs.enabled == 'true' | |
| run: security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true | |
| build-linux: | |
| name: Linux packages | |
| # RELEASE-BRANCH-ONLY: see the note on build-macos above. | |
| if: false | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: ./.github/actions/setup | |
| - name: Install pacman build dependencies | |
| run: sudo apt-get update && sudo apt-get install -y libarchive-tools | |
| - name: Stage whisper-stt binaries | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: bash scripts/stage-whisper-stt.sh linux-x64 | |
| - name: Build Linux app | |
| run: npm run build:linux -- --publish never | |
| - name: Upload Linux packages | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openscreen-linux | |
| path: | | |
| release/**/*.AppImage | |
| release/**/*.zsync | |
| release/**/*.deb | |
| release/**/*.pacman | |
| if-no-files-found: error | |
| retention-days: 30 | |
| publish-release: | |
| name: Publish GitHub release | |
| runs-on: ubuntu-latest | |
| # RELEASE-BRANCH-ONLY: build-macos / build-linux are disabled for this | |
| # Windows-only release. A skipped job in `needs` skips this one too, so they | |
| # must come out of the list, not just be gated. Restore all three on main. | |
| needs: | |
| - build-windows | |
| if: ${{ (github.event_name == 'push' && github.ref_type == 'tag') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Resolve release tag | |
| id: release | |
| env: | |
| INPUT_TAG: ${{ github.event.inputs.release_tag }} | |
| run: | | |
| if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then | |
| TAG="${GITHUB_REF_NAME}" | |
| else | |
| TAG="${INPUT_TAG}" | |
| fi | |
| if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta|alpha)\.[0-9]+)?$ ]]; then | |
| echo "::error::Release tag must look like v1.5.0 or v1.5.0-rc.1; got '${TAG}'" | |
| exit 1 | |
| fi | |
| VERSION="${TAG#v}" | |
| # For an RC tag (e.g. v1.5.0-rc.1) package.json is at the pre-release version | |
| # (1.5.0-rc.1), not the stable version (1.5.0). Compare against the full tag version. | |
| PACKAGE_VERSION="$(node -p 'require("./package.json").version')" | |
| if [[ "$PACKAGE_VERSION" != "$VERSION" ]]; then | |
| echo "::error::package.json version ${PACKAGE_VERSION} does not match ${VERSION} from tag ${TAG}" | |
| exit 1 | |
| fi | |
| if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-(rc|beta|alpha)\.[0-9]+$ ]]; then | |
| PRERELEASE_FLAG="--prerelease" | |
| IS_PRERELEASE="true" | |
| else | |
| PRERELEASE_FLAG="" | |
| IS_PRERELEASE="false" | |
| fi | |
| # Compute the previous stable tag for auto-generated release notes. We don't use | |
| # GitHub's "most recent prior release by date" because the fork carries re-published | |
| # upstream releases whose published_at is more recent than the fork's own first release. | |
| # For SemVer X.Y.Z: previous is vX.Y.(Z-1) if Z>0, else vX.(Y-1).0, else v(X-1).0.0. | |
| STABLE_VERSION="${VERSION%%-*}" | |
| IFS='.' read -r PX PY PZ <<< "$STABLE_VERSION" | |
| if (( PZ > 0 )); then | |
| NOTES_START_TAG="v${PX}.${PY}.$((PZ - 1))" | |
| elif (( PY > 0 )); then | |
| NOTES_START_TAG="v${PX}.$((PY - 1)).0" | |
| else | |
| NOTES_START_TAG="v$((PX - 1)).0.0" | |
| fi | |
| echo "Computed notes_start_tag=${NOTES_START_TAG} for tag=${TAG}" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "stable_version=$STABLE_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" | |
| echo "prerelease_flag=$PRERELEASE_FLAG" >> "$GITHUB_OUTPUT" | |
| echo "notes_start_tag=$NOTES_START_TAG" >> "$GITHUB_OUTPUT" | |
| - name: Download Windows installer | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: openscreen-windows | |
| path: artifacts/windows | |
| - name: Download macOS arm64 DMG | |
| continue-on-error: true | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: openscreen-mac-arm64 | |
| path: artifacts/mac-arm64 | |
| - name: Download macOS x64 DMG | |
| continue-on-error: true | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: openscreen-mac-x64 | |
| path: artifacts/mac-x64 | |
| - name: Download Linux packages | |
| # RELEASE-BRANCH-ONLY: tolerate the missing artifact from the disabled | |
| # Linux job (the macOS downloads below already do). Drop with the rest. | |
| continue-on-error: true | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: openscreen-linux | |
| path: artifacts/linux | |
| - name: Publish release assets | |
| env: | |
| GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }} | |
| TAG: ${{ steps.release.outputs.tag }} | |
| PRERELEASE_FLAG: ${{ steps.release.outputs.prerelease_flag }} | |
| NOTES_START_TAG: ${{ steps.release.outputs.notes_start_tag }} | |
| run: | | |
| mapfile -t FILES < <(find artifacts -type f | sort) | |
| if [[ "${#FILES[@]}" -eq 0 ]]; then | |
| echo "::error::No installer artifacts were downloaded" | |
| exit 1 | |
| fi | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| gh release upload "$TAG" "${FILES[@]}" --clobber | |
| else | |
| # --notes-start-tag controls which previous tag GitHub compares against | |
| # when auto-generating the release notes. Default behaviour (most recent | |
| # prior release by date) doesn't work for this fork because the v1.4.0 | |
| # release in the fork was re-published after v1.5.0, which makes GitHub | |
| # pick v1.4.0 as the "previous" for any v1.5.x release. | |
| # shellcheck disable=SC2086 | |
| gh release create "$TAG" "${FILES[@]}" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$TAG" \ | |
| --generate-notes \ | |
| --notes-start-tag "$NOTES_START_TAG" \ | |
| $PRERELEASE_FLAG | |
| fi | |
| if [[ -n "$PRERELEASE_FLAG" ]]; then | |
| gh release edit "$TAG" \ | |
| --draft=false \ | |
| --latest=false \ | |
| --title "$TAG" | |
| else | |
| gh release edit "$TAG" \ | |
| --draft=false \ | |
| --latest \ | |
| --title "$TAG" | |
| fi |