Fix NameError in list_search_commits final-cache write #33
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" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| # Persist per-user mining caches across runs so we don't re-fetch | |
| # PR details, commit stats, etc. that we've already pulled before. | |
| # Key includes the inputs.user (or 'full' for cron runs) so single- | |
| # user dispatches restore that user's cache specifically. | |
| - name: Restore mining caches | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| cache_*/api/ | |
| cache_*/bare/ | |
| cache/api/ | |
| cache/bare/ | |
| key: stats-cache-v1-${{ inputs.user || 'full' }}-${{ github.run_id }} | |
| restore-keys: | | |
| stats-cache-v1-${{ inputs.user || 'full' }}- | |
| stats-cache-v1- | |
| - name: Install gh CLI | |
| run: | | |
| type -p gh >/dev/null || ( | |
| sudo apt-get update -qq && sudo apt-get install -y gh | |
| ) | |
| # gh authenticates via the GH_TOKEN environment variable (which we | |
| # already set on each step that calls it); no explicit `gh auth login` | |
| # step needed. | |
| - 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" | |
| GH_TOKEN: ${{ secrets.GH_MINING_TOKEN }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| # Posted by generate_stats.py to /api/progress so the Worker's | |
| # loading page can render real-time phase info. | |
| STATS_PROGRESS_TOKEN: ${{ secrets.GH_MINING_TOKEN }} | |
| 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 }} |