data-ready #748
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: Sync data from the engine | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # The map's liveness, without a cross-repo credential. | |
| # | |
| # The scored dataset is produced by the engine repository. It also *pushes* the | |
| # data here with a token (PUBLISH_PAT). That push is a single point of failure: | |
| # when the token broke, every scheduled scan kept producing good data and none | |
| # of it arrived — the map sat frozen for days while both repositories looked | |
| # busy and the engine's run log said "failure" into the void. | |
| # | |
| # This is the pull side of the same hop and needs no cross-repo credential: it | |
| # reads the engine's published files over plain HTTPS and commits them here with | |
| # this workflow's own GITHUB_TOKEN, which cannot expire and cannot be | |
| # mis-scoped. Commits go through the Contents API, so they are GitHub-signed | |
| # (Verified) and pass the signed-commit ruleset. | |
| # | |
| # Both paths may run; whichever arrives first wins and the other skips on | |
| # identical content. If the engine's files are not publicly readable, this exits | |
| # clean and the push path remains the fallback. | |
| # | |
| # Two staggered ticks: GitHub drops scheduled runs under load, and a single | |
| # nightly-style cron can silently miss. The second tick costs seconds when the | |
| # data is already current (a conditional GET and an exit). | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| on: | |
| # The engine fires this the moment a scan is relayed, so the data is signed | |
| # and current within seconds rather than at the next cron. The crons stay as | |
| # the backstop: a dropped dispatch must cost freshness, not the map. | |
| repository_dispatch: | |
| types: [data-ready] | |
| schedule: | |
| - cron: "37 */3 * * *" # ~20 min after the engine's :17 scan lands | |
| - cron: "7 */6 * * *" # backstop tick, offset — survives a dropped run | |
| workflow_dispatch: | |
| permissions: | |
| contents: write # commits the engine's output into this repo | |
| actions: write # dispatches the redeploy (gh workflow run pages.yml) | |
| concurrency: | |
| group: radar-sync | |
| cancel-in-progress: false | |
| jobs: | |
| sync: | |
| name: Pull the engine's published data | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Pull published data from the engine (no PAT) | |
| env: | |
| ENGINE_REPO: AxonOS-BCI/axonos-radar-core | |
| GITHUB_TOKEN: ${{ github.token }} | |
| ENGINE_READ_TOKEN: ${{ secrets.ENGINE_READ_TOKEN }} # optional; restores the pull path for a private engine (fine-grained, engine repo only, Contents: Read) | |
| run: python3 scripts/sync_engine_data.py | |
| - name: Contract gate on whatever landed | |
| # The pull path validates BEFORE committing; the push path (the | |
| # engine's PAT) does not go through this repo's checks at all, and | |
| # every data commit carries [skip ci]. This step is the net under | |
| # both: whatever is in the working tree right now — pulled a moment | |
| # ago or pushed by the engine before this run — must satisfy the | |
| # public contract, or this run goes red. | |
| run: | | |
| pip install -r requirements-ci.txt --quiet | |
| python3 scripts/validate_payload.py data/radar.json | |
| python3 - <<'GATE' | |
| import json, sys | |
| import jsonschema | |
| jsonschema.validate(json.load(open("data/radar.json")), | |
| json.load(open("data/radar.schema.json"))) | |
| sys.path.insert(0, "scripts") | |
| from sync_engine_data import feed_is_well_formed | |
| ok, why = feed_is_well_formed(open("feed.xml", encoding="utf-8").read()) | |
| if not ok: | |
| raise SystemExit(f"::error::feed.xml is unusable: {why}") | |
| print("contract gate: schema + feed well-formed") | |
| GATE | |
| - name: Freshness readout | |
| # The contract gate proves the payload is well-formed. It cannot prove | |
| # the payload is recent: a frozen engine keeps serving last night's | |
| # valid JSON, this workflow finds nothing to commit, exits 0, and the | |
| # badge stays green while the public map quietly stops moving. That is | |
| # the failure this repository already paid for once. Read the age out | |
| # loud on every run so a stalled upstream is visible in the run log | |
| # rather than only in an issue the monitor may not open for hours. | |
| run: | | |
| python3 - <<'FRESH' | |
| import json, os | |
| from datetime import datetime, timezone | |
| limit = float(os.environ.get("RADAR_MAX_AGE_HOURS", "7")) | |
| at = json.load(open("data/last_run.json"))["at"] | |
| dt = datetime.fromisoformat(at.replace("Z", "+00:00")) | |
| age = (datetime.now(timezone.utc) - dt).total_seconds() / 3600 | |
| line = f"last engine publish {at} — {age:.1f}h old (limit {limit:.0f}h)" | |
| print(line) | |
| with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as fh: | |
| fh.write(f"- {'STALE' if age > limit else 'fresh'}: {line}\n") | |
| if age > limit: | |
| print(f"::warning title=Engine data is stale::{line}. The sync " | |
| "path is healthy; the upstream engine has not published. " | |
| "Check the engine repository's Actions quota and schedule.") | |
| FRESH | |
| - name: Redeploy the site with whatever arrived | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Data commits carry [skip ci], which suppresses the push trigger by | |
| # design; without this the site would wait for the pages cron. | |
| if gh workflow run pages.yml --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| echo "✓ redeploy dispatched" | |
| else | |
| echo "· could not dispatch pages.yml — it still runs on its own cron" | |
| fi |