diff --git a/.gitea/workflows/sync-github-mirror.yml b/.gitea/workflows/sync-github-mirror.yml new file mode 100644 index 0000000..edd9495 --- /dev/null +++ b/.gitea/workflows/sync-github-mirror.yml @@ -0,0 +1,81 @@ +# Auto-sync this Gitea repo to its public GitHub mirror on every push to main. +# +# Design (deliberate trade-offs): +# - Push-driven from Gitea (this repo IS the source of truth); GitHub is a mirror. +# - Each sync = one snapshot commit on GitHub referencing the source Gitea SHA. +# GitHub gets a real, growing history (one commit per Gitea push that changed +# the mirrored tree); NO force-push, NO history rewrites. +# - The mirror tree is FILTERED: `blog/`, `handoff.md`, `.claude/`, `.gitea/`, +# and the usual generated junk are never copied. They are not in the GitHub +# history either (per the original "do not push these" rule). +# - If a Gitea push touches only excluded paths, the rsync produces no diff and +# the workflow exits clean (no empty commit on GitHub). +# +# Prereqs (one-time): +# - Repo secret GH_MIRROR_TOKEN holds a GitHub PAT with `repo` scope (push to +# recklessop/ai-workflow-course). Name avoids the GITHUB_ reserved prefix. +# - The GitHub mirror exists at github.com/recklessop/ai-workflow-course. +name: Sync to GitHub mirror +on: + push: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: sync-github-mirror + cancel-in-progress: false # serialize; never cancel a sync mid-push + +jobs: + sync: + runs-on: docker + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Sync filtered tree to GitHub + shell: bash + env: + GH_MIRROR_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }} + run: | + set -euo pipefail + if [ -z "${GH_MIRROR_TOKEN:-}" ]; then + echo "::error::GH_MIRROR_TOKEN secret not set; see this workflow's header." + exit 1 + fi + command -v rsync >/dev/null || { apt-get update && apt-get install -y --no-install-recommends rsync; } + + GH_REPO="recklessop/ai-workflow-course" + SRC_SHA="$(git rev-parse --short HEAD)" + + # Clone the GitHub mirror into a sibling working dir + git clone --depth=1 "https://x-access-token:${GH_MIRROR_TOKEN}@github.com/${GH_REPO}.git" gh-mirror + + # Mirror this checkout's tree into gh-mirror/ with the exclusions. + # --delete drops files removed on the source; --exclude='.git' protects + # both repos' .git dirs from rsync touching them. + rsync -a --delete \ + --exclude='.git' \ + --exclude='.gitea/' \ + --exclude='.claude/' \ + --exclude='blog/' \ + --exclude='handoff.md' \ + --exclude='__pycache__/' \ + --exclude='*.pyc' \ + --exclude='tasks.json' \ + --exclude='.DS_Store' \ + ./ gh-mirror/ + + cd gh-mirror + git add -A + if git diff --cached --quiet; then + echo "no relevant changes for the mirror (source push only touched excluded paths); skipping" + exit 0 + fi + + git config user.name "Justin Paul" + git config user.email "justin@jpaul.me" + git commit -m "sync from gitea @ ${SRC_SHA}" + # Plain push: each sync is a fast-forward append (no rewrites). If a + # stranger pushed to GitHub main between clone and push, --force-with-lease + # would tell us; here we let it fail loudly so we notice the divergence. + git push origin HEAD:main