#!/usr/bin/env bash # Phase 1 sync: GitHub is the source of truth, Windy Git follows. # # ── Why this direction, and why the other one was wrong ──────────────────── # # On 2026-08-13 nine repos were migrated writable with push-mirrors pointed AT # GitHub. That was premature: a dozen agent sessions on the Mac mini are pushing # to GitHub continuously, so GitHub — not Windy Git — is where the current work # actually lives. A push-mirror force-updates refs, so on its 8-hour timer it # would have pushed Windy Git's stale copy over live work, silently, with no # conflict to notice. The mirrors were removed before the first timer fired. # # This script is the correct Phase 1: pull from GitHub, push into Windy Git. # # Mac mini agents ──push──▶ GitHub ──this script──▶ Windy Git ──▶ CI on Veron # # **It requires nothing from anyone.** No remote changes, no coordination, no # "everybody stop pushing for a minute." Agents keep working exactly as they are # and CI starts running on 24 cores. # # Windy Git is force-updated on purpose. In Phase 1 it holds nothing anyone # depends on, so GitHub always wins and there is no merge to reconcile — which # is the entire point of not flipping direction until a repo is quiet. # # Phase 2, per repo, only when that repo is idle: point its agents at Windy Git, # drop it from REPOS here, and add a push-mirror back to GitHub. One repo at a # time. Never a big-bang cutover across a dozen live sessions. set -uo pipefail : "${GITHUB_TOKEN:?GITHUB_TOKEN required}" : "${GITEA_ADMIN_TOKEN:?GITEA_ADMIN_TOKEN required}" GH_OWNER="${GITHUB_OWNER:-sneakyfree}" WG="${WG_HOST:-app.windygit.com}" WG_OWNER="${WINDYGIT_OWNER:-windyadmin}" WORK="${SYNC_WORK:-/srv/windygit/sync}" FAILED=0 # Repos Windy Git tracks FROM GitHub. Remove a repo from this list at the moment # it flips to Windy-Git-first, or the sync will fight its authors and win. REPOS="${SYNC_REPOS:-windy-calendar windy-search windy-registry Windy-Clone WindyCloud windy-cloud-sites windy-mind eternitas windy-agent windy-git windy-chat windy-mail windy-connect windy-drops windy-code-web windy-code windy-traveler windy-translate windytranslate-site windytraveler-site windy-hand windy-cloud-domains windy-cloud-vps windytalk windy-pro windy-inbox windy-text windy-call windy-cell windy-hand-site windy-calendar-site}" # Repos whose TAGS must not reach Windy Git. A tag push fires `on: push: tags` # workflows; windy-pro's build-electron is a matrix over ubuntu/macos/windows- # latest, labels no runner here has, so every leg would queue forever (and # queued jobs are invisible in /actions/tasks). Releases are built elsewhere. NO_TAGS="${SYNC_NO_TAGS:-windy-pro}" # `archive/*` branches never reach Windy Git (negative refspec, git >= 2.29). # They are off-machine safety copies of unpushed work (one-repo doctrine), not # work in progress: GitHub holds them, and CI time on them is waste. mkdir -p "$WORK" log() { printf '[sync %s] %s\n' "$(date -u +%H:%M:%SZ)" "$*"; } for r in $REPOS; do bare="$WORK/${r}.git" if [[ ! -d "$bare" ]]; then git clone --quiet --bare "https://x-access-token:${GITHUB_TOKEN}@github.com/${GH_OWNER}/${r}.git" "$bare" 2>/dev/null \ || { log "FAILED initial clone of $r"; FAILED=1; continue; } fi # +refs/heads/* — branches only, deliberately. # # `--mirror` would also carry refs/pull/* (GitHub's read-only PR refs, which # Gitea rejects) and every remote-tracking ref, turning a working sync into a # wall of errors that hides the one that matters. if ! git --git-dir="$bare" fetch --quiet --prune origin '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' 2>/dev/null; then log "FAILED fetch $r"; FAILED=1; continue fi before="$(git --git-dir="$bare" rev-parse HEAD 2>/dev/null || echo none)" if git --git-dir="$bare" push --quiet --force \ "https://${WG_OWNER}:${GITEA_ADMIN_TOKEN}@${WG}/${WG_OWNER}/${r}.git" \ '+refs/heads/*:refs/heads/*' '^refs/heads/archive/*' $([[ " $NO_TAGS " == *" $r "* ]] || echo '+refs/tags/*:refs/tags/*') 2>/dev/null; then log "$r ok (${before:0:7})" else log "FAILED push $r -> windy git"; FAILED=1 fi done # Jobs that name labels no runner has (ubuntu/macos/windows-latest) would wait # forever and invisibly; cancel them after 30 min. Never fails the sync. # Both DB steps go through `docker exec`, which hangs outright while the host # is in an IO stall (09-23: data2 SMR cliff wedged this sync for 10+ min and # stopped mirroring + the bridge for every lane). They are optional; mirroring # and the bridge are not. Bound them so a stuck exec costs one step, not the run. timeout -k 10 120 bash "$(dirname "$0")/cancel_unrunnable.sh" || log "janitor failed or timed out (non-fatal)" # Private repos can't run GitHub Actions; mirror their open PRs here so CI # fires, and post the verdicts back to GitHub as commit statuses. if ! python3 "$(dirname "$0")/pr_status_bridge.py"; then log "FAILED pr status bridge"; FAILED=1 fi # Runner guard (Boss 10-01): PUBLIC sneakyfree repos have self-hosted GitHub runners on Veron. # A PR that changes a workflow so a stranger's code could reach one gets a red # windy-git/runner-guard status. Each status is posted once; never fails the sync. timeout -k 10 120 python3 "$(dirname "$0")/runner_guard.py" pr --post || log "runner-guard failed or timed out (non-fatal)" # CI telemetry -> admin.windyword.ai (shapes declared with Windy Telemetry 40). # Sends nothing until WINDYGIT_TELEMETRY_TOKEN is set; never fails the sync. timeout -k 10 180 python3 "$(dirname "$0")/telemetry_emit.py" || log "telemetry emit failed or timed out (non-fatal)" [[ "$FAILED" -ne 0 ]] && { log "COMPLETED WITH FAILURES"; exit 1; } log "all repos in step with GitHub"