Build Electron App #9
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 | |
| - name: Cache caption assets | |
| uses: actions/cache@v4 | |
| with: | |
| path: caption-assets | |
| key: caption-assets-${{ runner.os }}-${{ hashFiles('scripts/fetch-caption-model.mjs') }} | |
| - 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-macos: | |
| name: macOS ${{ matrix.arch }} DMG | |
| 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: Cache caption assets | |
| uses: actions/cache@v4 | |
| with: | |
| path: caption-assets | |
| key: caption-assets-${{ runner.os }}-${{ hashFiles('scripts/fetch-caption-model.mjs') }} | |
| - 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 | |
| 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: Cache caption assets | |
| uses: actions/cache@v4 | |
| with: | |
| path: caption-assets | |
| key: caption-assets-${{ runner.os }}-${{ hashFiles('scripts/fetch-caption-model.mjs') }} | |
| - 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 | |
| needs: | |
| - build-windows | |
| - build-macos | |
| - build-linux | |
| 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}" | |
| # Strip pre-release suffix (e.g. "1.5.0-rc.1" -> "1.5.0") for package.json comparison. | |
| STABLE_VERSION="${VERSION%%-*}" | |
| PACKAGE_VERSION="$(node -p 'require("./package.json").version')" | |
| if [[ "$PACKAGE_VERSION" != "$STABLE_VERSION" ]]; then | |
| echo "::error::package.json version ${PACKAGE_VERSION} does not match stable version ${STABLE_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 | |
| 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" | |
| - 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 | |
| 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 }} | |
| 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 | |
| # shellcheck disable=SC2086 | |
| gh release create "$TAG" "${FILES[@]}" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$TAG" \ | |
| --generate-notes \ | |
| $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 |