name: HF ↔ GitHub Sync # Trigger on every push to main (GitHub → HF direction) # and hourly to pull any HF-side changes back (HF → GitHub direction) on: push: branches: [main] schedule: - cron: '0 * * * *' # hourly HF pull-check workflow_dispatch: inputs: force_direction: description: 'Force sync direction (hf-wins | gh-wins | auto)' required: false default: 'auto' env: HF_REPO_TYPE: space # model | dataset | space HF_REPO: ${{ vars.HF_REPO }} # e.g. your-org/codecraftlab jobs: sync: name: Sync HuggingFace ↔ GitHub runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout (full history) uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Configure git identity run: | git config user.email "sync-bot@codecraftlab.noreply" git config user.name "CodeCraftLab Sync Bot" - name: Install git-lfs run: | sudo apt-get install -y git-lfs git lfs install - name: Add HuggingFace remote run: | git remote add hf \ "https://user:${{ secrets.HF_TOKEN }}@huggingface.co/${HF_REPO_TYPE}s/${HF_REPO}" git fetch hf --prune - name: Detect divergence and resolve id: sync env: FORCE_DIRECTION: ${{ github.event.inputs.force_direction || 'auto' }} run: | set -euo pipefail HF_HEAD=$(git rev-parse hf/main 2>/dev/null || echo "NONE") GH_HEAD=$(git rev-parse HEAD) if [ "$HF_HEAD" = "NONE" ]; then echo "action=push-to-hf" >> "$GITHUB_OUTPUT" echo "reason=HF remote has no main branch — initial push" exit 0 fi BASE=$(git merge-base HEAD hf/main 2>/dev/null || echo "NONE") if [ "$FORCE_DIRECTION" = "hf-wins" ]; then echo "action=hf-wins" >> "$GITHUB_OUTPUT" echo "reason=Forced HF-wins override" elif [ "$FORCE_DIRECTION" = "gh-wins" ]; then echo "action=push-to-hf" >> "$GITHUB_OUTPUT" echo "reason=Forced GH-wins override" elif [ "$HF_HEAD" = "$GH_HEAD" ]; then echo "action=in-sync" >> "$GITHUB_OUTPUT" echo "reason=Already in sync" elif [ "$BASE" = "$GH_HEAD" ]; then # HF is ahead — pull HF → GitHub echo "action=hf-wins" >> "$GITHUB_OUTPUT" echo "reason=GitHub is behind HF — fast-forward" elif [ "$BASE" = "$HF_HEAD" ]; then # GitHub is ahead — push GitHub → HF echo "action=push-to-hf" >> "$GITHUB_OUTPUT" echo "reason=HF is behind GitHub — pushing" else # Both diverged — HF is source of truth echo "action=hf-wins" >> "$GITHUB_OUTPUT" echo "reason=CONFLICT: both diverged — HF wins (source of truth)" fi - name: "[In-sync] Nothing to do" if: steps.sync.outputs.action == 'in-sync' run: echo "✅ HF and GitHub are in sync — no action required." - name: "[Push] GitHub → HuggingFace" if: steps.sync.outputs.action == 'push-to-hf' run: | echo "📤 Pushing GitHub → HuggingFace" git push hf main - name: "[HF Wins] HuggingFace → GitHub" if: steps.sync.outputs.action == 'hf-wins' run: | echo "📥 HuggingFace wins — overwriting GitHub main" git reset --hard hf/main git push origin main --force-with-lease || git push origin main --force - name: Summary if: always() run: | echo "### Sync Result" >> "$GITHUB_STEP_SUMMARY" echo "- **Action:** ${{ steps.sync.outputs.action }}" >> "$GITHUB_STEP_SUMMARY" echo "- **Trigger:** ${{ github.event_name }}" >> "$GITHUB_STEP_SUMMARY" echo "- **Branch:** main" >> "$GITHUB_STEP_SUMMARY" # ------------------------------------------------------------------ # Validate HF Space config on every push # ------------------------------------------------------------------ validate-space-config: name: Validate HF Space README config runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check README frontmatter run: | python3 - <<'EOF' import re, sys with open("README.md") as f: content = f.read() match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) if not match: print("❌ README is missing HF Space YAML frontmatter") sys.exit(1) frontmatter = match.group(1) required_keys = ["title", "sdk", "app_port", "license"] missing = [k for k in required_keys if k + ":" not in frontmatter] if missing: print(f"❌ Missing frontmatter keys: {missing}") sys.exit(1) if "sdk: streamlit" in frontmatter: print("❌ sdk is still 'streamlit' — should be 'docker' for FastAPI") sys.exit(1) print("✅ HF Space frontmatter is valid") EOF