name: Publish Release on: release: types: [published] workflow_dispatch: inputs: dry_run: description: 'Build artifacts only, skip uploading to GitHub Release' required: false type: boolean default: true allow_update_published_release: description: 'Allow uploading assets to an already published release' required: false type: boolean default: false jobs: build: strategy: fail-fast: false matrix: include: - os: windows-latest platform: win32 arch: x64 - os: windows-11-arm platform: win32 arch: arm64 - os: macos-15-intel platform: darwin arch: x64 - os: macos-14 platform: darwin arch: arm64 - os: ubuntu-latest platform: linux arch: x64 - os: ubuntu-24.04-arm platform: linux arch: arm64 runs-on: ${{ matrix.os }} permissions: contents: read steps: - name: Checkout git repo uses: actions/checkout@v4 - name: Install Node and NPM uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install Linux dependencies if: matrix.platform == 'linux' run: | sudo apt-get update sudo apt-get install -y fakeroot rpm libsecret-1-dev pkg-config - name: Ensure WiX Toolset if: matrix.platform == 'win32' && matrix.arch == 'x64' shell: powershell run: | $installed = choco list --local-only wixtoolset --exact --limit-output if (-not $installed) { choco install wixtoolset --version=3.14.0 -y } elseif ($installed -match '^wixtoolset\|3\.14(\.|$)') { Write-Host "WiX Toolset already compatible: $installed" } else { Write-Host "Incompatible WiX Toolset detected: $installed" choco upgrade wixtoolset --version=3.14.0 --allow-downgrade -y } - name: Install dependencies run: npm ci - name: Make Artifacts if: matrix.platform != 'darwin' env: NODE_OPTIONS: --max-old-space-size=8192 SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} run: npm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }} # DMG creation may be flaky on GitHub macOS runners, so retry the full darwin make. - name: Make Artifacts (darwin with retry) if: matrix.platform == 'darwin' uses: nick-fields/retry@v3 with: max_attempts: 3 timeout_minutes: 20 retry_wait_seconds: 20 command: npm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }} env: NODE_OPTIONS: --max-old-space-size=8192 SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: make-${{ matrix.platform }}-${{ matrix.arch }} path: | out/make/**/*.exe out/make/**/*.dmg out/make/**/*.zip out/make/**/*.AppImage out/make/**/*.deb out/make/**/*.rpm out/make/**/*.msi out/make/**/*.yml out/make/**/*.txt if-no-files-found: error retention-days: 14 - name: Dry-run summary if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' run: | echo "Dry run enabled: build artifacts generated and uploaded; release publish step will be skipped." publish: name: Publish Release (Manual Approval) needs: build runs-on: ubuntu-latest if: needs.build.result == 'success' && (github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true') permissions: contents: write # Configure the `release` environment with required reviewers in GitHub settings # to enforce a manual approval gate before assets are uploaded. environment: name: release steps: - name: Checkout git repo uses: actions/checkout@v4 - name: Download Build Artifacts uses: actions/download-artifact@v4 with: pattern: make-* merge-multiple: true path: release-assets - name: Install Node and NPM uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Resolve release tag id: release_tag env: RELEASE_EVENT_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }} run: | if [ -n "$RELEASE_EVENT_TAG" ]; then tag="$RELEASE_EVENT_TAG" else version="$(node -e "const v=require('./package.json').version; if (!v) { throw new Error('package.json version is empty'); } process.stdout.write(v);")" tag="v$version" fi if [ -z "$tag" ]; then echo "::error::Resolved release tag is empty" exit 1 fi echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "Using release tag: $tag" - name: Ensure release is safe to update env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ steps.release_tag.outputs.tag }} ALLOW_UPDATE_PUBLISHED_RELEASE: ${{ github.event_name == 'release' && 'true' || (github.event_name == 'workflow_dispatch' && github.event.inputs.allow_update_published_release) || 'false' }} run: | if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then is_draft="$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')" if [ "$is_draft" != "true" ] && [ "$ALLOW_UPDATE_PUBLISHED_RELEASE" != "true" ]; then echo "::error::Release $RELEASE_TAG is already published. Refusing to upload assets." echo "::error::Re-run with workflow_dispatch input allow_update_published_release=true if you explicitly want to update it." exit 1 fi else gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --draft --notes "Automated release draft." fi - name: Upload assets to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ steps.release_tag.outputs.tag }} run: | mapfile -t files < <(find release-assets -type f \( \ -name '*.exe' -o \ -name '*.dmg' -o \ -name '*.zip' -o \ -name '*.AppImage' -o \ -name '*.deb' -o \ -name '*.rpm' -o \ -name '*.msi' -o \ -name '*.yml' -o \ -name '*.txt' \ \) | sort) if [ "${#files[@]}" -eq 0 ]; then echo "::error::No release assets found under release-assets/" exit 1 fi printf 'Uploading %s assets to %s\n' "${#files[@]}" "$RELEASE_TAG" gh release upload "$RELEASE_TAG" "${files[@]}" --clobber