Files
gacha-event-tracker/.github/workflows/ci.yml
T
Lucas Winther f07607911b
CI / Typecheck, test, feed (push) Successful in 20s
CI / Build site (push) Successful in 11s
CI / Container image (push) Successful in 40s
docs(workflows): clarify that GitHub only builds and serves while Gitea refreshes sources
Document the forge responsibilities across AGENTS.md, README.md, ARCHITECTURE.md, INGESTION.md, and the CI workflow headers: GitHub Actions only builds and serves (Pages deployment) without fetching data sources, while Gitea Actions exclusively runs the scheduled refresh and publishes the container image.
2026-09-18 10:14:37 +02:00

163 lines
5.9 KiB
YAML

name: CI
# GitHub Actions CI/CD
#
# GitHub only needs to build and serve (deploying to GitHub Pages), not refresh
# data sources. Scheduled fetching and snapshot refresh run exclusively on
# Gitea Actions (.gitea/workflows/refresh.yml).
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: Gitea's refresh workflow 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`);
for (const s of feed.sources) {
console.log(` ${s.sourceId.padEnd(24)} ${String(s.eventCount).padStart(3)}`);
}
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");
}
// Per source, not just in total: nine healthy sources hide a tenth
// that has gone to zero, and the total stays comfortably over the
// floor while one game shows an empty calendar.
//
// Which zero it is decides whether this build should fail, and
// the rule lives in shared/feed.ts rather than here. It was
// inline, and a test did pin it — by grepping this file for the
// string. That proved the check existed, never that it was right,
// and it was not: it read eventCount, which is counted after
// expiry, so a page whose events had all simply ended reddened the
// build. Behaviour belongs where behaviour can be exercised.
const { brokenSources, quietSources, staleSources } = await import("./src/shared/feed.ts");
for (const s of staleSources(feed.sources)) {
console.log(
` note: ${s.sourceId} parsed ${s.parsedCount} events, all of them ended — stale page, not a fault`,
);
}
for (const s of quietSources(feed.sources)) {
console.log(
` note: ${s.sourceId} parsed nothing and the page says so itself — between versions, not a fault`,
);
}
const broken = brokenSources(feed.sources);
if (broken.length > 0) {
throw new Error(
`sources parsing to nothing: ${broken.map((s) => s.sourceId).join(", ")}`,
);
}
'
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>/ by default, so the app is built with a
# matching base href unless overridden by vars.BASE_PATH (e.g. for
# custom domains). Built at the domain root it would 404 on every asset.
env:
BASE_PATH: ${{ vars.BASE_PATH || format('/{0}/', github.event.repository.name) }}
run: bun run build
- uses: actions/upload-artifact@v4
with:
name: site
path: public/
retention-days: 7
pages:
name: Deploy to Pages
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
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