Skip to content

Add launcher and direct application distribution #1

Add launcher and direct application distribution

Add launcher and direct application distribution #1

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