Skip to content

data-ready

data-ready #748

Workflow file for this run

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