Sync SolverForge Use Cases #15
Workflow file for this run
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
| # ---------------------------------------------------------------------------- | |
| # Publish SolverForge uc-* apps to Hugging Face Spaces solverforge-*. | |
| # | |
| # Canonical release flow: | |
| # 1. Cut an app-scoped tag such as solverforge-hospital@1.0.2. | |
| # 2. This workflow validates the tag against uc-hospital/Cargo.toml, | |
| # uc-hospital/Cargo.lock, and uc-hospital/CHANGELOG.md. | |
| # 3. The matching uc-* subtree is pushed to the matching Hugging Face Space. | |
| # | |
| # Required secrets : HF_TOKEN (HF token with write role on the spaces) | |
| # Required variables: HF_ORGANIZATION (HF org or username, e.g. SolverForge) | |
| # ---------------------------------------------------------------------------- | |
| name: Sync SolverForge Use Cases | |
| on: | |
| push: | |
| tags: | |
| - "solverforge-deliveries@*" | |
| - "solverforge-fsr@*" | |
| - "solverforge-hospital@*" | |
| - "solverforge-lessons@*" | |
| workflow_dispatch: | |
| inputs: | |
| target: | |
| description: > | |
| Folder to sync (e.g., "uc-hospital") or "all" to sync everything. | |
| Manual syncs are for recovery; app tags are the release path. | |
| required: true | |
| default: "all" | |
| concurrency: | |
| group: hf-sync-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| detect: | |
| name: Resolve release target | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.build-matrix.outputs.matrix }} | |
| empty: ${{ steps.build-matrix.outputs.empty }} | |
| release_tag: ${{ steps.release-tag.outputs.tag_name }} | |
| release_version: ${{ steps.release-tag.outputs.version }} | |
| release_folder: ${{ steps.release-tag.outputs.folder }} | |
| steps: | |
| - name: Checkout full history | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate app release tag | |
| id: release-tag | |
| if: github.event_name == 'push' | |
| run: node scripts/verify-usecase-release-tag.cjs "${GITHUB_REF_NAME}" | |
| - name: Build matrix | |
| id: build-matrix | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_FOLDER: ${{ steps.release-tag.outputs.folder }} | |
| TARGET: ${{ github.event.inputs.target }} | |
| run: | | |
| set -euo pipefail | |
| OPEN_SOURCE_FOLDERS=$(cat <<'FOLDERS' | |
| uc-deliveries | |
| uc-fsr | |
| uc-hospital | |
| uc-lessons | |
| FOLDERS | |
| ) | |
| filter_open_source() { | |
| while IFS= read -r folder; do | |
| [ -n "$folder" ] || continue | |
| if printf '%s\n' "$OPEN_SOURCE_FOLDERS" | grep -Fxq "$folder"; then | |
| printf '%s\n' "$folder" | |
| else | |
| echo "::error::Folder '$folder' is not an open-source SolverForge use case." | |
| exit 1 | |
| fi | |
| done | |
| } | |
| if [ "$EVENT_NAME" = "push" ]; then | |
| FOLDERS=$(printf '%s\n' "$RELEASE_FOLDER" | filter_open_source) | |
| elif [ "${TARGET:-all}" = "all" ]; then | |
| FOLDERS="$OPEN_SOURCE_FOLDERS" | |
| else | |
| [ -d "$TARGET" ] || { echo "::error::Folder '$TARGET' not found."; exit 1; } | |
| FOLDERS=$(printf '%s\n' "$TARGET" | filter_open_source) | |
| fi | |
| if [ -z "$FOLDERS" ]; then | |
| echo "::notice::No uc-* folder selected, nothing to sync." | |
| echo "matrix={\"folder\":[]}" >> "$GITHUB_OUTPUT" | |
| echo "empty=true" >> "$GITHUB_OUTPUT" | |
| else | |
| JSON=$(echo "$FOLDERS" | jq -R . | jq -sc '{"folder":.}') | |
| echo "Detected folders: $FOLDERS" | |
| echo "matrix=$JSON" >> "$GITHUB_OUTPUT" | |
| echo "empty=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| sync: | |
| name: Sync ${{ matrix.folder }} | |
| needs: detect | |
| if: needs.detect.outputs.empty == 'false' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: ${{ fromJson(needs.detect.outputs.matrix) }} | |
| fail-fast: false | |
| steps: | |
| - name: Checkout full history | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Configure git | |
| run: | | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git config user.name "github-actions[bot]" | |
| - name: Install Git LFS | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y git-lfs | |
| git lfs version | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install Node dependencies | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| - name: Install browser test dependencies | |
| run: make install-e2e | |
| - name: Validate release tag | |
| if: github.event_name == 'push' | |
| run: make release-ci APP="${{ matrix.folder }}" RELEASE_VERSION="${{ needs.detect.outputs.release_version }}" | |
| - name: Validate manual sync target | |
| if: github.event_name != 'push' | |
| run: make release-ci APP="${{ matrix.folder }}" | |
| - name: Build Hugging Face sync branch | |
| id: split | |
| run: | | |
| set -euo pipefail | |
| FOLDER="${{ matrix.folder }}" | |
| BRANCH="hf-push/${FOLDER}/${GITHUB_RUN_ID}" | |
| EXPORT_DIR="$(mktemp -d)" | |
| echo "Preparing $FOLDER as the Hugging Face Space root..." | |
| rsync -a --delete \ | |
| --exclude='.git' \ | |
| --exclude='target' \ | |
| --exclude='test-results' \ | |
| --exclude='playwright-report' \ | |
| --exclude='.osm_cache' \ | |
| "$FOLDER"/ "$EXPORT_DIR"/ | |
| git switch --orphan "$BRANCH" | |
| git rm -rf . >/dev/null 2>&1 || true | |
| find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} + | |
| rsync -a "$EXPORT_DIR"/ . | |
| rm -rf "$EXPORT_DIR" | |
| git lfs install --local | |
| git lfs track "docs/screenshot.png" | |
| git add .gitattributes . | |
| git commit -m "chore: sync ${FOLDER} Space" | |
| echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" | |
| - name: Push to Hugging Face Space | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| HF_ORGANIZATION: ${{ vars.HF_ORGANIZATION }} | |
| run: | | |
| FOLDER="${{ matrix.folder }}" | |
| BRANCH="${{ steps.split.outputs.branch }}" | |
| SPACE_NAME=$(echo "$FOLDER" | sed 's/^uc/solverforge/') | |
| SPACE="${HF_ORGANIZATION}/${SPACE_NAME}" | |
| HF_REMOTE="https://user:${HF_TOKEN}@huggingface.co/spaces/${SPACE}" | |
| echo "Pushing $FOLDER to hf.co/spaces/$SPACE (main branch)..." | |
| git push "$HF_REMOTE" "${BRANCH}:main" --force | |
| echo "::notice title=Sync OK::$FOLDER synced to hf.co/spaces/$SPACE" | |
| - name: Push release tag to Hugging Face Space | |
| if: github.event_name == 'push' | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| HF_ORGANIZATION: ${{ vars.HF_ORGANIZATION }} | |
| RELEASE_TAG: ${{ needs.detect.outputs.release_tag }} | |
| run: | | |
| FOLDER="${{ matrix.folder }}" | |
| BRANCH="${{ steps.split.outputs.branch }}" | |
| SPACE_NAME=$(echo "$FOLDER" | sed 's/^uc/solverforge/') | |
| SPACE="${HF_ORGANIZATION}/${SPACE_NAME}" | |
| HF_REMOTE="https://user:${HF_TOKEN}@huggingface.co/spaces/${SPACE}" | |
| git tag -f "$RELEASE_TAG" "$BRANCH" | |
| git push "$HF_REMOTE" "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" --force | |
| echo "::notice title=Tag Sync OK::$RELEASE_TAG synced to hf.co/spaces/$SPACE" | |
| - name: Cleanup temporary branch | |
| if: always() | |
| run: | | |
| BRANCH="${{ steps.split.outputs.branch }}" | |
| git switch --detach "$GITHUB_SHA" >/dev/null 2>&1 || true | |
| git branch -D "$BRANCH" 2>/dev/null || true |