doxa-maps/.forgejo/workflows/build.yml

120 lines
5.3 KiB
YAML

# build.yml — the SINGLE-BUILDER CI gate for the doxa-maps bundler.
#
# WHY THIS EXISTS (IDEA-FORGEJO-CICD-BUILD-LOCK-MULTIWRITER):
# Many agents edit + commit source to this repo in PARALLEL. That is safe. What is
# NOT safe is many agents each running `bun run build` into the shared, committed
# web-root (public/): concurrent builds clobber each other's output and race the
# `git add public/` that follows. internal/build-lock.sh makes that STRUCTURALLY
# impossible on ONE machine (an flock mutex); this workflow does the same thing at
# CLUSTER scope — it makes CI the ONE-AND-ONLY builder, and serializes it.
#
# THE CONTRACT for a swarm agent:
# edit files -> git commit -> git push (DONE. never run `bun run build`.)
# The push lands here. This job — and ONLY this job — runs the authoritative build
# and commits the regenerated public/ back. See the handoff doc:
# _qa-cycle/03-deployment-cycle/delegation/HANDOFF-forgejo-cicd-buildgate.md
#
# NOTE ON PATH: Forgejo reads .forgejo/workflows/ (and .gitea/workflows/), NOT
# .github/workflows/. The existing .github/workflows/pages.yml is a GitHub-Pages
# workflow and never runs here; this is its Forgejo graduation.
name: build-gate
# Trigger on every push to the source branches. workflow_dispatch lets the Driver
# run it by hand from the Forgejo UI.
on:
push:
branches:
- main
- doxa-maps-plugin-studio
# Source-only edits trigger a build. Skip pushes that touch NOTHING but the
# build output or CI itself — the build-output commit this job makes would
# otherwise re-trigger the job in a loop.
paths-ignore:
- 'public/**'
- '.forgejo/**'
workflow_dispatch:
# THE HEART OF THE GATE. All builds share ONE concurrency group, so Forgejo runs
# at most one at a time. A push that arrives while a build is running is QUEUED,
# not cancelled and not failed — cancel-in-progress is FALSE on purpose: we want
# every push's source to be built in order, never dropped. This is the flock
# nonblock/queue behaviour of build-lock.sh, expressed as a scheduler primitive.
concurrency:
group: doxa-maps-build
cancel-in-progress: false
jobs:
build:
# Requires a registered Forgejo Actions runner labelled `ubuntu-latest`
# (docker or host). If none is registered the job stays queued — see the
# handoff doc for the ONE Driver action that registers it.
runs-on: ubuntu-latest
# The job needs to read the repo AND commit the regenerated public/ back, so
# it needs write. The runner's automatic token carries the repo's scope.
permissions:
contents: write
steps:
- name: Checkout the pushed source
uses: actions/checkout@v4
with:
# Full history so the build-output commit is a normal fast-forward.
fetch-depth: 0
- name: Install bun
uses: https://github.com/oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies (locked)
run: bun install --frozen-lockfile
# THE AUTHORITATIVE BUILD. build:unlocked is the real chain (clean -> token
# -> per-bundle vite -> tree -> drop/parse checks -> verify -> gallery). We
# call :unlocked, NOT `bun run build`, because CI IS the single builder now —
# the local flock mutex would only get in its own way here. The MAPBOX_TOKEN
# secret becomes public/api/maps/token.json (gitignored) so the credential
# reaches the built site without ever entering git history.
- name: Build (authoritative, single-writer)
env:
MAPBOX_TOKEN: ${{ secrets.MAPBOX_TOKEN }}
VITE_MAPBOX_TOKEN: ${{ secrets.MAPBOX_TOKEN }}
BUILD_LOCK_OWNER: forgejo-ci
run: bun run build:unlocked
# Guards graduated from pages.yml: never publish a broken or credential-
# leaking web-root. A failure here fails the build — nothing is committed.
- name: Assert the token endpoint exists
run: |
test -s public/api/maps/token.json || {
echo "::error::public/api/maps/token.json is missing/empty — set the MAPBOX_TOKEN secret, or every basemap renders blank."
exit 1
}
- name: Assert no local credential is being published
run: |
if [ -e public/token.local.js ]; then
echo "::error::public/token.local.js is present in the web-root and must not be committed."
exit 1
fi
# Commit the regenerated public/ back to the branch. This is the ONE writer
# of build output in the whole system. If the build produced no change,
# commit nothing (idempotent). The token endpoint is gitignored, so it is
# never staged. The [skip ci]-equivalent is handled by the paths-ignore
# above: a public/-only commit does not re-trigger this job.
- name: Publish the built web-root
run: |
git config user.name "forgejo-ci"
git config user.email "ci@doxa.life"
git add public/
if git diff --cached --quiet; then
echo "No build-output change — nothing to publish."
exit 0
fi
git commit -m "build: regenerate public/ from ${GITHUB_SHA:-source}"
# Push back to the branch that triggered us. The runner token authorizes
# this; contents:write is granted above.
git push origin "HEAD:${GITHUB_REF_NAME}"