Commit fab92e702d

fab92e702d2db99b85572b8e505ce7b194de5130

parent: 811ffd16f5

Unsigned

cmc <hello@cleberg.net> · 2026-08-22 03:34 UTC

Build the site in CI on pull requests and pushes to main

The site had no CI: a broken org file or an orgo regression was caught by
looking at cleberg.net after deploying to it.

Builds with ENV=prod rather than the development path, for two reasons. The
development path runs 'ruff check --fix' and 'ruff format', which rewrite files
in place, and CI should verify the tree rather than edit it. Production is also
the build that actually ships, including the onion image-URL rewrite that runs
only when ENV=prod.

DEPLOY is left unset so main() builds and stops. Because that is an assumption
rather than a guarantee, rsync is shadowed with a failing stub for the duration
of the job: if the deploy path is ever reached from CI, the job fails instead of
quietly reaching for the production host.

orgo is pinned and cached on that pin, since compiling it dominates the run and
nothing bumps a 'cargo install' automatically.

Layout: unified · split

.github/workflows/build.yml added +77
@@ -0,0 +1,77 @@
1name: Build
2
3on:
4 pull_request:
5 push:
6 branches: [main]
7
8permissions:
9 contents: read
10
11env:
12 # orgo is installed from crates.io, so nothing bumps this automatically —
13 # dependabot does not see `cargo install` in a workflow. Raise it by hand when
14 # a new orgo lands, which is also how you find out orgo broke the site.
15 ORGO_VERSION: "0.22.0"
16
17jobs:
18 build:
19 runs-on: ubuntu-latest
20 steps:
21 - uses: actions/checkout@v7
22
23 # Compiling orgo takes minutes; the binary only changes when the pinned
24 # version does, so key the cache on it.
25 - name: Cache orgo
26 id: cache-orgo
27 uses: actions/cache@v6
28 with:
29 path: ~/.cargo/bin/orgo
30 key: orgo-${{ env.ORGO_VERSION }}-${{ runner.os }}
31
32 - name: Install orgo
33 if: steps.cache-orgo.outputs.cache-hit != 'true'
34 run: |
35 cargo --version
36 cargo install orgo --version "$ORGO_VERSION" --locked
37
38 - name: Show versions
39 run: |
40 orgo --version
41 python3 --version
42
43 # build.py deploys with rsync when DEPLOY=true and ENV=prod. CI passes
44 # neither, but "we did not set the variable" is an assumption, not a
45 # guarantee. Shadowing rsync with a failing stub turns it into one: if the
46 # deploy path is ever reached from CI, the job fails loudly instead of
47 # quietly reaching for a production host.
48 - name: Block the deploy path
49 run: |
50 mkdir -p "$RUNNER_TEMP/bin"
51 cat > "$RUNNER_TEMP/bin/rsync" <<'EOF'
52 #!/usr/bin/env bash
53 echo "::error::rsync was invoked from CI — the deploy path must never run here" >&2
54 exit 1
55 EOF
56 chmod +x "$RUNNER_TEMP/bin/rsync"
57 echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
58
59 # ENV=prod is deliberate. The development path in build.py runs
60 # `ruff check --fix` and `ruff format`, which rewrite files in place —
61 # CI should verify the tree, not edit it. Production is also the build
62 # that actually ships, including the onion image-URL rewrite. DEPLOY is
63 # left unset, so main() builds and stops.
64 - name: Build the site
65 env:
66 ENV: prod
67 BUILD: "true"
68 run: python3 build.py
69
70 - name: Check the build produced pages
71 run: |
72 pages=$(find .build -name '*.html' | wc -l)
73 echo "built $pages html pages"
74 if [ "$pages" -lt 100 ]; then
75 echo "::error::only $pages pages built; expected the full site"
76 exit 1
77 fi