Spaces:
Runtime error
Runtime error
| name: Build Electron Desktop App | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version (e.g., v1.6.8)" | |
| required: true | |
| type: string | |
| # Least-privilege default: read-only at the top level; each job grants the writes it | |
| # needs (build/release upload assets, publish-npm forwards npm provenance / packages | |
| # to the reusable workflow) β Scorecard TokenPermissions. | |
| permissions: | |
| contents: read | |
| jobs: | |
| validate: | |
| name: Validate version | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| version: ${{ steps.validate.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Validate version format | |
| id: validate | |
| env: | |
| # Pass workflow context via env (never interpolate ${{ ... }} straight | |
| # into the run: script body) so the shell receives variables, not | |
| # inlined text β zizmor template-injection mitigation. INPUT_VERSION is | |
| # the operator-supplied value and is regex-validated below before use. | |
| EVENT_NAME: ${{ github.event_name }} | |
| INPUT_VERSION: ${{ inputs.version }} | |
| run: | | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| VERSION="${GITHUB_REF#refs/tags/}" | |
| else | |
| VERSION="$INPUT_VERSION" | |
| fi | |
| if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Error: Invalid version format. Expected: v1.6.8" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "β Valid version: $VERSION" | |
| build: | |
| name: Build Electron (${{ matrix.platform }}) | |
| needs: validate | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: write # electron-builder may publish artifacts with GH_TOKEN | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: windows | |
| runner: windows-latest | |
| target: win | |
| ext: .exe | |
| - platform: macos-intel | |
| runner: macos-15-intel | |
| target: mac-x64 | |
| ext: .dmg | |
| - platform: macos-arm64 | |
| runner: macos-latest | |
| target: mac-arm64 | |
| ext: -arm64.dmg | |
| - platform: linux | |
| runner: ubuntu-latest | |
| target: linux | |
| ext: .AppImage | |
| deb_ext: .deb | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - name: Cache node_modules | |
| uses: actions/cache@v6.0.0 | |
| with: | |
| path: node_modules | |
| key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-node- | |
| - name: Install dependencies | |
| run: npm ci | |
| env: | |
| NPM_CONFIG_LEGACY_PEER_DEPS: true | |
| - name: Sanitize Windows home directory | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| run: | | |
| # The default USERPROFILE contains junction points (Application Data) | |
| # that cause EPERM errors during Next.js standalone build glob scans. | |
| # Create a clean temp profile directory to avoid this. | |
| mkdir -p "$RUNNER_TEMP/home" | |
| echo "USERPROFILE=$RUNNER_TEMP/home" >> "$GITHUB_ENV" | |
| - name: Build Next.js standalone | |
| env: | |
| JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation | |
| NODE_OPTIONS: "--max_old_space_size=6144" | |
| run: npm run build | |
| - name: Sync version in electron/package.json | |
| shell: bash | |
| env: | |
| # Pass the validated version via env (never interpolate ${{ ... }} | |
| # straight into the run: script body) β zizmor template-injection | |
| # mitigation. Already regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in | |
| # the `validate` job, so it cannot carry shell metacharacters. | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| run: | | |
| VERSION_NO_V="${VERSION#v}" | |
| node -e " | |
| const fs = require('fs'); | |
| const pkg = JSON.parse(fs.readFileSync('electron/package.json')); | |
| pkg.version = '$VERSION_NO_V'; | |
| fs.writeFileSync('electron/package.json', JSON.stringify(pkg, null, 2) + '\\n'); | |
| " | |
| echo "β electron/package.json version set to $VERSION_NO_V" | |
| - name: Install fpm (Linux .deb packaging tool) | |
| if: matrix.platform == 'linux' | |
| run: sudo gem install fpm --no-document | |
| - name: Install Electron dependencies | |
| working-directory: electron | |
| run: npm install --no-audit --no-fund | |
| - name: Build Electron for ${{ matrix.platform }} | |
| working-directory: electron | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: npm run build:${{ matrix.target }} | |
| - name: Smoke packaged Electron app | |
| if: matrix.platform != 'linux' | |
| # Best-effort smoke on Windows + macos-arm64: | |
| # - Windows: requestSingleInstanceLock() fails due to USERPROFILE | |
| # sanitization needed for the build step. | |
| # - macos-arm64: the headless GitHub arm64 runner crashes Electron's GPU | |
| # process (gpu_process_host exit_code=15 β network service crash β | |
| # "No rendezvous client, terminating process"), so the app can't bind | |
| # 127.0.0.1:20128 in time. The identical bundle is smoke-gated on | |
| # macos-intel + linux, so packaging is still verified per-OS; we don't | |
| # let the arm64 runner's GPU flakiness block the desktop release. | |
| continue-on-error: ${{ matrix.platform == 'windows' || matrix.platform == 'macos-arm64' }} | |
| env: | |
| ELECTRON_SMOKE_TIMEOUT_MS: 60000 | |
| ELECTRON_SMOKE_STREAM_LOGS: "1" | |
| run: npm run electron:smoke:packaged | |
| - name: Smoke packaged Electron app (Linux) | |
| if: matrix.platform == 'linux' | |
| env: | |
| ELECTRON_SMOKE_TIMEOUT_MS: 60000 | |
| ELECTRON_SMOKE_STREAM_LOGS: "1" | |
| run: xvfb-run -a npm run electron:smoke:packaged | |
| - name: Collect installers | |
| shell: bash | |
| run: | | |
| mkdir -p release-assets | |
| cd electron/dist-electron | |
| # Copy only installer files for this platform | |
| for file in *${{ matrix.ext }}; do | |
| [ -f "$file" ] && cp "$file" ../../release-assets/ | |
| done | |
| # Linux: also copy .deb package | |
| if [ "${{ matrix.platform }}" = "linux" ]; then | |
| for file in *.deb; do | |
| [ -f "$file" ] && cp "$file" ../../release-assets/ | |
| done | |
| fi | |
| # Windows: also copy portable standalone exe as OmniRoute.exe | |
| if [ "${{ matrix.platform }}" = "windows" ]; then | |
| for file in *.exe; do | |
| # Skip the NSIS installer (contains "Setup") | |
| case "$file" in *Setup*) continue ;; esac | |
| [ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break | |
| done | |
| fi | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electron-${{ matrix.platform }} | |
| path: release-assets/ | |
| release: | |
| name: Create Release | |
| needs: [validate, build] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # softprops/action-gh-release creates the GitHub Release | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: release-assets | |
| merge-multiple: true | |
| - name: Create source archives | |
| env: | |
| # Pass the validated version via env (never interpolate ${{ ... }} | |
| # straight into the run: script body) β zizmor template-injection | |
| # mitigation. Already regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in | |
| # the `validate` job, so it cannot carry shell metacharacters. | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| run: | | |
| # Create source code archives (excluding dev dependencies and build artifacts) | |
| export TARBALL="OmniRoute-${VERSION}.source.tar.gz" | |
| export ZIPBALL="OmniRoute-${VERSION}.source.zip" | |
| # Use git archive for clean source export | |
| git archive --format=tar.gz --prefix="OmniRoute-${VERSION}/" HEAD -o "release-assets/$TARBALL" | |
| git archive --format=zip --prefix="OmniRoute-${VERSION}/" HEAD -o "release-assets/$ZIPBALL" | |
| echo "β Created source archives:" | |
| ls -lh "release-assets/$TARBALL" "release-assets/$ZIPBALL" | |
| - name: List release files | |
| run: ls -la release-assets/ | |
| - name: Create Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.validate.outputs.version }} | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: true | |
| fail_on_unmatched_files: false | |
| files: | | |
| release-assets/*.dmg | |
| release-assets/*.exe | |
| release-assets/*.AppImage | |
| release-assets/*.deb | |
| release-assets/*.blockmap | |
| release-assets/*.source.tar.gz | |
| release-assets/*.source.zip | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| publish-npm: | |
| name: Publish to npm | |
| needs: [validate, release] | |
| permissions: | |
| # Must be `write`, not `read`: this job calls the reusable npm-publish.yml whose | |
| # `publish` job needs `contents: write` (gh release upload β attach the SBOM, #3874). | |
| # A reusable workflow's job cannot request more permission than the caller grants, | |
| # so a `read` here makes GitHub reject the run at startup (startup_failure). | |
| contents: write | |
| id-token: write # npm provenance (forwarded to the reusable workflow) | |
| packages: write # publish to npm.pkg.github.com | |
| uses: ./.github/workflows/npm-publish.yml | |
| with: | |
| version: ${{ needs.validate.outputs.version }} | |
| tag: latest | |
| secrets: | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |