rlru 0.1.18

Rocket League replay uploader
Documentation
name: Build distributable binaries

on:
  push:
    branches:
      - main
    tags:
      - "v*"
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  checks:
    name: Format, lint, and test
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Check Nix formatting
        run: |
          nix fmt -- --check .

      - name: Check Rust formatting
        run: |
          nix develop --command cargo fmt --all -- --check

      - name: Run Clippy
        run: |
          nix develop --command cargo clippy --workspace --all-targets -- -D warnings

      - name: Run tests
        run: |
          nix develop --command cargo test --workspace

  linux:
    name: Linux CLI x86_64
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Build Linux CLI archive
        run: |
          nix build .#dist-cli-linux-x86_64 --out-link result-linux-cli

      - name: Stage Linux artifact
        run: |
          set -euo pipefail

          mkdir -p dist
          cp -L result-linux-cli dist/rlru-cli-linux-x86_64.tar.gz

      - name: Upload Linux artifact
        uses: actions/upload-artifact@v4
        with:
          name: rlru-cli-linux-x86_64
          path: dist/rlru-cli-linux-x86_64.tar.gz
          if-no-files-found: error

  windows:
    name: Windows CLI x86_64
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Build Windows CLI archive
        run: |
          nix build .#dist-cli-windows-x86_64 --out-link result-windows-cli

      - name: Stage Windows artifact
        run: |
          set -euo pipefail

          mkdir -p dist
          cp -L result-windows-cli dist/rlru-cli-windows-x86_64.zip

      - name: Upload Windows artifact
        uses: actions/upload-artifact@v4
        with:
          name: rlru-cli-windows-x86_64
          path: dist/rlru-cli-windows-x86_64.zip
          if-no-files-found: error

  linux-appimage:
    name: Linux Desktop AppImage x86_64
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Build Linux desktop AppImage
        run: |
          nix build .#rlru-dioxus-appimage --out-link result-appimage

      - name: Stage AppImage artifact
        run: |
          set -euo pipefail

          mkdir -p dist
          if [[ -d result-appimage ]]; then
            appimage="$(find result-appimage -maxdepth 1 -type f -name '*.AppImage' -print -quit)"
            if [[ -z "$appimage" ]]; then
              echo "no AppImage file found in result-appimage" >&2
              find result-appimage -maxdepth 2 -type f -print >&2
              exit 1
            fi
            cp -L "$appimage" dist/rlru-dioxus-linux-x86_64.AppImage
          else
            cp -L result-appimage dist/rlru-dioxus-linux-x86_64.AppImage
          fi
          chmod +x dist/rlru-dioxus-linux-x86_64.AppImage

      - name: Upload AppImage artifact
        uses: actions/upload-artifact@v4
        with:
          name: rlru-dioxus-linux-x86_64-appimage
          path: dist/rlru-dioxus-linux-x86_64.AppImage
          if-no-files-found: error

  android:
    name: Android app release
    runs-on: ubuntu-latest
    timeout-minutes: 90
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes
            accept-flake-config = true

      - name: Configure Nix cache
        uses: cachix/cachix-action@v15
        with:
          name: nix-community
          skipPush: true

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-rlru-dioxus-android-gradle-${{ hashFiles('flake.lock', 'Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-rlru-dioxus-android-gradle-

      - name: Build Android release APK and AAB
        env:
          ANDROID_SIGNING_KEYSTORE_BASE64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }}
          ANDROID_SIGNING_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEYSTORE_PASSWORD }}
          ANDROID_SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
          ANDROID_SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
        run: nix run .#dioxus-android-release

      - name: Stage Android artifacts
        shell: bash
        run: |
          set -euo pipefail

          mkdir -p dist
          mapfile -t artifacts < <(
            find target/dx/rlru-dioxus/release/android \
              \( -path '*/build/outputs/apk/release/*.apk' -o -path '*/build/outputs/bundle/release/*.aab' \) \
              -type f \
              | sort
          )

          if [[ "${#artifacts[@]}" -eq 0 ]]; then
            echo "No Android APK/AAB artifacts were produced" >&2
            exit 1
          fi

          for artifact in "${artifacts[@]}"; do
            cp -v "$artifact" "dist/rlru-dioxus-android-$(basename "$artifact")"
          done

          ls -lh dist

      - name: Upload Android artifacts
        uses: actions/upload-artifact@v4
        with:
          name: rlru-dioxus-android-release
          path: dist/*
          if-no-files-found: error

  cargo-package:
    name: Cargo package check
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Check Cargo packages
        run: |
          nix develop --command cargo package -p psynet --locked
          echo "rlru is packaged by cargo publish after psynet is visible on crates.io"

  cargo-publish:
    name: Publish Cargo packages
    runs-on: ubuntu-latest
    needs:
      - cargo-package
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          extra_nix_config: |
            experimental-features = nix-command flakes

      - name: Validate release tag
        run: |
          set -euo pipefail

          version="$(nix develop --command cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "rlru") | .version')"
          psynet_version="$(nix develop --command cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "psynet") | .version')"

          if [[ "$GITHUB_REF_NAME" != "v$version" ]]; then
            echo "release tag $GITHUB_REF_NAME does not match rlru version $version" >&2
            exit 1
          fi

          if [[ "$psynet_version" != "$version" ]]; then
            echo "psynet version $psynet_version does not match rlru version $version" >&2
            exit 1
          fi

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euo pipefail

          if [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
            echo "CARGO_REGISTRY_TOKEN secret is not configured; skipping crates.io publishing"
            exit 0
          fi

          crate_version() {
            nix develop --command cargo metadata --no-deps --format-version 1 \
              | jq -r ".packages[] | select(.name == \"$1\") | .version"
          }

          crate_exists() {
            local crate="$1"
            local version="$2"

            curl -fsS -A "rlru-ci/1.0 (https://github.com/rlrml/rlru)" \
              "https://crates.io/api/v1/crates/$crate/$version" >/dev/null 2>&1
          }

          cargo_publish_with_retries() {
            local crate="$1"

            for attempt in 1 2 3; do
              local output
              output=$(nix develop --command cargo publish -p "$crate" --locked 2>&1) && return 0

              if echo "$output" | grep -q "already exists"; then
                echo "$crate is already published; skipping"
                return 0
              fi

              echo "$output" >&2

              if [[ "$attempt" -eq 3 ]]; then
                return 1
              fi

              sleep_seconds=$((attempt * 20))
              echo "cargo publish failed for $crate on attempt $attempt; retrying in ${sleep_seconds}s..."
              sleep "$sleep_seconds"
            done
          }

          publish_crate() {
            local crate="$1"
            local version
            version="$(crate_version "$crate")"

            if crate_exists "$crate" "$version"; then
              echo "$crate $version is already published; skipping"
              return
            fi

            cargo_publish_with_retries "$crate"
          }

          wait_for_crate() {
            local crate="$1"
            local version
            version="$(crate_version "$crate")"

            for _ in {1..30}; do
              if crate_exists "$crate" "$version"; then
                return
              fi
              sleep 10
            done

            echo "$crate $version did not become visible on crates.io in time" >&2
            exit 1
          }

          publish_crate psynet
          wait_for_crate psynet
          publish_crate rlru

  github-release:
    name: Publish GitHub release
    runs-on: ubuntu-latest
    needs:
      - checks
      - linux
      - windows
      - linux-appimage
      - android
    if: startsWith(github.ref, 'refs/tags/v')
    permissions:
      contents: write
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Validate release tag
        run: |
          set -euo pipefail

          version="$(sed -n '0,/^version = "\(.*\)"$/s//\1/p' Cargo.toml)"
          if [[ "$GITHUB_REF_NAME" != "v$version" ]]; then
            echo "release tag $GITHUB_REF_NAME does not match rlru version $version" >&2
            exit 1
          fi

      - name: Download release artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - name: Publish release assets
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
        run: |
          set -euo pipefail

          if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
            gh release upload "$GITHUB_REF_NAME" dist/* --clobber
          else
            gh release create "$GITHUB_REF_NAME" dist/* \
              --title "$GITHUB_REF_NAME" \
              --generate-notes \
              --verify-tag
          fi