`lastConfirmedAt` lives in gitignored bookkeeping that only refresh.yml restored, so the workflow that actually builds and deploys never saw it: every source reported its last *content change* as its last success, and the UI flagged anything whose bytes had not moved in two days as stale — which is most wiki pages most of the time. ci.yml now restores the same cache read-only before building the feed. The refresh push was a bare `git push`, so a human push landing in between made it non-fast-forward: the job failed and threw away pages it had just fetched, while the bookkeeping had already been saved, so those sources would not be re-asked for six hours. Rebase and retry instead — never force. The cache save key used run_id, which is stable across re-runs, so a re-run saved nothing and the run after it restored stale bookkeeping. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
178 lines
5.7 KiB
YAML
178 lines
5.7 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
# A new push supersedes the one before it on the same ref.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Least privilege by default; jobs opt into more where they need it.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
check:
|
|
name: Typecheck, test, feed
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: oven-sh/setup-bun@v2
|
|
with:
|
|
bun-version: "1.3"
|
|
|
|
- run: bun install --frozen-lockfile
|
|
|
|
# `lastConfirmedAt` — the "we asked the wiki and it said this is still
|
|
# current" half of the freshness badge — lives in the gitignored
|
|
# snapshots/*.state.json, so without this the feed built here has only
|
|
# `contentChangedAt` to go on and every source reads as stale two days
|
|
# after its bytes last moved, which for a wiki page is most of the time.
|
|
# Restore-only: refresh.yml owns writing this cache, and a miss just
|
|
# returns the pre-existing fallback behaviour.
|
|
- name: Restore refresh bookkeeping
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: snapshots/*.state.json
|
|
key: refresh-state-
|
|
restore-keys: refresh-state-
|
|
|
|
- name: Typecheck
|
|
run: bun run typecheck
|
|
|
|
- name: Test
|
|
# Offline by design: no fixture is re-fetched, so a red run always means
|
|
# the code changed, never that a source was unreachable.
|
|
run: bun test
|
|
|
|
- name: Build feed
|
|
run: bun run build:feed
|
|
|
|
- name: Feed sanity
|
|
# A source that quietly stops yielding events is the failure mode a
|
|
# parser-only pipeline is most prone to, and nothing else surfaces it.
|
|
run: |
|
|
bun -e '
|
|
const feed = await Bun.file("public/data/events.v1.json").json();
|
|
const games = new Set(feed.events.map((e) => e.game));
|
|
console.log(`${feed.events.length} events across ${games.size} games`);
|
|
if (feed.events.length < 20) {
|
|
throw new Error(`feed collapsed to ${feed.events.length} events`);
|
|
}
|
|
if (feed.events.some((e) => !e.startsAt)) {
|
|
throw new Error("events without a start date");
|
|
}
|
|
'
|
|
|
|
build:
|
|
name: Build site
|
|
runs-on: ubuntu-latest
|
|
needs: check
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: oven-sh/setup-bun@v2
|
|
with:
|
|
bun-version: "1.3"
|
|
- run: bun install --frozen-lockfile
|
|
|
|
# This is the job whose output is deployed, so this is the one that must
|
|
# see the refresh bookkeeping; see the same step in `check`.
|
|
- name: Restore refresh bookkeeping
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: snapshots/*.state.json
|
|
key: refresh-state-
|
|
restore-keys: refresh-state-
|
|
|
|
- name: Build
|
|
# Pages serves from /<repo>/, so the app is built with a matching base
|
|
# href. Built at the domain root it would 404 on every asset.
|
|
env:
|
|
BASE_PATH: /${{ github.event.repository.name }}/
|
|
run: bun run build
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: site
|
|
path: public/
|
|
retention-days: 7
|
|
|
|
image:
|
|
name: Container image
|
|
runs-on: ubuntu-latest
|
|
needs: check
|
|
# Pushing an image for every pull request fills the registry; do it where
|
|
# the artefact could actually be deployed.
|
|
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Lowercase image name
|
|
id: img
|
|
# GHCR rejects any uppercase in a repository name, and
|
|
# github.repository preserves the owner's casing verbatim.
|
|
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
|
|
|
|
- uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
push: true
|
|
tags: |
|
|
${{ steps.img.outputs.name }}:latest
|
|
${{ steps.img.outputs.name }}:${{ github.sha }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
|
|
# Deploying to Pages needs two one-time steps that CI cannot do for itself:
|
|
#
|
|
# 1. Settings → Pages → Source: "GitHub Actions".
|
|
# The default GITHUB_TOKEN cannot do this. Creating a Pages site needs
|
|
# `administration: write`, which is not a permission a workflow can grant
|
|
# GITHUB_TOKEN, so `configure-pages` with enablement: true fails with
|
|
# "Resource not accessible by integration".
|
|
# 2. Settings → Secrets and variables → Actions → Variables:
|
|
# set DEPLOY_PAGES to "true".
|
|
#
|
|
# Gated on that variable so the pipeline stays green for anyone who does not
|
|
# want Pages, rather than failing on every push forever.
|
|
pages:
|
|
name: Deploy to Pages
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
if: >-
|
|
github.ref == 'refs/heads/main' &&
|
|
github.event_name != 'pull_request' &&
|
|
vars.DEPLOY_PAGES == 'true'
|
|
permissions:
|
|
pages: write
|
|
id-token: write
|
|
environment:
|
|
name: github-pages
|
|
url: ${{ steps.deploy.outputs.page_url }}
|
|
steps:
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
name: site
|
|
path: public
|
|
- uses: actions/configure-pages@v5
|
|
- uses: actions/upload-pages-artifact@v3
|
|
with:
|
|
path: public
|
|
- id: deploy
|
|
uses: actions/deploy-pages@v4
|