fix(build): name Etienne Lescot as the Windows publisher #44
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 | |
| # Build each arch NATIVELY. `macos-latest` is Apple Silicon, and everything the | |
| # 1.8.0 macOS path added keys off the HOST arch: fetch-ffmpeg-macos.mjs configures | |
| # with `--arch=${process.arch}`, and build-macos-compositor-addon.mjs installs into | |
| # `darwin-${process.arch}` and runs cargo without `--target`. So the x64 job on an | |
| # arm64 runner produced arm64 output in darwin-arm64/, and packaging then failed | |
| # with "Refusing to package an incomplete macOS payload — looked in darwin-x64". | |
| # v1.7.0 shipped an x64 DMG because it had neither the compositor addon nor a | |
| # vendored ffmpeg to build; both arrived with 1.8.0 and nobody could see the | |
| # breakage while this job sat behind `if: false`. | |
| # Running x64 on an Intel runner fixes it without threading a target arch through | |
| # ffmpeg's configure, cargo and the output paths — four blind changes on a release | |
| # branch, none of them testable without a Mac. | |
| runs-on: ${{ matrix.arch == 'x64' && 'macos-15-intel' || '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 }} | |
| # The two steps below are what `npm run build:mac` does and this job did not. | |
| # Windows gets them for free because its job just runs `npm run build:win`, | |
| # which chains fetch:ffmpeg + build:native:compositor; macOS spells its steps | |
| # out (it needs `--dir` plus a hand-rolled DMG and signing) and drifted. The | |
| # result was a .app with no compositor addon — preview and export dead in the | |
| # installed app, silently. `scripts/before-pack.cjs` now refuses to package | |
| # that, so this is also what keeps the job from failing at the pack step. | |
| - name: Cache LGPL ffmpeg tree | |
| uses: actions/cache@v4 | |
| with: | |
| # fetch-ffmpeg-macos.mjs BUILDS ffmpeg from source (~5 min): BtbN ships no | |
| # macOS target and every circulating macOS build is GPL, which would | |
| # relicense this MIT app. The script pins the release and checksums it, so | |
| # keying on the script itself busts the cache when the pin moves. | |
| path: crates/thirdparty | |
| key: ffmpeg-macos-${{ matrix.arch }}-${{ hashFiles('scripts/fetch-ffmpeg-macos.mjs') }} | |
| - name: Vendor LGPL ffmpeg | |
| run: npm run fetch:ffmpeg:mac | |
| - name: Cache cargo + compositor build tree | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| crates/target | |
| key: cargo-macos-${{ matrix.arch }}-${{ hashFiles('crates/Cargo.lock') }} | |
| restore-keys: | | |
| cargo-macos-${{ matrix.arch }}- | |
| - name: Build Metal compositor addon | |
| run: npm run build:native:compositor:mac | |
| - 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" | |
| # `--${{ matrix.arch }}` above does NOT restrict the architecture: the | |
| # `arch` list in electron-builder.json5's `mac.target` names both x64 and | |
| # arm64 and the config wins, so BOTH bundles are produced in every job — | |
| # x64 in release/<ver>/mac/, arm64 in release/<ver>/mac-arm64/. The old | |
| # `find release/<ver> ... | head -n1` took whichever came first in | |
| # directory order (x64, in practice), so the arm64 job could package the | |
| # x64 bundle into a DMG named `-arm64-`. Nothing downstream compared the | |
| # name against the contents, so that would have published silently. | |
| - name: Find .app bundle | |
| id: find_app | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| if [[ "${{ matrix.arch }}" == "arm64" ]]; then ARCH_DIR="mac-arm64"; else ARCH_DIR="mac"; fi | |
| APP_BUNDLE="$(find "release/${VERSION}/${ARCH_DIR}" -maxdepth 2 -name "*.app" -type d | head -n1)" | |
| if [[ -z "$APP_BUNDLE" ]]; then | |
| echo "::error::No .app bundle found in release/${VERSION}/${ARCH_DIR}/" | |
| find "release/${VERSION}" -maxdepth 4 -print || true | |
| exit 1 | |
| fi | |
| echo "app_bundle=$APP_BUNDLE" >> "$GITHUB_OUTPUT" | |
| # The guard for the above: refuse to build a DMG whose name would not | |
| # match its contents. An Intel bundle on an Apple Silicon Mac runs under | |
| # Rosetta 2 — compositor, encoder and whisper all translated — which is | |
| # slow enough to be unusable, so a mislabelled DMG is a real user harm. | |
| - name: Verify .app architecture matches the job | |
| run: | | |
| BIN="${{ steps.find_app.outputs.app_bundle }}/Contents/MacOS/Openscreen" | |
| if [[ "${{ matrix.arch }}" == "arm64" ]]; then EXPECTED="arm64"; else EXPECTED="x86_64"; fi | |
| ACTUAL="$(lipo -archs "$BIN")" | |
| echo "job arch=${{ matrix.arch }} expected=${EXPECTED} actual=${ACTUAL}" | |
| if [[ " ${ACTUAL} " != *" ${EXPECTED} "* ]]; then | |
| echo "::error::The ${{ matrix.arch }} job produced a '${ACTUAL}' bundle — refusing to publish a mislabelled DMG" | |
| exit 1 | |
| fi | |
| - 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 }}" | |
| # Name the DMG after the machine, not the instruction set. "x64" reads | |
| # to most people as "the normal 64-bit one" and "arm64" as the exotic | |
| # variant, which is exactly backwards on any Mac sold since 2020 — and | |
| # picking the wrong one silently costs Rosetta 2. `Intel` and | |
| # `Apple-Silicon` are what About This Mac shows the user. | |
| case "$ARCH" in | |
| arm64) ARCH_LABEL="Apple-Silicon" ;; | |
| x64) ARCH_LABEL="Intel" ;; | |
| *) ARCH_LABEL="$ARCH" ;; | |
| esac | |
| DMG_NAME="Openscreen-macOS-${ARCH_LABEL}-${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: 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 | |
| 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}" | |
| # 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 | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: openscreen-mac-arm64 | |
| path: artifacts/mac-arm64 | |
| - name: Download macOS x64 DMG | |
| 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 }} | |
| 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 |