Dedup /api/refresh (6h TTL) + live partial deploys #7
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: Mine GitHub user stats + deploy to Cloudflare | |
| on: | |
| schedule: | |
| - cron: "0 6 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| user: | |
| description: "Single GitHub login to mine (skips the full users.txt loop)." | |
| required: false | |
| push: | |
| branches: [main] | |
| paths: | |
| - "generate_stats.py" | |
| - "stats_template.html" | |
| - "cloudflare/**" | |
| - ".github/workflows/mine-and-deploy.yml" | |
| concurrency: | |
| group: mine-and-deploy | |
| cancel-in-progress: false | |
| jobs: | |
| mine-and-deploy: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # for appending new users to users.txt | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GH_MINING_TOKEN || github.token }} | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Install gh CLI | |
| run: | | |
| type -p gh >/dev/null || ( | |
| sudo apt-get update -qq && sudo apt-get install -y gh | |
| ) | |
| - name: Authenticate gh | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_MINING_TOKEN }} | |
| run: echo "$GH_TOKEN" | gh auth login --with-token | |
| - name: Determine target users | |
| id: targets | |
| working-directory: . | |
| env: | |
| INPUT_USER: ${{ inputs.user }} | |
| run: | | |
| set -e | |
| mkdir -p cloudflare/public | |
| # Build the list of users we'll mine THIS run. | |
| if [ -n "$INPUT_USER" ]; then | |
| echo "Single-user mine: $INPUT_USER" | |
| # Persist new users into users.txt so future scheduled runs | |
| # include them. | |
| if ! grep -qiE "^${INPUT_USER}$" cloudflare/users.txt; then | |
| echo "$INPUT_USER" >> cloudflare/users.txt | |
| echo "added=true" >> $GITHUB_OUTPUT | |
| fi | |
| echo "$INPUT_USER" > /tmp/targets.txt | |
| else | |
| echo "Full mine of users.txt" | |
| grep -vE '^\s*(#|$)' cloudflare/users.txt > /tmp/targets.txt | |
| fi | |
| # Pre-stage pirate's enhanced version | |
| if [ -f stats.html ]; then | |
| cp stats.html cloudflare/public/pirate.html | |
| fi | |
| cat /tmp/targets.txt | |
| - name: Mine each user (with live in-progress deploys) | |
| working-directory: . | |
| env: | |
| NO_COLOR: "1" | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| run: | | |
| set -e | |
| deploy_now() { | |
| (cd cloudflare && npx --yes wrangler@latest deploy --minify 2>&1 | | |
| tail -2) || echo "::warning::interim deploy failed" | |
| } | |
| watch_and_deploy() { | |
| # Watches stats_<user>.html every 30s while $1 (PID) is alive. | |
| # Copies any updated file into the deploy dir and re-deploys so | |
| # the live page shows partial data as mining progresses. | |
| local pid="$1" user="$2" src="stats_${user}.html" \ | |
| dst="cloudflare/public/${user}.html" last_mtime=0 | |
| while kill -0 "$pid" 2>/dev/null; do | |
| sleep 30 | |
| if [ -f "$src" ]; then | |
| local mtime | |
| mtime=$(stat -c %Y "$src" 2>/dev/null \ | |
| || stat -f %m "$src" 2>/dev/null || echo 0) | |
| if [ "$mtime" -gt "$last_mtime" ]; then | |
| cp "$src" "$dst" | |
| echo "::group::Interim deploy of @$user (live)" | |
| deploy_now | |
| echo "::endgroup::" | |
| last_mtime="$mtime" | |
| fi | |
| fi | |
| done | |
| } | |
| while IFS= read -r user || [ -n "$user" ]; do | |
| user="${user%%#*}" | |
| user="${user//[[:space:]]/}" | |
| [ -z "$user" ] && continue | |
| [ "$user" = "pirate" ] && continue | |
| echo "::group::Mining @$user" | |
| # Run mining in the background; watch loop deploys partials. | |
| python3 generate_stats.py --user "$user" \ | |
| --no-search-commits \ | |
| --max-api-fetches 1500 & | |
| MINE_PID=$! | |
| watch_and_deploy "$MINE_PID" "$user" & | |
| WATCH_PID=$! | |
| wait "$MINE_PID" || echo "::warning::mining @$user exited non-zero" | |
| # Stop the watcher and do a final deploy with the final HTML. | |
| kill "$WATCH_PID" 2>/dev/null || true | |
| wait "$WATCH_PID" 2>/dev/null || true | |
| if [ -f "stats_$user.html" ]; then | |
| cp "stats_$user.html" "cloudflare/public/$user.html" | |
| echo "::group::Final deploy of @$user" | |
| deploy_now | |
| echo "::endgroup::" | |
| fi | |
| echo "::endgroup::" | |
| done < /tmp/targets.txt | |
| - name: Commit added users.txt entries | |
| if: steps.targets.outputs.added == 'true' | |
| working-directory: cloudflare | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add users.txt | |
| git diff --staged --quiet || git commit -m "Add ${{ inputs.user }} to users.txt [skip ci]" | |
| git push || echo "::warning::push failed (no commit permission?)" | |
| - name: Final deploy | |
| working-directory: cloudflare | |
| run: npx --yes wrangler@latest deploy | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |