Sponsors Refresh #679
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
| name: Sponsors Refresh | |
| # Hourly job that fetches sponsor data from the GitHub Sponsors API and | |
| # commits the result to the repo. This is the SINGLE source of truth for | |
| # sponsor state — evolve.sh reads the committed files and does not hit | |
| # the API. Decoupling sponsor freshness from the 8h evolution gap means | |
| # SPONSORS.md / README.md / sponsors/*.json stay current even when no | |
| # evolution session runs. | |
| # | |
| # Side effect: refresh_sponsors.py opens shoutout issues for newly-eligible | |
| # sponsors ($10+ tier), which is why this job needs `issues: write` and | |
| # passes a bot GH_TOKEN to the processing step. | |
| on: | |
| schedule: | |
| - cron: '15 * * * *' # hourly, offset 15 minutes from the evolution cron to avoid push races | |
| workflow_dispatch: | |
| concurrency: | |
| group: sponsors-refresh | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| issues: write | |
| jobs: | |
| refresh: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Generate bot token | |
| id: bot-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ steps.bot-token.outputs.token }} | |
| ref: main | |
| fetch-depth: 1 | |
| - name: Detect bot identity | |
| id: bot-info | |
| run: | | |
| set -euo pipefail | |
| SLUG="${{ steps.bot-token.outputs.app-slug }}" | |
| if [ -z "$SLUG" ]; then | |
| echo "::error::GitHub App slug is empty." | |
| exit 1 | |
| fi | |
| echo "login=${SLUG}[bot]" >> "$GITHUB_OUTPUT" | |
| echo "email=${SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT" | |
| - name: Configure git | |
| run: | | |
| set -euo pipefail | |
| git config user.name "${{ steps.bot-info.outputs.login }}" | |
| git config user.email "${{ steps.bot-info.outputs.email }}" | |
| - name: Fetch sponsor data | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| run: | | |
| set -euo pipefail | |
| # GH_PAT must have read:user scope. gh writes either a result | |
| # or a {"errors": [...]} body to /tmp/sponsor_raw.json — either | |
| # way refresh_sponsors.py surfaces it loudly via FetchFailed. | |
| # We tolerate a non-zero gh exit here because the error body is | |
| # what the downstream processor needs to see. | |
| gh api graphql -f query='{ viewer { sponsorshipsAsMaintainer(first: 100, activeOnly: true) { totalCount nodes { isOneTimePayment sponsorEntity { ... on User { login } ... on Organization { login } } tier { monthlyPriceInCents isOneTime } } } } }' \ | |
| > /tmp/sponsor_raw.json 2>/tmp/sponsor_query_stderr.log || true | |
| if [ -s /tmp/sponsor_query_stderr.log ]; then | |
| echo "WARNING: gh sponsor query stderr:" | |
| sed 's/^/ /' /tmp/sponsor_query_stderr.log | |
| fi | |
| - name: Process and update sponsor files | |
| env: | |
| # Bot token for `gh issue create` (shoutout issues). Needs | |
| # `issues: write`, granted at the job level above. | |
| GH_TOKEN: ${{ steps.bot-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| OUTPUT=$(python3 scripts/refresh_sponsors.py) | |
| echo "→ refresh_sponsors output: $OUTPUT" | |
| - name: Commit and push if changed | |
| env: | |
| GH_TOKEN: ${{ steps.bot-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| git add sponsors/active.json sponsors/sponsor_info.json SPONSORS.md README.md | |
| if git diff --cached --quiet; then | |
| echo "→ No sponsor changes to commit." | |
| exit 0 | |
| fi | |
| git commit -m "sponsors: hourly refresh" | |
| # Rebase-on-race retry loop. The evolution workflow pushes to | |
| # main on a separate hourly schedule, so a race is expected. | |
| # We commit first, then loop: on push failure, fetch origin/main, | |
| # rebase our commit onto it, and retry. Abort (loudly) if rebase | |
| # fails — a conflict on auto-generated sponsor files means | |
| # something is seriously wrong and a human should look. | |
| for attempt in 1 2 3 4 5; do | |
| if git push origin HEAD:main; then | |
| echo "→ Push succeeded on attempt $attempt." | |
| exit 0 | |
| fi | |
| echo " Push failed (attempt $attempt) — rebasing onto origin/main and retrying..." | |
| git fetch origin main | |
| if ! git rebase origin/main; then | |
| git rebase --abort || true | |
| echo "::error::rebase onto origin/main failed — manual intervention required" | |
| exit 1 | |
| fi | |
| done | |
| echo "::error::push failed after 5 attempts" | |
| exit 1 |