name: Refresh GitHub data # Keep the deployed GitHub snapshot (stats / releases / contributors / commits) # fresh without pushing code. Every 30 minutes the snapshot is regenerated from # the GitHub API and compared against what is currently deployed; only when the # numbers actually moved (stars, release, commits) is the Pages deploy # dispatched, which rebuilds the site and ships the fresh data. No change means # no rebuild — the site isn't rebuilt 48 times a day for nothing. # # Notes: # - The snapshot file (public/data/github.json) is gitignored and regenerated # on every build by the `prebuild` script, so nothing needs committing — the # deploy build produces the fresh data by itself. # - The site also live-fetches GitHub data in the browser, so this workflow # mostly improves the first paint and the no-JS / rate-limited fallback. # - `gh workflow run deploy.yml` is used because pushes made with the default # GITHUB_TOKEN do not trigger other workflows. on: schedule: - cron: "*/30 * * * *" # every 30 minutes (may be delayed under load) workflow_dispatch: inputs: force: description: "Rebuild even if the data did not change" type: boolean default: false permissions: contents: read actions: write # needed for `gh workflow run deploy.yml` jobs: refresh: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - name: Regenerate the GitHub snapshot run: npm run fetch:data env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy only when the deployed data is stale id: changed # GITHUB_REPOSITORY (always "owner/name") is used to build the Pages # URL — github.event.repository can be unavailable on schedule events. env: FORCE: ${{ inputs.force }} run: | node - <<'NODE' (async () => { const fs = require("fs"); const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/"); const rootRepo = `${owner}.github.io`.toLowerCase(); const site = repo.toLowerCase() === rootRepo ? `https://${owner}.github.io` : `https://${owner}.github.io/${repo}`; const base = `${site}/data/github.json`; const cur = JSON.parse(fs.readFileSync("public/data/github.json", "utf8")); const live = await (async () => { if (process.env.FORCE === "true") return null; try { const res = await fetch(base, { cache: "no-store" }); if (!res.ok) return null; // not deployed yet → establish the baseline return await res.json(); } catch { return null; // unreachable / first deploy → rebuild } })(); if (live === null) { fs.appendFileSync(process.env.GITHUB_OUTPUT, "changed=true\n"); process.exit(0); } // Compare real data only. generatedAt always differs; and the repo // object's temp_clone_token rotates on every authenticated call // (permissions is token-shaped too) — without stripping those the // JSON never matches and the site rebuilds every 30 minutes. Strip // defensively: the deployed file may predate the fetch script's own // sanitization. for (const obj of [cur, live]) { delete obj.generatedAt; if (obj.repo) { delete obj.repo.temp_clone_token; delete obj.repo.permissions; } if (obj.latestRelease && Array.isArray(obj.latestRelease.assets)) { // Download counts move on every download — compare releases by // their identity (tag, date, notes), not by popularity noise. for (const asset of obj.latestRelease.assets) delete asset.download_count; } } const didChange = JSON.stringify(cur) !== JSON.stringify(live); fs.appendFileSync(process.env.GITHUB_OUTPUT, `changed=${didChange}\n`); })(); NODE - name: Dispatch the deploy if: steps.changed.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh workflow run deploy.yml --ref main -f force_full=true echo "Deployed data was stale — dispatched a rebuild." - name: Nothing to do if: steps.changed.outputs.changed != 'true' run: echo "Deployed data is current — no rebuild needed."