S-Dreamer/CodeCraftLab
0
1name: HF ↔ GitHub Sync2 3# Trigger on every push to main (GitHub → HF direction)4# and hourly to pull any HF-side changes back (HF → GitHub direction)5on:6 push:7 branches: [main]8 schedule:9 - cron: '0 * * * *' # hourly HF pull-check10 workflow_dispatch:11 inputs:12 force_direction:13 description: 'Force sync direction (hf-wins | gh-wins | auto)'14 required: false15 default: 'auto'16 17env:18 HF_REPO_TYPE: space # model | dataset | space19 HF_REPO: ${{ vars.HF_REPO }} # e.g. your-org/codecraftlab20 21jobs:22 sync:23 name: Sync HuggingFace ↔ GitHub24 runs-on: ubuntu-latest25 permissions:26 contents: write27 28 steps:29 - name: Checkout (full history)30 uses: actions/checkout@v431 with:32 fetch-depth: 033 token: ${{ secrets.GITHUB_TOKEN }}34 35 - name: Configure git identity36 run: |37 git config user.email "sync-bot@codecraftlab.noreply"38 git config user.name "CodeCraftLab Sync Bot"39 40 - name: Install git-lfs41 run: |42 sudo apt-get install -y git-lfs43 git lfs install44 45 - name: Add HuggingFace remote46 run: |47 git remote add hf \48 "https://user:${{ secrets.HF_TOKEN }}@huggingface.co/${HF_REPO_TYPE}s/${HF_REPO}"49 git fetch hf --prune50 51 - name: Detect divergence and resolve52 id: sync53 env:54 FORCE_DIRECTION: ${{ github.event.inputs.force_direction || 'auto' }}55 run: |56 set -euo pipefail57 58 HF_HEAD=$(git rev-parse hf/main 2>/dev/null || echo "NONE")59 GH_HEAD=$(git rev-parse HEAD)60 61 if [ "$HF_HEAD" = "NONE" ]; then62 echo "action=push-to-hf" >> "$GITHUB_OUTPUT"63 echo "reason=HF remote has no main branch — initial push"64 exit 065 fi66 67 BASE=$(git merge-base HEAD hf/main 2>/dev/null || echo "NONE")68 69 if [ "$FORCE_DIRECTION" = "hf-wins" ]; then70 echo "action=hf-wins" >> "$GITHUB_OUTPUT"71 echo "reason=Forced HF-wins override"72 elif [ "$FORCE_DIRECTION" = "gh-wins" ]; then73 echo "action=push-to-hf" >> "$GITHUB_OUTPUT"74 echo "reason=Forced GH-wins override"75 elif [ "$HF_HEAD" = "$GH_HEAD" ]; then76 echo "action=in-sync" >> "$GITHUB_OUTPUT"77 echo "reason=Already in sync"78 elif [ "$BASE" = "$GH_HEAD" ]; then79 # HF is ahead — pull HF → GitHub80 echo "action=hf-wins" >> "$GITHUB_OUTPUT"81 echo "reason=GitHub is behind HF — fast-forward"82 elif [ "$BASE" = "$HF_HEAD" ]; then83 # GitHub is ahead — push GitHub → HF84 echo "action=push-to-hf" >> "$GITHUB_OUTPUT"85 echo "reason=HF is behind GitHub — pushing"86 else87 # Both diverged — HF is source of truth88 echo "action=hf-wins" >> "$GITHUB_OUTPUT"89 echo "reason=CONFLICT: both diverged — HF wins (source of truth)"90 fi91 92 - name: "[In-sync] Nothing to do"93 if: steps.sync.outputs.action == 'in-sync'94 run: echo "✅ HF and GitHub are in sync — no action required."95 96 - name: "[Push] GitHub → HuggingFace"97 if: steps.sync.outputs.action == 'push-to-hf'98 run: |99 echo "📤 Pushing GitHub → HuggingFace"100 git push hf main101 102 - name: "[HF Wins] HuggingFace → GitHub"103 if: steps.sync.outputs.action == 'hf-wins'104 run: |105 echo "📥 HuggingFace wins — overwriting GitHub main"106 git reset --hard hf/main107 git push origin main --force-with-lease || git push origin main --force108 109 - name: Summary110 if: always()111 run: |112 echo "### Sync Result" >> "$GITHUB_STEP_SUMMARY"113 echo "- **Action:** ${{ steps.sync.outputs.action }}" >> "$GITHUB_STEP_SUMMARY"114 echo "- **Trigger:** ${{ github.event_name }}" >> "$GITHUB_STEP_SUMMARY"115 echo "- **Branch:** main" >> "$GITHUB_STEP_SUMMARY"116 117 # ------------------------------------------------------------------118 # Validate HF Space config on every push119 # ------------------------------------------------------------------120 validate-space-config:121 name: Validate HF Space README config122 runs-on: ubuntu-latest123 steps:124 - uses: actions/checkout@v4125 126 - name: Check README frontmatter127 run: |128 python3 - <<'EOF'129 import re, sys130 131 with open("README.md") as f:132 content = f.read()133 134 match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)135 if not match:136 print("❌ README is missing HF Space YAML frontmatter")137 sys.exit(1)138 139 frontmatter = match.group(1)140 required_keys = ["title", "sdk", "app_port", "license"]141 missing = [k for k in required_keys if k + ":" not in frontmatter]142 if missing:143 print(f"❌ Missing frontmatter keys: {missing}")144 sys.exit(1)145 146 if "sdk: streamlit" in frontmatter:147 print("❌ sdk is still 'streamlit' — should be 'docker' for FastAPI")148 sys.exit(1)149 150 print("✅ HF Space frontmatter is valid")151 EOF152 