File size: 5,260 Bytes
b9ed97d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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