torc 0.22.1

Workflow management system
name: Build Release Binaries

on:
  push:
    tags:
      - 'v*'  # Triggers on version tags like v0.7.0
  workflow_dispatch:  # Allows manual trigger from GitHub UI
    inputs:
      tag_name:
        description: 'Tag name for the release'
        required: false
        default: 'manual-build'

permissions:
  contents: write
  packages: write

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # macOS Apple Silicon
          - os: macos-14
            target: aarch64-apple-darwin
            use_cross: false

          # Linux x86_64 (musl for static binaries - works on all distros)
          - os: ubuntu-latest
            target: x86_64-unknown-linux-musl
            use_cross: false

          # Windows x86_64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            use_cross: false

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install cross-compilation tool
        if: matrix.use_cross
        run: |
          cargo install cross --git https://github.com/cross-rs/cross

      - name: Install musl tools (Linux musl)
        if: matrix.target == 'x86_64-unknown-linux-musl' && !matrix.use_cross
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools

      - name: Install OpenSSL for Windows
        if: matrix.os == 'windows-latest'
        run: |
          vcpkg install openssl:x64-windows-static-md
          echo "OPENSSL_DIR=C:/vcpkg/installed/x64-windows-static-md" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          echo "OPENSSL_ROOT_DIR=C:/vcpkg/installed/x64-windows-static-md" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          echo "OPENSSL_INCLUDE_DIR=C:/vcpkg/installed/x64-windows-static-md/include" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          echo "OPENSSL_LIB_DIR=C:/vcpkg/installed/x64-windows-static-md/lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          echo "VCPKG_ROOT=C:/vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

      - name: Cache cargo registry
        uses: actions/cache@v5
        with:
          path: ~/.cargo/registry
          key: ${{ runner.os }}-${{ matrix.target }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-${{ matrix.target }}-cargo-registry-

      - name: Cache cargo index
        uses: actions/cache@v5
        with:
          path: ~/.cargo/git
          key: ${{ runner.os }}-${{ matrix.target }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-${{ matrix.target }}-cargo-index-

      - name: Cache cargo build
        uses: actions/cache@v5
        with:
          path: target
          key: ${{ runner.os }}-${{ matrix.target }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-${{ matrix.target }}-cargo-build-

      - name: Set binary extension
        id: binary_ext
        shell: bash
        run: |
          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            echo "ext=.exe" >> $GITHUB_OUTPUT
          else
            echo "ext=" >> $GITHUB_OUTPUT
          fi

      - name: Install SQLx CLI
        run: cargo install sqlx-cli --no-default-features --features sqlite

      - name: Setup database
        run: |
          sqlx database setup --source torc-server/migrations
        env:
          DATABASE_URL: sqlite:db/sqlite/dev.db

      - name: Build binaries (with cross)
        if: matrix.use_cross
        run: |
          cross build --release --target ${{ matrix.target }} --workspace --all-features

      - name: Build binaries (native)
        if: "!matrix.use_cross"
        run: |
          cargo build --workspace --all-features --release --target ${{ matrix.target }}

      - name: Create release archive
        shell: bash
        run: |
          mkdir -p release
          cd target/${{ matrix.target }}/release

          # Determine archive name
          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            ARCHIVE_NAME="torc-${{ matrix.target }}.zip"
            7z a ../../../release/$ARCHIVE_NAME torc.exe torc-server.exe torc-slurm-job-runner.exe torc-dash.exe torc-htpasswd.exe torc-mcp-server.exe
          else
            ARCHIVE_NAME="torc-${{ matrix.target }}.tar.gz"
            tar czf ../../../release/$ARCHIVE_NAME torc torc-server torc-slurm-job-runner torc-dash torc-htpasswd torc-mcp-server
          fi

          cd ../../../release
          ls -lh

      - name: Upload artifacts
        uses: actions/upload-artifact@v7
        with:
          name: torc-${{ matrix.target }}
          path: release/*
          retention-days: 7

  publish-crate:
    name: Publish to crates.io
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - name: Publish to crates.io
        run: cargo publish --locked
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          SQLX_OFFLINE: "true"

  create-release:
    name: Create GitHub Release
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Download all artifacts
        uses: actions/download-artifact@v8
        with:
          path: artifacts

      - name: Display structure of downloaded files
        run: ls -R artifacts

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          draft: true
          generate_release_notes: true
          files: |
            artifacts/torc-aarch64-apple-darwin/*.tar.gz
            artifacts/torc-x86_64-unknown-linux-musl/*.tar.gz
            artifacts/torc-x86_64-pc-windows-msvc/*.zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  docker:
    name: Build and Push Docker Image
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Extract version and image name
        id: meta
        run: |
          echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
          echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT

      - name: Download Linux musl artifact
        uses: actions/download-artifact@v8
        with:
          name: torc-x86_64-unknown-linux-musl
          path: artifact

      - name: Extract binaries for Docker build
        run: tar xzf artifact/torc-x86_64-unknown-linux-musl.tar.gz -C artifact/

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          build-args: VERSION=${{ steps.meta.outputs.version }}
          tags: |
            ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.version }}
            ${{ steps.meta.outputs.image }}:latest

  deploy-docs:
    name: Deploy Versioned Docs
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    concurrency:
      group: "pages"
      cancel-in-progress: false
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Extract version from tag
        id: version
        run: echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT

      - name: Setup mdBook
        uses: peaceiris/actions-mdbook@v2
        with:
          mdbook-version: '0.4.52'

      - name: Install mdbook-mermaid
        run: cargo install mdbook-mermaid@0.16.0

      - name: Build documentation
        run: |
          cd docs
          VERSION="${{ steps.version.outputs.version }}"
          # Override site-url and edit-url for this release version
          sed -i "s|site-url = \"/torc/\"|site-url = \"/torc/${VERSION}/\"|" book.toml
          sed -i "s|edit-url-template = \"https://github.com/NatLabRockies/torc/edit/main/docs/{path}\"|edit-url-template = \"https://github.com/NatLabRockies/torc/tree/${VERSION}/docs/{path}\"|" book.toml
          mdbook build

      - name: Setup gh-pages branch
        run: .github/scripts/gh-pages-setup.sh
        env:
          GITHUB_TOKEN: ${{ github.token }}

      - name: Deploy versioned docs
        run: |
          cd gh-pages-deploy
          VERSION="${{ steps.version.outputs.version }}"

          # Copy build into versioned subdirectory
          rm -rf "${VERSION}"
          cp -r ../docs/book "${VERSION}"

          # Root redirect (only if missing)
          if [ ! -f index.html ]; then
            cp ../docs/redirect.html index.html
          fi

          # Update versions.json using inline Python
          python3 << 'PYEOF'
          import json, os, re, shutil

          version = os.environ.get("VERSION", "${{ steps.version.outputs.version }}")
          versions_file = "versions.json"

          # Load existing or create new
          if os.path.exists(versions_file):
              with open(versions_file) as f:
                  data = json.load(f)
          else:
              data = {"latest_release": None, "versions": []}

          # Ensure "latest (dev)" entry exists
          has_latest = any(v["version"] == "latest" for v in data["versions"])
          if not has_latest:
              data["versions"].insert(0, {
                  "version": "latest",
                  "label": "latest (main)",
                  "path": "/torc/latest/"
              })

          # Remove existing entry for this version if present
          data["versions"] = [v for v in data["versions"] if v["version"] != version]

          # Add new version entry after "latest"
          new_entry = {
              "version": version,
              "label": version,
              "path": f"/torc/{version}/"
          }

          # Find insertion point: after "latest", before other versions
          insert_idx = 1  # after "latest"
          data["versions"].insert(insert_idx, new_entry)

          # Sort release versions by semver (descending), keep "latest" first
          def semver_key(v):
              if v["version"] == "latest":
                  return (999, 999, 999)
              # Extracts major.minor.patch; pre-release suffixes are ignored for sorting
              m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", v["version"])
              if m:
                  return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
              return (0, 0, 0)

          data["versions"].sort(key=semver_key, reverse=True)

          # Update latest_release
          data["latest_release"] = version

          # Keep at most MAX_VERSIONS release versions (plus "latest").
          # Older versions beyond this limit are pruned from versions.json and disk.
          MAX_VERSIONS = 5
          release_versions = [v for v in data["versions"] if v["version"] != "latest"]
          if len(release_versions) > MAX_VERSIONS:
              pruned = release_versions[MAX_VERSIONS:]
              data["versions"] = [v for v in data["versions"] if v not in pruned]
              # Delete directories for pruned versions
              for p in pruned:
                  dir_name = p["version"]
                  if os.path.isdir(dir_name):
                      shutil.rmtree(dir_name)
                      print(f"Pruned old version: {dir_name}")

          with open(versions_file, "w") as f:
              json.dump(data, f, indent=2)
              f.write("\n")

          print(f"Updated versions.json: {json.dumps(data, indent=2)}")
          PYEOF
        env:
          VERSION: ${{ steps.version.outputs.version }}

      - name: Commit and push
        run: |
          cd gh-pages-deploy
          ../.github/scripts/gh-pages-commit.sh "Deploy docs ${VERSION} from ${GITHUB_SHA::8}"
        env:
          VERSION: ${{ steps.version.outputs.version }}