Add launcher and direct application distribution #1
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: Launcher Release | |
| on: | |
| push: | |
| branches: | |
| - rust-wip | |
| paths: | |
| - .github/workflows/launcher-release.yml | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: Branch, tag, or commit to package. | |
| required: true | |
| default: rust-wip | |
| type: string | |
| engine_repo: | |
| description: Public repository containing engine release archives. | |
| required: true | |
| default: gajop/spring-rust | |
| type: string | |
| engine_release_tag: | |
| description: Immutable engine release tag to include and reference. | |
| required: true | |
| type: string | |
| release_tag: | |
| description: Immutable SemVer launcher/editor release tag, for example v0.2.0-beta.1. | |
| required: true | |
| type: string | |
| channel: | |
| description: Update channel whose pointer manifests should move to this release. | |
| required: true | |
| default: beta | |
| type: choice | |
| options: | |
| - stable | |
| - beta | |
| - nightly | |
| publish: | |
| description: Sign artifacts, publish the version release, and update the channel pointer. | |
| required: true | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: launcher-release-${{ inputs.release_tag }} | |
| cancel-in-progress: false | |
| jobs: | |
| quality: | |
| name: Launcher quality (Linux) | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.ref || github.ref }} | |
| submodules: recursive | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy, rustfmt | |
| - uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: "3.13" | |
| - name: Rust checks | |
| run: | | |
| cargo fmt --manifest-path launcher/Cargo.toml --all -- --check | |
| cargo clippy --manifest-path launcher/Cargo.toml --workspace --all-targets --all-features -- -D warnings | |
| cargo test --manifest-path launcher/Cargo.toml --workspace --all-features | |
| - uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| manifest-path: launcher/Cargo.toml | |
| command: check advisories | |
| arguments: --all-features | |
| command-arguments: --config launcher/deny.toml | |
| - name: Packager checks | |
| run: | | |
| uv run --project ./build --locked ruff check build/sbc_packager | |
| uv run --project ./build --locked pyright build/sbc_packager | |
| quality-windows: | |
| name: Launcher quality (Windows) | |
| runs-on: windows-2025 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.ref || github.ref }} | |
| submodules: recursive | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy, rustfmt | |
| - name: Windows Rust checks | |
| run: | | |
| cargo fmt --manifest-path launcher/Cargo.toml --all -- --check | |
| cargo clippy --manifest-path launcher/Cargo.toml --workspace --all-targets --all-features -- -D warnings | |
| cargo test --manifest-path launcher/Cargo.toml --workspace --all-features | |
| build: | |
| name: Build ${{ matrix.target }} | |
| needs: | |
| - quality | |
| - quality-windows | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux | |
| target: linux-x86_64 | |
| runner: ubuntu-24.04 | |
| setup_id: latest-linux | |
| engine_binary: spring | |
| engine_platform: amd64-linux | |
| cli_binary: sbc | |
| native_plugin: librust_plugin.so | |
| desktop_bundle: appimage | |
| desktop_source: launcher/target/release/bundle/appimage/SpringBoard_VERSION_amd64.AppImage | |
| desktop_name: SpringBoard-download_VERSION_amd64.AppImage | |
| standalone_name: SpringBoard-standalone_VERSION_amd64.AppImage | |
| cli_archive_suffix: tar.gz | |
| - platform: win32 | |
| target: windows-x86_64 | |
| runner: windows-2025 | |
| setup_id: latest-win | |
| engine_binary: spring.exe | |
| engine_platform: amd64-windows | |
| cli_binary: sbc.exe | |
| native_plugin: rust_plugin.dll | |
| desktop_bundle: nsis | |
| desktop_source: launcher/target/release/bundle/nsis/SpringBoard_VERSION_x64-setup.exe | |
| desktop_name: SpringBoard-download_VERSION_x64-setup.exe | |
| standalone_name: SpringBoard-standalone_VERSION_x64-setup.exe | |
| cli_archive_suffix: zip | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| submodules: recursive | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: launcher/tauri/package-lock.json | |
| - uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: "3.13" | |
| - name: Install Linux packaging dependencies | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install --yes \ | |
| libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev \ | |
| librsvg2-dev patchelf p7zip-full | |
| - name: Validate release inputs | |
| shell: bash | |
| env: | |
| PUBLISH: ${{ inputs.publish }} | |
| PUBLIC_KEY: ${{ vars.TAURI_SIGNING_PUBLIC_KEY }} | |
| run: | | |
| version="${{ inputs.release_tag }}" | |
| version="${version#v}" | |
| if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then | |
| echo "release_tag must be SemVer, optionally prefixed with v" >&2 | |
| exit 1 | |
| fi | |
| case "${{ inputs.channel }}" in | |
| stable) | |
| [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { | |
| echo "stable releases require a plain x.y.z version" >&2 | |
| exit 1 | |
| } | |
| ;; | |
| beta) | |
| [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9A-Za-z.-]+$ ]] || { | |
| echo "beta releases require an x.y.z-beta.N version" >&2 | |
| exit 1 | |
| } | |
| ;; | |
| nightly) | |
| [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-nightly\.[0-9A-Za-z.-]+$ ]] || { | |
| echo "nightly releases require an x.y.z-nightly.DATE.N version" >&2 | |
| exit 1 | |
| } | |
| ;; | |
| esac | |
| if [[ "$PUBLISH" == "true" ]]; then | |
| test -n "$PUBLIC_KEY" || { echo "TAURI_SIGNING_PUBLIC_KEY variable is required" >&2; exit 1; } | |
| fi | |
| - name: Install frontend dependencies | |
| working-directory: launcher/tauri | |
| run: npm ci | |
| - name: Build CLI and native plugin | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| SBC_VERSION="$version" cargo build --manifest-path launcher/Cargo.toml --release -p sbc | |
| cargo build --manifest-path native/Cargo.toml --release | |
| - name: Download and test engine archive | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ENGINE_REPO: ${{ inputs.engine_repo }} | |
| ENGINE_TAG: ${{ inputs.engine_release_tag }} | |
| run: | | |
| archive="recoil_${ENGINE_TAG}_${{ matrix.engine_platform }}.7z" | |
| gh release download "$ENGINE_TAG" \ | |
| --repo "$ENGINE_REPO" \ | |
| --pattern "$archive" \ | |
| --dir .launcher-release/engine | |
| 7z t ".launcher-release/engine/$archive" | |
| - name: Build editor bundle and release config | |
| shell: bash | |
| env: | |
| ENGINE_REPO: ${{ inputs.engine_repo }} | |
| ENGINE_TAG: ${{ inputs.engine_release_tag }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| CHANNEL: ${{ inputs.channel }} | |
| PUBLIC_KEY: ${{ vars.TAURI_SIGNING_PUBLIC_KEY }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| engine_archive="recoil_${ENGINE_TAG}_${{ matrix.engine_platform }}.7z" | |
| editor_asset="springboard-editor-${version}-${{ matrix.target }}.zip" | |
| mkdir -p artifacts .launcher-release/config | |
| uv run --project ./build --locked sbc-packager-editor \ | |
| --repo-root . \ | |
| --native-plugin "native/target/release/${{ matrix.native_plugin }}" \ | |
| --output "artifacts/$editor_asset" \ | |
| --platform "${{ matrix.platform }}" \ | |
| --run-config config/ui-rust.json \ | |
| --git-hash "$version" | |
| channel_base="https://github.com/${GITHUB_REPOSITORY}/releases/download/channel-${CHANNEL}" | |
| engine_url="https://github.com/${ENGINE_REPO}/releases/download/${ENGINE_TAG}/${engine_archive}" | |
| editor_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}/${editor_asset}" | |
| config_args=( | |
| --config-in dist_cfg/config.json | |
| --output ".launcher-release/config/distribution-${{ matrix.target }}.json" | |
| --setup-id "${{ matrix.setup_id }}" | |
| --platform "${{ matrix.platform }}" | |
| --channel "$CHANNEL" | |
| --engine-version "$ENGINE_TAG" | |
| --engine-url "$engine_url" | |
| --engine-archive ".launcher-release/engine/$engine_archive" | |
| --editor-version "$version" | |
| --editor-game "SpringBoard Core $version" | |
| --editor-url "$editor_url" | |
| --editor-archive "artifacts/$editor_asset" | |
| ) | |
| if [[ -n "$PUBLIC_KEY" ]]; then | |
| printf '%s' "$PUBLIC_KEY" > .launcher-release/updater.pub | |
| config_args+=( | |
| --channel-base-url "$channel_base" | |
| --public-key-file .launcher-release/updater.pub | |
| ) | |
| fi | |
| uv run --project ./build --locked sbc-packager-release-config "${config_args[@]}" | |
| - name: Build CLI-only download package | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| raw_cli="sbc-${version}-${{ matrix.target }}${{ matrix.platform == 'win32' && '.exe' || '' }}" | |
| cp "launcher/target/release/${{ matrix.cli_binary }}" "artifacts/$raw_cli" | |
| uv run --project ./build --locked sbc-packager-cli-download \ | |
| --cli-binary "artifacts/$raw_cli" \ | |
| --config ".launcher-release/config/distribution-${{ matrix.target }}.json" \ | |
| --output "artifacts/sbc-${version}-${{ matrix.target }}.${{ matrix.cli_archive_suffix }}" \ | |
| --platform "${{ matrix.platform }}" | |
| - name: Prepare standalone resources | |
| shell: bash | |
| env: | |
| ENGINE_TAG: ${{ inputs.engine_release_tag }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| engine_archive="recoil_${ENGINE_TAG}_${{ matrix.engine_platform }}.7z" | |
| editor_asset="springboard-editor-${version}-${{ matrix.target }}.zip" | |
| engine_dir=".launcher-release/standalone-files/engine/$ENGINE_TAG" | |
| editor_dir=".launcher-release/standalone-files/editor/$version" | |
| mkdir -p "$engine_dir" "$editor_dir" | |
| 7z x -y ".launcher-release/engine/$engine_archive" "-o$engine_dir" | |
| 7z x -y "artifacts/$editor_asset" "-o$editor_dir" | |
| test -f "$engine_dir/${{ matrix.engine_binary }}" | |
| editor_sdz="$editor_dir/games/SpringBoard Core $version.sdz" | |
| test -f "$editor_sdz" | |
| 7z l "$editor_sdz" | grep --fixed-strings "native/${{ matrix.native_plugin }}" | |
| - name: Generate Tauri flavor configs | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| CHANNEL: ${{ inputs.channel }} | |
| PUBLIC_KEY: ${{ vars.TAURI_SIGNING_PUBLIC_KEY }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| channel_base="https://github.com/${GITHUB_REPOSITORY}/releases/download/channel-${CHANNEL}" | |
| common_args=( | |
| --version "$version" | |
| --config ".launcher-release/config/distribution-${{ matrix.target }}.json" | |
| ) | |
| if [[ -n "$PUBLIC_KEY" ]]; then | |
| common_args+=(--channel-base-url "$channel_base" --public-key-file .launcher-release/updater.pub) | |
| fi | |
| uv run --project ./build --locked sbc-packager-tauri-config \ | |
| --output .launcher-release/tauri-download.json \ | |
| "${common_args[@]}" \ | |
| --no-updater-artifacts | |
| uv run --project ./build --locked sbc-packager-tauri-config \ | |
| --output .launcher-release/tauri-standalone.json \ | |
| "${common_args[@]}" \ | |
| --content .launcher-release/standalone-files \ | |
| --no-updater-artifacts | |
| - name: Build download-mode desktop package | |
| shell: bash | |
| run: | | |
| npm run --prefix launcher/tauri tauri -- build \ | |
| --bundles "${{ matrix.desktop_bundle }}" \ | |
| --config "$PWD/.launcher-release/tauri-download.json" | |
| - name: Collect download-mode desktop package | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| source_path="${{ matrix.desktop_source }}" | |
| source_path="${source_path//VERSION/$version}" | |
| output_name="${{ matrix.desktop_name }}" | |
| output_name="${output_name//VERSION/$version}" | |
| cp "$source_path" "artifacts/$output_name" | |
| - name: Build standalone desktop package | |
| shell: bash | |
| run: | | |
| npm run --prefix launcher/tauri tauri -- build \ | |
| --bundles "${{ matrix.desktop_bundle }}" \ | |
| --config "$PWD/.launcher-release/tauri-standalone.json" | |
| - name: Collect standalone desktop package | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| run: | | |
| version="${RELEASE_TAG#v}" | |
| source_path="${{ matrix.desktop_source }}" | |
| source_path="${source_path//VERSION/$version}" | |
| output_name="${{ matrix.standalone_name }}" | |
| output_name="${output_name//VERSION/$version}" | |
| cp "$source_path" "artifacts/$output_name" | |
| - name: Copy distribution config for manifest assembly | |
| shell: bash | |
| run: | | |
| cp ".launcher-release/config/distribution-${{ matrix.target }}.json" artifacts/ | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: launcher-${{ matrix.target }} | |
| path: artifacts/ | |
| if-no-files-found: error | |
| compression-level: 0 | |
| publish: | |
| name: Publish version and ${{ inputs.channel }} channel | |
| needs: build | |
| if: github.event_name == 'workflow_dispatch' && inputs.publish | |
| runs-on: ubuntu-24.04 | |
| environment: release | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: launcher/tauri/package-lock.json | |
| - uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: "3.13" | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: launcher-* | |
| path: release-inputs | |
| merge-multiple: true | |
| - name: Sign desktop artifacts and assemble manifests | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| mkdir -p channel-assets | |
| test -n "$TAURI_SIGNING_PRIVATE_KEY" || { | |
| echo "TAURI_SIGNING_PRIVATE_KEY release-environment secret is required" >&2 | |
| exit 1 | |
| } | |
| test -n "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" || { | |
| echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD release-environment secret is required" >&2 | |
| exit 1 | |
| } | |
| version="${RELEASE_TAG#v}" | |
| linux_desktop="SpringBoard-download_${version}_amd64.AppImage" | |
| windows_desktop="SpringBoard-download_${version}_x64-setup.exe" | |
| linux_cli="sbc-${version}-linux-x86_64" | |
| windows_cli="sbc-${version}-windows-x86_64.exe" | |
| release_base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}" | |
| npm ci --prefix launcher/tauri | |
| npm run --prefix launcher/tauri tauri -- signer sign "$PWD/release-inputs/$linux_desktop" | |
| npm run --prefix launcher/tauri tauri -- signer sign "$PWD/release-inputs/$windows_desktop" | |
| uv run --project ./build --locked sbc-packager-release-manifests platform \ | |
| --output release-inputs/platform-linux-x86_64.json \ | |
| --target linux-x86_64 \ | |
| --cli-executable "release-inputs/$linux_cli" \ | |
| --cli-url "$release_base/$linux_cli" \ | |
| --desktop-artifact "release-inputs/$linux_desktop" \ | |
| --desktop-url "$release_base/$linux_desktop" \ | |
| --desktop-signature "release-inputs/$linux_desktop.sig" | |
| uv run --project ./build --locked sbc-packager-release-manifests platform \ | |
| --output release-inputs/platform-windows-x86_64.json \ | |
| --target windows-x86_64 \ | |
| --cli-executable "release-inputs/$windows_cli" \ | |
| --cli-url "$release_base/$windows_cli" \ | |
| --desktop-artifact "release-inputs/$windows_desktop" \ | |
| --desktop-url "$release_base/$windows_desktop" \ | |
| --desktop-signature "release-inputs/$windows_desktop.sig" | |
| uv run --project ./build --locked sbc-packager-release-manifests combine \ | |
| --version "$RELEASE_TAG" \ | |
| --platform-metadata release-inputs/platform-linux-x86_64.json \ | |
| --platform-metadata release-inputs/platform-windows-x86_64.json \ | |
| --distribution-config release-inputs/distribution-linux-x86_64.json \ | |
| --distribution-config release-inputs/distribution-windows-x86_64.json \ | |
| --cli-output channel-assets/cli-update.json \ | |
| --desktop-output channel-assets/desktop-update.json \ | |
| --distribution-output channel-assets/distribution.json | |
| npm run --prefix launcher/tauri tauri -- signer sign "$PWD/channel-assets/cli-update.json" | |
| npm run --prefix launcher/tauri tauri -- signer sign "$PWD/channel-assets/distribution.json" | |
| - name: Prepare immutable version assets | |
| run: | | |
| mkdir -p version-assets | |
| find release-inputs -maxdepth 1 -type f \ | |
| ! -name 'platform-*.json' \ | |
| ! -name 'distribution-*.json' \ | |
| -exec cp '{}' version-assets/ \; | |
| cp channel-assets/cli-update.json channel-assets/cli-update.json.sig version-assets/ | |
| cp channel-assets/desktop-update.json version-assets/ | |
| cp channel-assets/distribution.json channel-assets/distribution.json.sig version-assets/ | |
| cd version-assets | |
| sha256sum -- * | sort -k2 > SHA256SUMS | |
| - name: Publish immutable version release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| CHANNEL: ${{ inputs.channel }} | |
| run: | | |
| prerelease=() | |
| if [[ "$CHANNEL" != "stable" ]]; then prerelease=(--prerelease); fi | |
| if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then | |
| echo "Refusing to overwrite immutable release $RELEASE_TAG" >&2 | |
| exit 1 | |
| fi | |
| gh release create "$RELEASE_TAG" version-assets/* \ | |
| --target "${{ inputs.ref }}" \ | |
| --title "SpringBoard $RELEASE_TAG" \ | |
| --generate-notes \ | |
| "${prerelease[@]}" | |
| - name: Move signed channel pointer | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CHANNEL: ${{ inputs.channel }} | |
| run: | | |
| channel_tag="channel-$CHANNEL" | |
| if gh release view "$channel_tag" >/dev/null 2>&1; then | |
| gh release upload "$channel_tag" channel-assets/* --clobber | |
| else | |
| gh release create "$channel_tag" channel-assets/* \ | |
| --target "${{ inputs.ref }}" \ | |
| --title "SpringBoard $CHANNEL update channel" \ | |
| --notes "Mutable signed update pointers for the $CHANNEL channel." \ | |
| --prerelease | |
| fi |