mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-08-14 13:23:37 +00:00
Two caches, one on each side. In the browser, the response is stored in Cache Storage keyed by the server's ETag, so the transfer is paid once per device rather than once per visit. A stored copy opens without the gate: consent was given the first time and reuse costs no network. A redeploy changes the ETag, so the new version replaces the old instead of answering with last week's data, and every older version of that database is dropped so one dataset never occupies the disk twice. When the server cannot be reached at all, any stored version is used, which incidentally makes the site work offline. Storing is best effort — a full disk means downloading again next time, which is no reason to fail the page. The response is cached from a clone while the original is read for the progress bar, rather than buffering the file a second time in the one place where memory is already the binding constraint. In CI, the built databases are cached on the inputs that determine them: data/**, parser/** and datasets.json. Parsing 348 MB of spreadsheets is the slow part of the job, and a web or docs change cannot alter a database, so those pushes restore instead of rebuilding. Exact matches only — no restore-keys, since a near-miss would publish databases built from inputs the commit does not describe.
171 lines
6.7 KiB
YAML
171 lines
6.7 KiB
YAML
name: Deploy to GitHub Pages
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
# Pull requests run the build job only: the deploy job is guarded to main, so
|
|
# a branch can be verified end to end without touching the live site.
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
pages: write
|
|
id-token: write
|
|
|
|
# Keyed by ref, not just "pages". With one shared group a pull-request run and
|
|
# a main deploy compete for the same lane, and cancel-in-progress means the
|
|
# newer one wins: the deploy of #10 was killed 3m22s in by a PR run that
|
|
# started after it, and the site silently stayed on the previous build while
|
|
# every check stayed green. Per ref, a push still cancels its own superseded
|
|
# run, which is the case where cancelling is worth having.
|
|
concurrency:
|
|
group: pages-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
# Every Go module here is cgo-free — grate, excelize, yaml.v3, x/net,
|
|
# x/text and modernc.org/sqlite — so no C toolchain is needed. Set
|
|
# explicitly rather than relying on the default.
|
|
CGO_ENABLED: '0'
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
# The version comes from go.mod rather than a literal here. setup-go sets
|
|
# GOTOOLCHAIN=local, so Go will not fetch the toolchain a module asks for
|
|
# — the one installed has to satisfy it. A literal '1.26' resolves to
|
|
# whatever patch the runner manifest has, which was 1.26.5 against a
|
|
# go.mod requiring 1.26.6, and the build stopped there. All three modules
|
|
# are bumped together, so parser/go.mod speaks for them.
|
|
- uses: actions/setup-go@v7
|
|
with:
|
|
go-version-file: parser/go.mod
|
|
cache-dependency-path: |
|
|
parser/go.sum
|
|
crawler/go.sum
|
|
assembler/go.sum
|
|
|
|
# web/ is the only npm project in the repository; the other stages are Go.
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: '24'
|
|
cache: 'npm'
|
|
cache-dependency-path: web/package-lock.json
|
|
|
|
- name: Install web dependencies
|
|
working-directory: web
|
|
run: npm ci
|
|
|
|
# The reader-fidelity suite compares every real input file against a
|
|
# committed hash oracle, so it is the regression guard for the whole
|
|
# reader. Runs before anything is built.
|
|
- name: Test parser
|
|
run: go -C parser test ./...
|
|
|
|
# The crawler is not part of the build — it only refreshes data/ by hand.
|
|
# It is still tested here so it cannot rot unnoticed, and because its
|
|
# fixture test guards the parser: input filenames decide which row
|
|
# survives a duplicate exam number.
|
|
- name: Test crawler
|
|
run: go -C crawler test ./...
|
|
|
|
# The assembler owns every guard between a built database and the
|
|
# published site, so its tests are the ones that prove a short or missing
|
|
# database cannot ship.
|
|
- name: Test assembler
|
|
run: go -C assembler test ./...
|
|
|
|
# The tests cover the framework-free modules, including the ASCII fold
|
|
# that has to match the Go parser.
|
|
- name: Test web
|
|
working-directory: web
|
|
run: npm test
|
|
|
|
- name: Lint web
|
|
working-directory: web
|
|
run: npm run lint
|
|
|
|
# excelize carries an open advisory, and the 2017 refresh runbook feeds
|
|
# network-downloaded spreadsheets straight into the parser.
|
|
- name: Vulnerability scan
|
|
run: |
|
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
GOVULNCHECK="$(go env GOPATH)/bin/govulncheck"
|
|
for m in parser crawler assembler; do (cd "$m" && "$GOVULNCHECK" ./...); done
|
|
|
|
# Parsing 348 MB of spreadsheets is the slowest part of the job, and its
|
|
# output is a pure function of the inputs in the key: the source files,
|
|
# the parser (schema, transforms, configs) and the registry the row-count
|
|
# and size guards read. Anything else — web code, docs, workflow — cannot
|
|
# change a database, so those pushes restore one instead of rebuilding.
|
|
#
|
|
# Exact matches only, with no restore-keys: a near-miss here would mean
|
|
# publishing a database built from different inputs than the commit says.
|
|
- name: Cache the built databases
|
|
id: db-cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: .build/public/db
|
|
key: db-${{ hashFiles('data/**', 'parser/**', 'datasets.json') }}
|
|
|
|
# The whole pipeline: compile the parser, build and verify each database
|
|
# against its registry row count and size, build the web app, and
|
|
# assemble _site — refusing to continue if a database is short, an
|
|
# artifact looks truncated, or one is missing entirely. On a cache hit
|
|
# the database step is skipped; the artifact it would have produced was
|
|
# verified by the run that built and stored it.
|
|
- name: Build site
|
|
run: |
|
|
if [ "${{ steps.db-cache.outputs.cache-hit }}" = "true" ]; then
|
|
echo "Databases restored from cache; building the site only."
|
|
go -C assembler run ./cmd/assemble site
|
|
else
|
|
go -C assembler run ./cmd/assemble
|
|
fi
|
|
|
|
- uses: actions/upload-pages-artifact@v5
|
|
with:
|
|
path: _site
|
|
|
|
deploy:
|
|
# Deploy only from main. pull_request and workflow_dispatch both run on
|
|
# other branches, and publishing one would put that branch's output on the
|
|
# live site while concurrency cancel-in-progress killed an in-flight good
|
|
# deploy on the way.
|
|
if: github.ref == 'refs/heads/main'
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
environment:
|
|
name: github-pages
|
|
url: ${{ steps.deployment.outputs.page_url }}
|
|
steps:
|
|
- id: deployment
|
|
uses: actions/deploy-pages@v5
|
|
|
|
- uses: actions/checkout@v7
|
|
|
|
# Checks the published file is a database, not an error page or a
|
|
# truncated upload, by reading its first bytes. A range request is used
|
|
# only because it is the cheapest way to see them without pulling 142 MB;
|
|
# the site itself downloads the file whole.
|
|
- name: Verify the published databases are readable
|
|
env:
|
|
PAGE_URL: ${{ steps.deployment.outputs.page_url }}
|
|
run: |
|
|
set -euo pipefail
|
|
for id in $(jq -r '.datasets[].id' datasets.json); do
|
|
url="${PAGE_URL%/}/db/${id}.sqlite3"
|
|
if ! magic=$(curl -sf -r 0-14 -H 'Accept-Encoding: identity;q=1, *;q=0' "$url"); then
|
|
echo "::error::$url is not fetchable"
|
|
exit 1
|
|
fi
|
|
if [ "$magic" != "SQLite format 3" ]; then
|
|
echo "::error::$url does not start with the SQLite header"
|
|
exit 1
|
|
fi
|
|
echo "$url: SQLite format 3"
|
|
done
|