name: Build Release Binaries
on:
push:
tags:
- 'v*' workflow_dispatch: 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:
- os: macos-14
target: aarch64-apple-darwin
use_cross: false
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
use_cross: false
- 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@v3
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 }}