BitFun 0.2.19-beta.1 Beta #62
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: Desktop Package | |
| on: | |
| release: | |
| types: | |
| - published | |
| workflow_dispatch: | |
| inputs: | |
| tag_name: | |
| description: "Release tag (stable: v0.2.18, beta: v0.2.18-beta.1)." | |
| required: false | |
| type: string | |
| checkout_ref: | |
| description: "Commit, branch, or tag to build. Defaults to tag_name, then HEAD." | |
| required: false | |
| type: string | |
| release_channel: | |
| description: "Immutable update channel compiled into the Desktop artifact." | |
| required: false | |
| default: stable | |
| type: choice | |
| options: | |
| - stable | |
| - beta | |
| upload_to_release: | |
| description: "Upload built artifacts to the release specified by tag_name." | |
| required: false | |
| default: false | |
| type: boolean | |
| relay_image_only: | |
| description: "Publish only the Relay Server image and signed descriptor (no Desktop packages)." | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| packages: write | |
| concurrency: | |
| group: desktop-package-${{ (github.event.release.prerelease || inputs.release_channel == 'beta') && 'beta' || github.event.release.tag_name || inputs.tag_name || github.sha }} | |
| cancel-in-progress: false | |
| jobs: | |
| # ── Resolve version info ─────────────────────────────────────────── | |
| prepare: | |
| name: Prepare | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.meta.outputs.version }} | |
| release_tag: ${{ steps.meta.outputs.release_tag }} | |
| upload_to_release: ${{ steps.meta.outputs.upload_to_release }} | |
| checkout_ref: ${{ steps.meta.outputs.checkout_ref }} | |
| release_channel: ${{ steps.meta.outputs.release_channel }} | |
| relay_image_only: ${{ steps.meta.outputs.relay_image_only }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve version metadata | |
| id: meta | |
| shell: bash | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_SHA: ${{ github.sha }} | |
| RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} | |
| INPUT_TAG_NAME: ${{ inputs.tag_name }} | |
| INPUT_CHECKOUT_REF: ${{ inputs.checkout_ref }} | |
| INPUT_RELEASE_CHANNEL: ${{ inputs.release_channel }} | |
| INPUT_UPLOAD_TO_RELEASE: ${{ inputs.upload_to_release }} | |
| INPUT_RELAY_IMAGE_ONLY: ${{ inputs.relay_image_only }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then | |
| TAG="${RELEASE_TAG_NAME}" | |
| VERSION="${TAG#v}" | |
| UPLOAD="true" | |
| CHECKOUT_REF="${TAG}" | |
| if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then | |
| CHANNEL="beta" | |
| else | |
| CHANNEL="stable" | |
| fi | |
| elif [[ -n "${INPUT_TAG_NAME}" ]]; then | |
| TAG="${INPUT_TAG_NAME}" | |
| VERSION="${TAG#v}" | |
| CHANNEL="${INPUT_RELEASE_CHANNEL:-stable}" | |
| # A one-off image backfill must use the workflow branch: an older | |
| # tag does not contain Dockerfile.release or this publishing job. | |
| if [[ "${INPUT_RELAY_IMAGE_ONLY}" == "true" ]]; then | |
| CHECKOUT_REF="${GITHUB_SHA}" | |
| elif [[ -n "${INPUT_CHECKOUT_REF}" ]]; then | |
| CHECKOUT_REF="${INPUT_CHECKOUT_REF}" | |
| else | |
| CHECKOUT_REF="${TAG}" | |
| fi | |
| if [[ "${INPUT_UPLOAD_TO_RELEASE}" == "true" ]]; then | |
| UPLOAD="true" | |
| else | |
| UPLOAD="false" | |
| fi | |
| else | |
| VERSION="$(jq -r '.version' package.json)" | |
| TAG="v${VERSION}" | |
| UPLOAD="false" | |
| CHECKOUT_REF="${GITHUB_SHA}" | |
| CHANNEL="${INPUT_RELEASE_CHANNEL:-stable}" | |
| fi | |
| node --input-type=module -e \ | |
| "import { validateReleaseVersion } from './scripts/release-channel.mjs'; validateReleaseVersion(process.argv[1], process.argv[2]);" \ | |
| "${CHANNEL}" "${VERSION}" | |
| git fetch origin main --tags --force | |
| CHECKOUT_SHA="$(git rev-parse "${CHECKOUT_REF}^{commit}")" | |
| if [[ "${GITHUB_REPOSITORY}" == "GCWing/BitFun" ]] && \ | |
| ! git merge-base --is-ancestor "${CHECKOUT_SHA}" origin/main; then | |
| echo "Ref ${CHECKOUT_REF} (${CHECKOUT_SHA}) is not part of the protected main history." >&2 | |
| exit 1 | |
| fi | |
| TAG_SHA="$(git rev-parse --verify --quiet "${TAG}^{commit}" || true)" | |
| if [[ -n "${TAG_SHA}" && "${TAG_SHA}" != "${CHECKOUT_SHA}" ]]; then | |
| echo "Existing tag ${TAG} points to ${TAG_SHA}, not requested commit ${CHECKOUT_SHA}." >&2 | |
| exit 1 | |
| fi | |
| CHECKOUT_REF="${CHECKOUT_SHA}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "release_tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "upload_to_release=$UPLOAD" >> "$GITHUB_OUTPUT" | |
| echo "checkout_ref=$CHECKOUT_REF" >> "$GITHUB_OUTPUT" | |
| echo "release_channel=$CHANNEL" >> "$GITHUB_OUTPUT" | |
| echo "relay_image_only=${INPUT_RELAY_IMAGE_ONLY:-false}" >> "$GITHUB_OUTPUT" | |
| # ── Build per platform ───────────────────────────────────────────── | |
| package: | |
| name: Package (${{ matrix.platform.name }}) | |
| runs-on: ${{ matrix.platform.os }} | |
| needs: prepare | |
| if: needs.prepare.outputs.relay_image_only != 'true' | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| BITFUN_ENABLE_UPDATER_ARTIFACTS: ${{ needs.prepare.outputs.upload_to_release }} | |
| BITFUN_RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }} | |
| TAURI_UPDATER_ENDPOINT: ${{ github.repository != 'GCWing/BitFun' && needs.prepare.outputs.release_channel == 'beta' && format('https://github.com/{0}/releases/download/channel-beta/latest.json', github.repository) || '' }} | |
| TAURI_UPDATER_FALLBACK_ENDPOINT: ${{ github.repository != 'GCWing/BitFun' && needs.prepare.outputs.release_channel == 'beta' && format('https://github.com/{0}/releases/download/channel-beta/latest.json', github.repository) || '' }} | |
| TAURI_UPDATER_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }} | |
| # Same trust root, compiled into the Desktop binary so one-click relay | |
| # deploy can verify the signed checksum locally and hand the remote host | |
| # a hash it does not have to trust the mirror for. | |
| BITFUN_RELEASE_PUBKEY: ${{ secrets.BITFUN_RELEASE_PUBKEY || secrets.TAURI_UPDATER_PUBKEY }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform: | |
| - os: ubuntu-latest | |
| name: linux-x64 | |
| target: x86_64-unknown-linux-gnu | |
| build_command: pnpm run desktop:build:linux -- --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage | |
| - os: ubuntu-24.04-arm | |
| name: linux-arm64 | |
| target: aarch64-unknown-linux-gnu | |
| # Thin LTO is now the workspace-wide [profile.release] default, so | |
| # the previous CARGO_PROFILE_RELEASE_LTO=thin override is gone. | |
| build_command: pnpm run desktop:build:linux -- --target aarch64-unknown-linux-gnu --bundles deb,rpm,appimage | |
| - os: macos-15 | |
| name: macos-arm64 | |
| target: aarch64-apple-darwin | |
| build_command: pnpm run desktop:build:arm64 | |
| - os: macos-15-intel | |
| name: macos-x64 | |
| target: x86_64-apple-darwin | |
| build_command: pnpm run desktop:build:x86_64 | |
| - os: windows-latest | |
| name: windows-x64 | |
| target: x86_64-pc-windows-msvc | |
| build_command: | | |
| $ErrorActionPreference = 'Stop' | |
| pnpm run desktop:build:nsis --target x86_64-pc-windows-msvc --verbose | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| $desktopExe = "target/x86_64-pc-windows-msvc/release/bitfun-desktop.exe" | |
| if (-not (Test-Path $desktopExe)) { | |
| throw "Desktop executable was not found after NSIS build: $desktopExe" | |
| } | |
| pnpm run installer:build:only | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| - name: Install NSIS (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| choco install nsis -y --no-progress | |
| $nsisRoot = "${env:ProgramFiles(x86)}\NSIS" | |
| $nsisBin = "${env:ProgramFiles(x86)}\NSIS\Bin" | |
| if (Test-Path $nsisRoot) { | |
| Add-Content $env:GITHUB_PATH $nsisRoot | |
| } | |
| if (Test-Path $nsisBin) { | |
| Add-Content $env:GITHUB_PATH $nsisBin | |
| } | |
| - name: Verify NSIS (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| where.exe makensis | |
| makensis /VERSION | |
| - name: Install Linux system dependencies (Tauri bundler) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| sudo apt-get update | |
| if apt-cache show libwebkit2gtk-4.1-dev >/dev/null 2>&1; then | |
| WEBKIT_PKG=libwebkit2gtk-4.1-dev | |
| else | |
| WEBKIT_PKG=libwebkit2gtk-4.0-dev | |
| fi | |
| if apt-cache show libappindicator3-dev >/dev/null 2>&1; then | |
| APPINDICATOR_PKG=libappindicator3-dev | |
| else | |
| APPINDICATOR_PKG=libayatana-appindicator3-dev | |
| fi | |
| # Tauri pins AppImage GTK input methods to its bundled cache, so the | |
| # fcitx5 GTK3 bridge must be present before linuxdeploy builds it. | |
| sudo apt-get install -y --no-install-recommends \ | |
| pkg-config \ | |
| xdg-utils \ | |
| libglib2.0-dev \ | |
| libgtk-3-dev \ | |
| fcitx5-frontend-gtk3 \ | |
| libxdo-dev \ | |
| "$WEBKIT_PKG" \ | |
| "$APPINDICATOR_PKG" \ | |
| librsvg2-dev \ | |
| patchelf \ | |
| fakeroot \ | |
| rpm \ | |
| libleptonica-dev \ | |
| libtesseract-dev \ | |
| tesseract-ocr \ | |
| tesseract-ocr-eng | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v5 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - name: Setup Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.platform.target }} | |
| - name: Cache Rust build | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "package-v2-${{ matrix.platform.name }}" | |
| cache-bin: false | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Configure Apple Developer ID signing and notarization | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| BITFUN_REQUIRE_APPLE_SIGNING: ${{ needs.prepare.outputs.upload_to_release }} | |
| run: bash scripts/ci/setup-macos-signing.sh | |
| - name: Project beta build version | |
| if: needs.prepare.outputs.release_channel == 'beta' | |
| run: node scripts/set-build-version.mjs --version "${{ needs.prepare.outputs.version }}" | |
| - name: Verify release version metadata | |
| run: node scripts/verify-release-version-sync.mjs --version "${{ needs.prepare.outputs.version }}" | |
| - name: Build desktop app | |
| run: ${{ matrix.platform.build_command }} | |
| - name: Verify Apple signature and notarization | |
| if: runner.os == 'macOS' | |
| shell: bash | |
| run: bash scripts/ci/verify-macos-signing.sh "${{ matrix.platform.target }}" | |
| - name: Verify AppImage fcitx5 GTK module | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: bash scripts/ci/verify-appimage-fcitx.sh "${{ matrix.platform.target }}" | |
| - name: Upload bundles | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: bitfun-${{ needs.prepare.outputs.release_tag }}-${{ matrix.platform.name }}-bundle | |
| if-no-files-found: error | |
| path: | | |
| target/*/release/bundle | |
| target/release/bundle | |
| src/apps/desktop/target/release/bundle | |
| BitFun-Installer/src-tauri/target/release/bitfun-installer.exe | |
| linux-binaries: | |
| name: Linux CLI and Relay Server | |
| needs: prepare | |
| if: >- | |
| needs.prepare.outputs.relay_image_only != 'true' && | |
| needs.prepare.outputs.release_channel == 'stable' | |
| uses: ./.github/workflows/linux-binaries.yml | |
| secrets: | |
| release_signing_key: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| release_signing_password: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| release_pubkey: ${{ secrets.TAURI_UPDATER_PUBKEY }} | |
| with: | |
| checkout_ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| version: ${{ needs.prepare.outputs.version }} | |
| artifact_prefix: ${{ needs.prepare.outputs.release_tag }} | |
| # Publish the Relay once, as a multi-platform image. User servers only pull | |
| # this image; they no longer download an archive and build a runtime image. | |
| publish-relay-image: | |
| name: Publish Relay Server Image | |
| needs: [prepare, linux-binaries] | |
| if: >- | |
| always() && | |
| ((needs.prepare.outputs.upload_to_release == 'true' && | |
| needs.prepare.outputs.release_channel == 'stable') || | |
| needs.prepare.outputs.relay_image_only == 'true') && | |
| (needs.prepare.outputs.relay_image_only == 'true' || | |
| needs.linux-binaries.result == 'success') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| env: | |
| IMAGE: ghcr.io/gcwing/bitfun-relay-server | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref }} | |
| - name: Download Relay archives from this release run | |
| if: needs.prepare.outputs.relay_image_only != 'true' | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: bitfun-linux-${{ needs.prepare.outputs.release_tag }}-* | |
| path: linux-release-assets | |
| merge-multiple: true | |
| - name: Download Relay archives from the existing release (image-only backfill) | |
| if: needs.prepare.outputs.relay_image_only == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p linux-release-assets | |
| gh release download "${RELEASE_TAG}" \ | |
| --repo GCWing/BitFun \ | |
| --dir linux-release-assets \ | |
| --pattern 'bitfun-relay-server-*.tar.gz' \ | |
| --pattern 'bitfun-relay-server-*.tar.gz.sha256' | |
| - name: Verify image inputs | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test -f linux-release-assets/bitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz | |
| test -f linux-release-assets/bitfun-relay-server-aarch64-unknown-linux-gnu.tar.gz | |
| for archive in linux-release-assets/bitfun-relay-server-*.tar.gz; do | |
| (cd linux-release-assets && sha256sum --check "$(basename "${archive}").sha256") | |
| done | |
| cp src/apps/relay-server/Dockerfile.release linux-release-assets/Dockerfile.release | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| - name: Resolve image tags | |
| id: image-tags | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.version }} | |
| IMAGE_ONLY: ${{ needs.prepare.outputs.relay_image_only }} | |
| RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }} | |
| run: | | |
| set -euo pipefail | |
| asset_version="${RELEASE_VERSION%%+*}" | |
| { | |
| echo 'value<<EOF' | |
| echo "${IMAGE}:${RELEASE_TAG}" | |
| echo "${IMAGE}:${asset_version}" | |
| if [[ "${IMAGE_ONLY}" == "true" ]]; then | |
| # Backfilling an older release must not roll the floating tag | |
| # backwards. GitHub's latest endpoint excludes prereleases. | |
| latest_release="$(gh api repos/GCWing/BitFun/releases/latest --jq .tag_name)" | |
| if [[ "${RELEASE_TAG}" == "${latest_release}" ]]; then | |
| echo "${IMAGE}:latest" | |
| fi | |
| elif [[ "${RELEASE_CHANNEL}" == "stable" ]]; then | |
| # The normal release workflow can create the GitHub Release only | |
| # after packaging, so it cannot rely on /releases/latest yet. | |
| echo "${IMAGE}:latest" | |
| fi | |
| echo EOF | |
| } >>"$GITHUB_OUTPUT" | |
| - name: Build and push multi-platform image | |
| id: image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: linux-release-assets | |
| file: linux-release-assets/Dockerfile.release | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| provenance: false | |
| sbom: false | |
| build-args: | | |
| VERSION=${{ needs.prepare.outputs.version }} | |
| REVISION=${{ needs.prepare.outputs.release_tag }} | |
| tags: ${{ steps.image-tags.outputs.value }} | |
| - name: Smoke-test published image on both platforms | |
| shell: bash | |
| env: | |
| IMAGE_DIGEST: ${{ steps.image.outputs.digest }} | |
| run: bash scripts/relay/smoke-image.sh "${IMAGE}@${IMAGE_DIGEST}" | |
| - name: Verify manifest and generate signed descriptor | |
| shell: bash | |
| env: | |
| IMAGE_DIGEST: ${{ steps.image.outputs.digest }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.version }} | |
| BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }} | |
| run: | | |
| set -euo pipefail | |
| [[ "${IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]] | |
| docker buildx imagetools inspect "${IMAGE}@${IMAGE_DIGEST}" --raw >relay-image-manifest.json | |
| jq -e ' | |
| [.manifests[].platform | .os + "/" + .architecture] as $platforms | |
| | ($platforms | index("linux/amd64")) != null | |
| and ($platforms | index("linux/arm64")) != null | |
| ' relay-image-manifest.json >/dev/null | |
| jq -n \ | |
| --arg image "${IMAGE}" \ | |
| --arg tag "${RELEASE_TAG}" \ | |
| --arg version "${RELEASE_VERSION}" \ | |
| --arg digest "${IMAGE_DIGEST}" \ | |
| '{ | |
| schema_version: 1, | |
| image: $image, | |
| tag: $tag, | |
| version: $version, | |
| digest: $digest, | |
| platforms: ["linux/amd64", "linux/arm64"] | |
| }' >relay-image.json | |
| bash scripts/sign-release-assets.sh relay-image.json | |
| test -s relay-image.json.sig | |
| - name: Upload signed image descriptor | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: bitfun-relay-image-${{ needs.prepare.outputs.release_tag }} | |
| if-no-files-found: error | |
| retention-days: 7 | |
| path: | | |
| relay-image.json | |
| relay-image.json.sig | |
| # The package is private on first creation. This deliberately fails until | |
| # its visibility is changed to public, preventing an apparently green | |
| # release that anonymous customer servers cannot pull. | |
| - name: Verify anonymous pull access | |
| shell: bash | |
| env: | |
| IMAGE_DIGEST: ${{ steps.image.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| docker logout ghcr.io >/dev/null 2>&1 || true | |
| clean_config="$(mktemp -d)" | |
| trap 'rm -rf "$clean_config"' EXIT | |
| DOCKER_CONFIG="$clean_config" docker buildx imagetools inspect \ | |
| "${IMAGE}@${IMAGE_DIGEST}" >/dev/null | |
| - name: Attach descriptor to an existing release (image-only backfill) | |
| if: needs.prepare.outputs.relay_image_only == 'true' | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.prepare.outputs.release_tag }} | |
| files: | | |
| relay-image.json | |
| relay-image.json.sig | |
| fail_on_unmatched_files: true | |
| # ── Upload assets to GitHub Release ──────────────────────────────── | |
| upload-release-assets: | |
| name: Upload Release Assets | |
| needs: [prepare, package, linux-binaries, publish-relay-image] | |
| if: >- | |
| always() && | |
| needs.prepare.outputs.upload_to_release == 'true' && | |
| needs.package.result == 'success' && | |
| (needs.prepare.outputs.release_channel == 'beta' || | |
| (needs.linux-binaries.result == 'success' && | |
| needs.publish-relay-image.result == 'success')) | |
| runs-on: ubuntu-latest | |
| env: | |
| REQUIRED_UPDATER_PLATFORMS: windows-x86_64,darwin-x86_64,darwin-aarch64,linux-x86_64,linux-aarch64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Download bundled artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: bitfun-${{ needs.prepare.outputs.release_tag }}-*-bundle | |
| path: release-assets | |
| merge-multiple: true | |
| - name: Download Linux binary artifacts | |
| if: needs.prepare.outputs.release_channel == 'stable' | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: bitfun-linux-${{ needs.prepare.outputs.release_tag }}-* | |
| path: linux-release-assets | |
| merge-multiple: true | |
| - name: Download Relay image descriptor | |
| if: needs.prepare.outputs.release_channel == 'stable' | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: bitfun-relay-image-${{ needs.prepare.outputs.release_tag }} | |
| path: relay-image-assets | |
| - name: List release assets | |
| env: | |
| RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }} | |
| run: | | |
| echo "Release assets:" | |
| find release-assets -type f | sort | |
| if [[ "${RELEASE_CHANNEL}" == "stable" ]]; then | |
| echo "Linux CLI and Relay Server assets:" | |
| find linux-release-assets -type f | sort | |
| echo "Relay image descriptor:" | |
| find relay-image-assets -type f | sort | |
| fi | |
| - name: Prepare versioned Windows installer | |
| run: | | |
| node scripts/prepare-windows-installer-asset.mjs \ | |
| --assets-dir release-assets \ | |
| --version "${{ needs.prepare.outputs.version }}" \ | |
| --out-dir release-manual-assets | |
| - name: Sign versioned Windows installer | |
| shell: bash | |
| env: | |
| BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }} | |
| run: bash scripts/sign-release-assets.sh release-manual-assets/*.exe | |
| - name: Collect updater assets | |
| run: | | |
| node scripts/collect-tauri-updater-assets.mjs \ | |
| --assets-dir release-assets \ | |
| --version "${{ needs.prepare.outputs.version }}" \ | |
| --out-dir release-updater-assets \ | |
| --required-platforms "${REQUIRED_UPDATER_PLATFORMS}" | |
| - name: Generate updater manifest | |
| run: | | |
| node scripts/generate-tauri-latest-json.mjs \ | |
| --assets-dir release-updater-assets \ | |
| --manual-assets-dir release-manual-assets \ | |
| --version "${{ needs.prepare.outputs.version }}" \ | |
| --tag "${{ needs.prepare.outputs.release_tag }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --out release-updater-assets/latest.json \ | |
| --required-platforms "${REQUIRED_UPDATER_PLATFORMS}" | |
| - name: Verify updater manifest | |
| run: | | |
| node scripts/verify-tauri-latest-json.mjs \ | |
| --manifest release-updater-assets/latest.json \ | |
| --version "${{ needs.prepare.outputs.version }}" \ | |
| --required-platforms "${REQUIRED_UPDATER_PLATFORMS}" \ | |
| --required-manual-platforms "windows-x86_64" | |
| - name: Generate Linux binaries manifest | |
| if: needs.prepare.outputs.release_channel == 'stable' | |
| run: | | |
| node scripts/generate-linux-binaries-manifest.mjs \ | |
| --assets-dir linux-release-assets \ | |
| --version "${{ needs.prepare.outputs.version }}" \ | |
| --tag "${{ needs.prepare.outputs.release_tag }}" \ | |
| --repo "GCWing/BitFun" \ | |
| --out linux-release-assets/linux-binaries.json | |
| # The Tauri bundler signs the five updater artifacts during `tauri build`, | |
| # but the installers people download by hand from the release page — dmg, | |
| # deb, rpm, the Windows installer and the direct AppImages — shipped with | |
| # no signature at all. Sign them with the same key so every published | |
| # artifact is verifiable. (This is not OS-level code signing: Gatekeeper | |
| # and SmartScreen still need Apple/Authenticode certificates.) | |
| - name: Sign installer packages | |
| shell: bash | |
| env: | |
| BITFUN_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| BITFUN_SIGNING_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| BITFUN_SIGNING_PUBKEY: ${{ secrets.TAURI_UPDATER_PUBKEY }} | |
| run: | | |
| set -euo pipefail | |
| mapfile -t assets < <( | |
| find release-assets -type f \ | |
| \( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' \ | |
| -o -name '*.dmg' \) | sort | |
| ) | |
| if [[ "${#assets[@]}" -eq 0 ]]; then | |
| echo "No installer packages found to sign." | |
| exit 0 | |
| fi | |
| bash scripts/sign-release-assets.sh "${assets[@]}" | |
| # Publish the public key alongside the signatures: a signature nobody | |
| # can fetch a key for is not verifiable. | |
| node scripts/write-minisign-public-key.mjs \ | |
| --out release-assets/minisign.pub | |
| - name: Stage stable release assets | |
| if: needs.prepare.outputs.release_channel == 'stable' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| shopt -s globstar | |
| node scripts/stage-github-release-assets.mjs \ | |
| --out-dir release-upload-assets \ | |
| release-updater-assets/* \ | |
| release-manual-assets/*.exe \ | |
| release-manual-assets/*.exe.sig \ | |
| release-assets/**/*.AppImage \ | |
| release-assets/**/*.AppImage.sig \ | |
| release-assets/**/*.deb \ | |
| release-assets/**/*.deb.sig \ | |
| release-assets/**/*.dmg \ | |
| release-assets/**/*.dmg.sig \ | |
| release-assets/**/*.rpm \ | |
| release-assets/**/*.rpm.sig \ | |
| release-assets/minisign.pub \ | |
| linux-release-assets/bitfun-cli-*.tar.gz \ | |
| linux-release-assets/bitfun-cli-*.tar.gz.sha256 \ | |
| linux-release-assets/bitfun-relay-server-*.tar.gz \ | |
| linux-release-assets/bitfun-relay-server-*.tar.gz.sha256 \ | |
| linux-release-assets/*.tar.gz.sig \ | |
| linux-release-assets/*.tar.gz.sha256.sig \ | |
| linux-release-assets/linux-binaries.json \ | |
| relay-image-assets/relay-image.json \ | |
| relay-image-assets/relay-image.json.sig | |
| - name: Stage beta release assets | |
| if: needs.prepare.outputs.release_channel == 'beta' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| shopt -s globstar | |
| node scripts/stage-github-release-assets.mjs \ | |
| --out-dir release-upload-assets \ | |
| release-updater-assets/* \ | |
| release-manual-assets/*.exe \ | |
| release-manual-assets/*.exe.sig \ | |
| release-assets/**/*.AppImage \ | |
| release-assets/**/*.AppImage.sig \ | |
| release-assets/**/*.deb \ | |
| release-assets/**/*.deb.sig \ | |
| release-assets/**/*.dmg \ | |
| release-assets/**/*.dmg.sig \ | |
| release-assets/**/*.rpm \ | |
| release-assets/**/*.rpm.sig \ | |
| release-assets/minisign.pub | |
| - name: Upload to release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.prepare.outputs.release_tag }} | |
| target_commitish: ${{ needs.prepare.outputs.checkout_ref }} | |
| prerelease: ${{ needs.prepare.outputs.release_channel == 'beta' }} | |
| name: ${{ needs.prepare.outputs.release_channel == 'beta' && format('BitFun {0} Beta', needs.prepare.outputs.version) || format('BitFun {0}', needs.prepare.outputs.version) }} | |
| generate_release_notes: true | |
| files: release-upload-assets/* | |
| fail_on_unmatched_files: true | |
| - name: Verify published updater manifest | |
| run: | | |
| curl -fsSL --retry 5 --retry-delay 3 \ | |
| "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.release_tag }}/latest.json" \ | |
| -o latest.published.json | |
| node scripts/verify-tauri-latest-json.mjs \ | |
| --manifest latest.published.json \ | |
| --version "${{ needs.prepare.outputs.version }}" \ | |
| --required-platforms "${REQUIRED_UPDATER_PLATFORMS}" \ | |
| --required-manual-platforms "windows-x86_64" \ | |
| --check-urls true | |
| - name: Verify published Linux binaries manifest | |
| if: needs.prepare.outputs.release_channel == 'stable' | |
| run: | | |
| curl -fsSL --retry 5 --retry-delay 3 \ | |
| "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/linux-binaries.json" \ | |
| -o linux-binaries.published.json | |
| test "$(jq -r '.version' linux-binaries.published.json)" = "${{ needs.prepare.outputs.version }}" | |
| while IFS= read -r cli_url; do | |
| curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sig" -o /dev/null | |
| curl -fsSL --retry 5 --retry-delay 3 "${cli_url}.sha256.sig" -o /dev/null | |
| done < <(jq -r '.platforms[].cli.url' linux-binaries.published.json) | |
| - name: Verify published Relay image descriptor | |
| if: needs.prepare.outputs.release_channel == 'stable' | |
| run: | | |
| curl -fsSL --retry 5 --retry-delay 3 \ | |
| "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json" \ | |
| -o relay-image.published.json | |
| test "$(jq -r '.tag' relay-image.published.json)" = "${{ needs.prepare.outputs.release_tag }}" | |
| test "$(jq -r '.image' relay-image.published.json)" = "ghcr.io/gcwing/bitfun-relay-server" | |
| jq -e '.digest | test("^sha256:[0-9a-f]{64}$")' relay-image.published.json >/dev/null | |
| curl -fsSL --retry 5 --retry-delay 3 \ | |
| "https://github.com/GCWing/BitFun/releases/download/${{ needs.prepare.outputs.release_tag }}/relay-image.json.sig" \ | |
| -o /dev/null | |
| - name: Resolve beta channel promotion | |
| id: beta-channel | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }} | |
| run: | | |
| set -euo pipefail | |
| channel_status="$(curl -sS --retry 5 --retry-delay 3 \ | |
| --output channel.release.json \ | |
| --write-out '%{http_code}' \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H 'Accept: application/vnd.github+json' \ | |
| -H 'X-GitHub-Api-Version: 2022-11-28' \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/channel-beta")" | |
| case "${channel_status}" in | |
| 200) | |
| echo "channel_exists=true" >>"$GITHUB_OUTPUT" | |
| curl -fsSL --retry 5 --retry-delay 3 \ | |
| "https://github.com/${GITHUB_REPOSITORY}/releases/download/channel-beta/latest.json" \ | |
| -o current.beta.json | |
| ;; | |
| 404) | |
| echo "channel_exists=false" >>"$GITHUB_OUTPUT" | |
| if [[ "${RELEASE_CHANNEL}" == "stable" ]]; then | |
| echo "promote=false" >>"$GITHUB_OUTPUT" | |
| echo "No beta channel exists; stable release does not need to create one." | |
| exit 0 | |
| fi | |
| ;; | |
| *) | |
| echo "Could not inspect channel-beta release (GitHub API returned ${channel_status})." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| args=(--candidate latest.published.json --github-output "$GITHUB_OUTPUT") | |
| if [[ -s current.beta.json ]]; then | |
| args+=(--current current.beta.json) | |
| fi | |
| node scripts/plan-channel-promotion.mjs "${args[@]}" | |
| - name: Publish beta channel manifest | |
| if: steps.beta-channel.outputs.promote == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CHECKOUT_REF: ${{ needs.prepare.outputs.checkout_ref }} | |
| CHANNEL_EXISTS: ${{ steps.beta-channel.outputs.channel_exists }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${CHANNEL_EXISTS}" != "true" ]]; then | |
| gh release create channel-beta \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --target "${CHECKOUT_REF}" \ | |
| --prerelease \ | |
| --title "BitFun Beta Update Channel" \ | |
| --notes "Mutable updater pointer. Installable beta releases use immutable version tags." | |
| fi | |
| mkdir -p beta-channel | |
| cp latest.published.json beta-channel/latest.json | |
| gh release upload channel-beta beta-channel/latest.json \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --clobber | |
| curl -fsSL --retry 5 --retry-delay 3 \ | |
| "https://github.com/${GITHUB_REPOSITORY}/releases/download/channel-beta/latest.json" \ | |
| -o channel-beta.published.json | |
| test "$(jq -r '.version' channel-beta.published.json)" = \ | |
| "${{ steps.beta-channel.outputs.candidate_version }}" | |
| # Nudge the openbitfun.com mirror to sync now instead of on its next | |
| # 10-minute cron tick. Until the mirror has these bytes, CN clients have | |
| # only the GitHub origin to fall back to. Best effort: the cron run is | |
| # still the source of truth, so a failed or unconfigured ping never fails | |
| # the release. Receiver: scripts/openbitfun-release-sync.sh. | |
| # Host restore: deploy/openbitfun-host/README.md. | |
| - name: Request openbitfun mirror sync | |
| if: github.repository == 'GCWing/BitFun' | |
| continue-on-error: true | |
| env: | |
| SYNC_WEBHOOK_URL: ${{ secrets.OPENBITFUN_SYNC_WEBHOOK_URL }} | |
| RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} | |
| RELEASE_CHANNEL: ${{ needs.prepare.outputs.release_channel }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${SYNC_WEBHOOK_URL:-}" ]]; then | |
| echo "OPENBITFUN_SYNC_WEBHOOK_URL is not configured; the mirror will pick this up on its next cron run." | |
| exit 0 | |
| fi | |
| curl -fsSL -X POST --retry 3 --retry-delay 5 --max-time 30 \ | |
| -H 'Content-Type: application/json' \ | |
| -d "{\"tag\":\"${RELEASE_TAG}\",\"channel\":\"${RELEASE_CHANNEL}\"}" \ | |
| "${SYNC_WEBHOOK_URL}" >/dev/null | |
| echo "Mirror sync requested for ${RELEASE_TAG}." |