Repository navigation
🚀 Sync gh-pages from main #259
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Routine deploy sync: on a schedule (and on demand), if `main` is ahead of the | |
| # `gh-pages` Pages branch AND main's latest commit is fully green in CI, open a | |
| # PR from `main` into `gh-pages` and merge it. GitHub Pages (legacy "Deploy from | |
| # a branch") then builds `gh-pages` — the only run triggered by that update. | |
| # | |
| # Why a schedule instead of reacting to CI: by the time the routine fires, main's | |
| # CI has already finished, so we just read the existing check results — no | |
| # waiting, no reruns. `gh-pages` is a pure mirror of `main` (kept in lockstep by | |
| # this workflow), so the PR is always a clean, conflict-free merge. | |
| # | |
| # Only the Pages build runs on the gh-pages update because: | |
| # • every push-triggered workflow is scoped to main/master (validate-solutions | |
| # included), and | |
| # • the merge is performed with GITHUB_TOKEN, whose pushes don't start new | |
| # Actions runs — while the legacy Pages build (a Pages-service job) still | |
| # fires on the branch update. | |
| # | |
| # NOTE: scheduled/dispatch runs use this file from the default branch, so it only | |
| # takes effect once merged to `main`. | |
| name: 🚀 Sync gh-pages from main | |
| on: | |
| schedule: | |
| # 2×/day: 14:20 UTC catches the daytime bot-merge window (bot merges cluster | |
| # ~08:50–14:03 UTC), 02:20 UTC catches evening dependabot merges. The old | |
| # 6-hourly cadence no-oped ~58% of the time ("gh-pages already up to date"); | |
| # the workflow still self-gates on ahead+green, and workflow_dispatch covers | |
| # publish-now needs. Tunable. | |
| - cron: '20 2 * * *' # daily (was twice daily) | |
| workflow_dispatch: | |
| permissions: | |
| contents: write # push the merge to gh-pages | |
| pull-requests: write # open + merge the PR | |
| checks: read # read main's check-runs | |
| statuses: read # read main's commit statuses | |
| actions: read # resolve this run's check_suite_id (self-exclusion) | |
| concurrency: | |
| group: sync-gh-pages | |
| cancel-in-progress: false | |
| jobs: | |
| sync: | |
| name: Open & merge main → gh-pages when green | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: 🏰 Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| # A PAT (if provided) guarantees the legacy Pages build fires everywhere; | |
| # otherwise the default token, which is enough for legacy Pages. | |
| token: ${{ secrets.GH_PAGES_DEPLOY_TOKEN || secrets.GITHUB_TOKEN }} | |
| - name: 🔎 Is main ahead of gh-pages? | |
| id: ahead | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags --prune origin main gh-pages | |
| COUNT="$(git rev-list --count origin/gh-pages..origin/main)" | |
| SHA="$(git rev-parse origin/main)" | |
| echo "count=$COUNT" >> "$GITHUB_OUTPUT" | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| if [ "$COUNT" -eq 0 ]; then | |
| echo "✅ gh-pages already up to date with main — nothing to sync." | tee -a "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "main is $COUNT commit(s) ahead of gh-pages (HEAD ${SHA})." | tee -a "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: 🟢 Is main's latest commit green? | |
| id: ci | |
| if: steps.ahead.outputs.count != '0' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ steps.ahead.outputs.sha }} | |
| RUN_ID: ${{ github.run_id }} | |
| run: | | |
| python3 - <<'PY' | |
| import json, os, subprocess, sys | |
| repo = os.environ["REPO"] | |
| sha = os.environ["SHA"] | |
| out = os.environ["GITHUB_OUTPUT"] | |
| FAILISH = {"failure", "cancelled", "timed_out", | |
| "action_required", "startup_failure", "stale"} | |
| def gh(path): | |
| r = subprocess.run( | |
| ["gh", "api", "-H", "Accept: application/vnd.github+json", path], | |
| capture_output=True, text=True) | |
| if r.returncode != 0: | |
| sys.stderr.write(r.stderr) | |
| raise SystemExit(f"gh api failed: {path}") | |
| return json.loads(r.stdout or "{}") | |
| # This workflow runs ON main's HEAD, so its own job shows up among the | |
| # sha's check runs as in-progress — counting it makes the state | |
| # permanently "pending" and the merge step unreachable (the bug that | |
| # silently blocked every scheduled sync). Exclude the check suite | |
| # belonging to THIS run before judging greenness: never grade yourself. | |
| self_suite = gh(f"repos/{repo}/actions/runs/{os.environ['RUN_ID']}").get("check_suite_id") | |
| pending, failed, passed = [], [], [] | |
| for run in gh(f"repos/{repo}/commits/{sha}/check-runs?per_page=100").get("check_runs", []): | |
| n = run.get("name", "?") | |
| if run.get("check_suite", {}).get("id") == self_suite: | |
| continue | |
| if run.get("status") != "completed": | |
| pending.append(n) | |
| elif run.get("conclusion") in FAILISH: | |
| failed.append(f"{n}={run.get('conclusion')}") | |
| else: # success / neutral / skipped | |
| passed.append(n) | |
| # Classic commit statuses (external integrations). Guard total_count: | |
| # the combined state is "pending" when there are zero statuses. | |
| st = gh(f"repos/{repo}/commits/{sha}/status") | |
| if st.get("total_count", 0) > 0: | |
| s = st.get("state") | |
| if s == "pending": | |
| pending.append("commit-status") | |
| elif s in ("failure", "error"): | |
| failed.append(f"commit-status={s}") | |
| else: | |
| passed.append("commit-status") | |
| state = "pending" if pending else ("failed" if failed else "green") | |
| print(f"[ci] state={state} green={len(passed)} pending={pending} failed={failed}") | |
| with open(out, "a") as fh: | |
| fh.write(f"state={state}\n") | |
| PY | |
| - name: 🔀 Open or reuse PR, then merge it | |
| if: steps.ahead.outputs.count != '0' && steps.ci.outputs.state == 'green' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SHA: ${{ steps.ahead.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| NUM="$(gh pr list --base gh-pages --head main --state open --json number --jq '.[0].number // empty')" | |
| if [ -z "$NUM" ]; then | |
| SHORT="$(git rev-parse --short "$SHA")" | |
| URL="$(gh pr create --base gh-pages --head main \ | |
| --title "🚀 Deploy: sync gh-pages with main@${SHORT}" \ | |
| --body "Automated routine deploy sync. \`main\` is green and ahead of \`gh-pages\`; merging republishes the site via the GitHub Pages build. No CI is re-run.")" | |
| NUM="$(printf '%s\n' "$URL" | grep -oE '[0-9]+$' | tail -1)" | |
| echo "Opened PR #${NUM}: ${URL}" | |
| else | |
| echo "Reusing open PR #${NUM}." | |
| fi | |
| # Wait for GitHub to compute mergeability (a large sync PR isn't instant). | |
| for _ in $(seq 1 15); do | |
| STATE="$(gh pr view "$NUM" --json mergeable --jq .mergeable)" | |
| [ "$STATE" = "MERGEABLE" ] && break | |
| if [ "$STATE" = "CONFLICTING" ]; then | |
| echo "::error::PR #${NUM} conflicts — gh-pages has diverged from main." | |
| gh pr comment "$NUM" --body "🛑 Deploy sync can't merge: \`gh-pages\` diverged from \`main\`. Re-run the one-time reconciliation (merge main into gh-pages with \`-X theirs\`)." || true | |
| exit 1 | |
| fi | |
| sleep 4 | |
| done | |
| # Merge commit to preserve history; NEVER --delete-branch (the head is main!). | |
| if gh pr merge "$NUM" --merge; then | |
| echo "✅ Merged PR #${NUM} into gh-pages — the Pages build will deploy." | tee -a "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "::error::Could not merge PR #${NUM} (branch protection?); leaving it open." | |
| gh pr comment "$NUM" --body "🛑 Automated deploy sync could not merge this PR. Please resolve manually." || true | |
| exit 1 | |
| fi | |
| - name: 📊 Summary | |
| if: always() | |
| env: | |
| COUNT: ${{ steps.ahead.outputs.count }} | |
| STATE: ${{ steps.ci.outputs.state }} | |
| run: | | |
| { | |
| echo "## 🚀 gh-pages sync" | |
| if [ "${COUNT:-0}" = "0" ]; then | |
| echo "- gh-pages already up to date — no action." | |
| elif [ "$STATE" = "green" ]; then | |
| echo "- main green & ahead → PR opened and merged into gh-pages." | |
| elif [ "$STATE" = "pending" ]; then | |
| echo "- main CI still running — will retry on the next routine run." | |
| else | |
| echo "- main CI not green (\`${STATE:-unknown}\`) — not deploying." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" |