Automation / How it updates
How the data updates
This section is a static site. There is no server and no live feed. A scheduled job refreshes one small data file and the site is rebuilt around it.
Pipeline
From race result to static page
- 01
Schedule
A GitHub Actions cron job starts every Monday at 06:00 UTC, the morning after a typical race weekend. It can also be started by hand from the Actions tab.
- 02
Fetch
scripts/sync-f1-telemetry.js requests the latest race result from the Jolpica API with a timeout and a few retries. If the API is down, the existing file is kept.
- 03
Commit
The script rewrites app/f1-tyres/data/latest-sync.json only if the race changed. The workflow commits that file and pushes only when there is a diff.
- 04
Rebuild
The new commit triggers a static rebuild and redeploy on the host. The JSON is imported at build time, so visitors get plain static HTML.
What is and is not automated
Only the latest race result (winner and fastest lap) is synced. The next Grand Prix nominations, the historical race list and the compound guide live in app/f1-tyres/data/f1Data.js and are edited by hand.
Current snapshot
What this build contains
The values below were read from latest-sync.json when the site was built.
- Data last updated
- 2026-10-02 12:43 UTC
- Race
- Azerbaijan Grand Prix (2026, round 15)
- Circuit
- Baku City Circuit
- Race date
- 2026-09-26
- Winner
- George Russell (Mercedes), 51 laps in 1:38:02.143
- Fastest lap
- George Russell (Mercedes), 1:44.916 on lap 49
- Source
- https://api.jolpi.ca/ergast/f1/
Try it
Query the source yourself
This is a real request from your browser to the same API the sync job uses. It is a check, not a sync: it does not update the site.
Check the API from your browser
Requests https://api.jolpi.ca/ergast/f1/current/last/results.json directly. Nothing is saved and the site data is not changed.
Source
The workflow and the script
Both files are read from the repository at build time, so what you see here is what runs.
.github/workflows/f1-tyre-sync.yml
name: F1 tyre data sync
# Pulls the latest finished race result from the Jolpica F1 API into
# app/f1-tyres/data/latest-sync.json and commits it only when it changed.
# The commit is what triggers the static site to be rebuilt and redeployed.
on:
schedule:
# Mondays at 06:00 UTC, the morning after a typical race weekend.
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: f1-tyre-sync
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Fetch the latest race result
run: node scripts/sync-f1-telemetry.js
- name: Commit the data file if it changed
run: |
if git diff --quiet -- app/f1-tyres/data/latest-sync.json; then
echo "latest-sync.json is unchanged. Nothing to commit."
exit 0
fi
git config user.name "f1-tyre-sync[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add app/f1-tyres/data/latest-sync.json
git commit -m "Sync latest F1 race result"
git pushscripts/sync-f1-telemetry.js
/**
* Sync the latest finished Formula 1 race result into app/f1-tyres/data/latest-sync.json.
*
* Source: Jolpica-F1 (the community-run, open-source Ergast replacement). No API key needed.
* Run by .github/workflows/f1-tyre-sync.yml; safe to run locally with `node scripts/sync-f1-telemetry.js`.
*
* Behaviour:
* - The request has a timeout and a couple of retries.
* - If the API is unreachable, slow, or returns nothing usable, the existing JSON file is left untouched
* and the script exits 0 (a warning is printed; it is a transient condition, not a failure).
* - The file is only rewritten when the race data actually changed, so an unchanged week produces no diff.
* - The script exits 1 only for genuine local errors, such as being unable to read or write the file.
*/
const fs = require('node:fs');
const path = require('node:path');
const ENDPOINT = 'https://api.jolpi.ca/ergast/f1/current/last/results.json';
const SOURCE = 'https://api.jolpi.ca/ergast/f1/';
const OUT_PATH = path.join(__dirname, '..', 'app', 'f1-tyres', 'data', 'latest-sync.json');
const TIMEOUT_MS = 15000;
const ATTEMPTS = 3;
const RETRY_DELAY_MS = 3000;
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** GitHub Actions turns `::warning::` lines into annotations; elsewhere it is just a readable line. */
function warn(message) {
console.warn(`::warning::[F1-SYNC] ${message}`);
}
function fullName(driver) {
return driver ? `${driver.givenName} ${driver.familyName}` : undefined;
}
async function fetchOnce() {
const res = await fetch(ENDPOINT, {
signal: AbortSignal.timeout(TIMEOUT_MS),
headers: { Accept: 'application/json', 'User-Agent': 'tenziki-f1-tyre-sync' },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function fetchWithRetry() {
let lastError;
for (let attempt = 1; attempt <= ATTEMPTS; attempt += 1) {
try {
return await fetchOnce();
} catch (err) {
lastError = err;
console.log(`[F1-SYNC] Attempt ${attempt}/${ATTEMPTS} failed: ${err.message}`);
if (attempt < ATTEMPTS) await wait(RETRY_DELAY_MS);
}
}
throw lastError;
}
/** Reduce the API response to the small structure the site displays. */
function toRaceSummary(json) {
const race = json?.MRData?.RaceTable?.Races?.[0];
if (!race || !Array.isArray(race.Results) || race.Results.length === 0) return null;
const winner = race.Results.find((r) => r.position === '1') || race.Results[0];
const fastest = race.Results.find((r) => r.FastestLap?.rank === '1');
const summary = {
season: race.season,
round: race.round,
name: race.raceName,
circuit: race.Circuit?.circuitName,
date: race.date,
winner: {
driver: fullName(winner.Driver),
team: winner.Constructor?.name,
laps: winner.laps,
time: winner.Time?.time,
},
};
if (fastest) {
summary.fastestLap = {
driver: fullName(fastest.Driver),
team: fastest.Constructor?.name,
time: fastest.FastestLap?.Time?.time,
lap: fastest.FastestLap?.lap,
};
const speed = fastest.FastestLap?.AverageSpeed?.speed;
if (speed) summary.fastestLap.speedKmh = speed;
}
return summary;
}
function readExisting() {
try {
return JSON.parse(fs.readFileSync(OUT_PATH, 'utf8'));
} catch (err) {
if (err.code === 'ENOENT') return null;
// A corrupt file will simply be replaced by fresh data.
console.log(`[F1-SYNC] Existing file could not be parsed (${err.message}); it will be replaced.`);
return null;
}
}
async function main() {
console.log(`[F1-SYNC] Fetching ${ENDPOINT}`);
let json;
try {
json = await fetchWithRetry();
} catch (err) {
warn(`Jolpica API unavailable (${err.message}). Keeping the existing data file unchanged.`);
return;
}
const latest = toRaceSummary(json);
if (!latest) {
warn('The API responded but returned no race results. Keeping the existing data file unchanged.');
return;
}
console.log(`[F1-SYNC] Latest race: ${latest.name} (${latest.season} round ${latest.round}), ${latest.date}`);
console.log(`[F1-SYNC] Winner: ${latest.winner.driver} (${latest.winner.team})`);
if (latest.fastestLap) {
console.log(`[F1-SYNC] Fastest lap: ${latest.fastestLap.driver}, ${latest.fastestLap.time} on lap ${latest.fastestLap.lap}`);
}
const existing = readExisting();
if (existing && JSON.stringify(existing.latestFinishedRace) === JSON.stringify(latest)) {
console.log('[F1-SYNC] No change since the last sync. Nothing written.');
return;
}
const payload = {
lastSyncedAt: new Date().toISOString(),
source: SOURCE,
cost: '$0.00',
latestFinishedRace: latest,
};
fs.writeFileSync(OUT_PATH, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
console.log(`[F1-SYNC] Wrote ${path.relative(process.cwd(), OUT_PATH)}`);
}
main().catch((err) => {
console.error(`[F1-SYNC] Sync failed: ${err.stack || err.message}`);
process.exit(1);
});Setup
Keeping it running
- Keep the repository on GitHub with Actions enabled. The workflow needs the default contents: write permission, which the file already requests.
- Deploy the static export from the same repository using a host with Git-based deploys, so that every push to the main branch triggers a build.
- To test the job without waiting for Monday, open the Actions tab, choose F1 tyre data sync and press Run workflow.
- If your deploy is itself a GitHub Actions workflow, note that pushes made with the default token do not start other workflows. Trigger the deploy from this one (for example with workflow_run) or run the sync with a personal access token.
The Jolpica API is free and needs no key, but it is community run and rate limited, which is why the script keeps the previous file whenever a request fails.
Next