Build / Notarize macOS & Windows → Release #15
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 / Notarize macOS & Windows → Release | |
| on: | |
| workflow_dispatch: | |
| push: | |
| tags: | |
| - 'v[0-9]+.[0-9]+.[0-9]+' | |
| env: # shared across jobs | |
| APP_NAME: Localforge | |
| NODE_VERSION: '20' | |
| jobs: | |
| # ──────────────────────────────────────────────────────────────────────────────── | |
| # 1. Build matrix – macOS + Windows run in parallel | |
| # ──────────────────────────────────────────────────────────────────────────────── | |
| build: | |
| strategy: | |
| matrix: | |
| include: | |
| - os: macos-14 | |
| arch: arm64 | |
| readable_arch: ARM64 | |
| package_script: npm run package:mac-arm64 | |
| app_dir_suffix: arm64 | |
| installer_dir_suffix: arm64 | |
| artifact_name_prefix: localforge-osx-arm64 | |
| create_dmg_script: npm run create-dmg:arm64 | |
| - os: macos-14 | |
| arch: x64 # electron-packager uses x64 for Intel | |
| readable_arch: Intel | |
| package_script: npm run package:mac-intel | |
| app_dir_suffix: x64 | |
| installer_dir_suffix: intel | |
| artifact_name_prefix: localforge-osx-intel | |
| create_dmg_script: npm run create-dmg:intel | |
| - os: windows-latest | |
| arch: x64 # Assuming x64 for Windows | |
| readable_arch: Windows | |
| package_script: npm run package:win | |
| app_dir_suffix: win32-x64 # from your original script | |
| installer_dir_suffix: win # For consistency, though not used for DMG | |
| artifact_name_prefix: localforge-windows | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| # ---------- common ---------- | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' # Or your required version | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| # ---------- macOS-only : import cert & env ---------- | |
| - name: Import macOS code-sign cert | |
| if: runner.os == 'macOS' | |
| id: import_cert # Give this step an ID to reference its output | |
| uses: apple-actions/import-codesign-certs@v2 | |
| with: | |
| p12-file-base64: ${{ secrets.MACOS_CERTIFICATE_P12 }} | |
| p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| # ---------- build / package ---------- | |
| - name: Package Electron OSX App (${{ matrix.readable_arch }}) | |
| if: runner.os == 'macOS' | |
| env: | |
| MACOS_SIGNING_IDENTITY_FROM_ACTION: ${{ steps.import_cert.outputs.signing-identity }} | |
| run: | | |
| echo "Packaging for Mac..." | |
| ${{ matrix.package_script }} | |
| - name: Package Electron Windows App | |
| if: runner.os == 'Windows' | |
| run: | | |
| echo "Packaging for Windows..." | |
| ${{ matrix.package_script }} | |
| # ---------- macOS-only : notarize, staple, dmg ---------- | |
| - name: Notarise & staple (macOS) | |
| if: runner.os == 'macOS' | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| APP_PATH="dist/Localforge-darwin-${{ matrix.app_dir_suffix }}/Localforge.app" | |
| ZIP_PATH="dist/Localforge-mac-${{ matrix.arch }}.zip" # Arch specific zip for notarization | |
| ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_PATH" | |
| echo "Submitting to notarisation…" | |
| xcrun notarytool submit "$ZIP_PATH" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --output-format json --wait | |
| xcrun stapler staple --verbose "$APP_PATH" | |
| echo "Creating DMG using script: ${{ matrix.create_dmg_script }}" | |
| ${{ matrix.create_dmg_script }} | |
| - name: Rename DMG so filenames are unique | |
| if: runner.os == 'macOS' | |
| run: | | |
| DMG_DIR="dist/installers-${{ matrix.installer_dir_suffix }}" | |
| mv "$DMG_DIR/Localforge.dmg" \ | |
| "$DMG_DIR/Localforge-${{ matrix.installer_dir_suffix }}.dmg" | |
| # ---------- Windows portable build via electron-packager -------- | |
| - name: Package Windows (portable) | |
| if: runner.os == 'Windows' | |
| run: ${{ matrix.package_script }} | |
| # Optional signing ------------------------------------------------- (120$, maybe later) | |
| # - name: Sign main executable | |
| # if: runner.os == 'Windows' && env.CSC_LINK != '' | |
| # env: | |
| # CSC_LINK: ${{ secrets.WIN_CODESIGN_PFX }} | |
| # CSC_KEY_PASSWORD: ${{ secrets.WIN_CODESIGN_PFX_PASSWORD }} | |
| # run: | | |
| # echo "%CSC_LINK%" | base64 -d > code.pfx | |
| # # Sign the main EXE | |
| # "/Program Files (x86)/Windows Kits/10/bin/10.0.22621.0/x64/signtool.exe" sign ^ | |
| # /f code.pfx ^ | |
| # /p "%CSC_KEY_PASSWORD%" ^ | |
| # /tr http://timestamp.digicert.com ^ | |
| # /td sha256 ^ | |
| # /fd sha256 ^ | |
| # "dist/Localforge-win32-x64/Localforge.exe" | |
| # Zip the folder so users download one file ----------------------- | |
| - name: Zip portable build | |
| if: runner.os == 'Windows' | |
| run: | | |
| 7z a -r dist/Localforge-win32-x64.zip dist/Localforge-${{ matrix.app_dir_suffix }}\* | |
| # ───────────── macOS artifact (.dmg) ───────────── | |
| - name: Upload macOS artifact (${{ matrix.readable_arch }}) | |
| if: runner.os == 'macOS' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name_prefix }}-installer | |
| path: dist/installers-${{ matrix.installer_dir_suffix }}/*.dmg | |
| if-no-files-found: error | |
| # ───────────── Windows artifact (portable .zip or installer) ───────────── | |
| - name: Upload Windows artifact | |
| if: runner.os == 'Windows' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name_prefix }}-installer | |
| path: dist/Localforge-win32-x64.zip | |
| if-no-files-found: error | |
| # ──────────────────────────────────────────────────────────────────────────────── | |
| # 2. Release job – waits for both installers, attaches to one GitHub Release | |
| # ──────────────────────────────────────────────────────────────────────────────── | |
| release: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout tags | |
| uses: actions/checkout@v4 | |
| # we need the tags in order to figure out the latest one | |
| with: { fetch-depth: 0 } | |
| # ---------- decide which tag to use ---------- | |
| - name: Compute tag name | |
| id: tag | |
| run: | | |
| # If this run was triggered by `git push --tag`, use that tag | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| else | |
| # Manual run → grab *latest* annotated or lightweight tag | |
| git fetch --tags --force | |
| TAG_NAME="$(git describe --tags --abbrev=0)" | |
| fi | |
| echo "Using tag: $TAG_NAME" | |
| echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" | |
| - name: download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: '*-installer' # grabs macOS-installer & Windows-installer | |
| path: dist-artifacts | |
| # publish / update the release | |
| - name: Publish GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| files: | | |
| dist-artifacts/**/Localforge-*.dmg | |
| dist-artifacts/localforge-windows-installer/*.zip | |
| draft: true |