Compare commits

..

16 Commits

Author SHA1 Message Date
clawbot
bcb90e74b4 Run script/lint from script/check (closes #9)
All checks were successful
check / check (push) Successful in 14s
script/check ran only fmt-check then test, so script/lint was never
invoked anywhere in the gate: make check shims to script/check, the
Dockerfile runs make check, script/cibuild builds the Dockerfile, and
the pre-commit hook calls script/check. The script was dead code that
the README advertised as part of the gate.

It now runs test, lint, fmt-check in the canonical order. script/lint
is hugo --minify --printPathWarnings, which reports render-target
collisions that the plain hugo --minify in script/test does not; that
signal was being discarded.

The gate still modifies no tracked files. script/test and script/lint
both write to public/, which is gitignored and was already written by
script/test before this change.

Corrects the two documents that enumerated the old two-step gate: the
README Entrypoints line for script/check, and the Dockerfile header
comment above the RUN make check that executes it.
2026-08-09 10:09:13 +00:00
9959cb5794 Merge pull request '#22: Hash-pin every external reference in deploy.yml (closes #7)'
All checks were successful
check / check (push) Successful in 4s
Build and Deploy to Cloudflare Pages / build (push) Successful in 9s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 18s
2026-08-09 07:03:40 +02:00
54ed6376af Hash-pin every external reference in deploy.yml (closes #7)
All checks were successful
check / check (push) Successful in 6s
deploy.yml was the last file in the repo carrying mutable external references.
Both job container images are now pinned by digest, all three `uses:` by a full
40-hex commit SHA, and the wrangler install by exact version, each with a
version/date comment above the reference.

- build container: klakegg/hugo:ext-alpine (abandoned since 2021, mutable tag)
  replaced by the exact alpine 3.21 digest the Dockerfile already pins, with a
  pre-checkout `apk add --no-cache nodejs git tar` step, `shell: sh` as the job
  default, then script/bootstrap and script/test. One pinned base and one
  dependency list now serve both the check build and the deploy build.
- deploy container: node:20 -> node@sha256:8f693eaa... (node 20.20.2 bookworm).
- actions/checkout: v4 -> 11bd7190... (v4.2.2), the same SHA check.yml pins.
- actions/upload-artifact: -> ff15f030... (v3.2.1).
- actions/download-artifact: -> 9bc31d5c... (v3.0.2).
- wrangler: `npm install -g wrangler` -> `wrangler@4.86.0`.

Also drops the dead feat/initial-site push trigger, reindents to 4-space YAML
to match check.yml, and adds `if: github.ref_name == 'main'` to the deploy job
so it can never publish from a branch.

This is the second attempt. The first passed two adversarial reviews, merged,
and broke the deploy, because deploy.yml triggers only on push to main and so
nobody could execute what they were reviewing. This time the workflow was
temporarily triggered on the branch, with the deploy job guarded off, and
iterated against the commit-status API until the build job ran green for real.
Doing that found two independent breaks that review had not:

1. actions/upload-artifact v4 fails on this Gitea Actions instance -- artifacts
   v4 is a different wire protocol and it is not served here. Two otherwise
   identical branch jobs, one with the v4 upload step and one without, failed
   and passed respectively. The issue asked for the v3 -> v4 bump; the
   artifact actions instead stay on the v3 line, pinned by SHA, at the exact
   commits the mutable @v3 references were already resolving to. Tracked
   separately in issue 20.
2. wrangler 4.120.0 requires node >= 22 and refuses to start on the pinned
   node 20 container. `npm install` only warns about engines, so the install
   step would have passed and the deploy step would have failed. The unpinned
   command this replaces was never installing `latest` either: npm resolves a
   bare name to the newest version whose engines the running node satisfies,
   which on node 20 is 4.86.0. So 4.86.0 is what has actually been deploying
   this site, and that is what is pinned. Tracked separately in issue 21.

The temporary branch trigger and the temporary probe workflow used to bisect
this are removed in this commit; the deploy guard is deliberately kept.

Verified: make check and script/cibuild green; the build job observed green on
the branch under act_runner (commit 73f912c, "Successful in 7s"); a probe job
pair rehearsed the deploy job end to end -- same pinned node image, same pinned
download action, same pinned wrangler, real site tarball extracted -- stopping
at `wrangler pages deploy --help` instead of publishing. The real deploy job
remains unexercised: it needs CLOUDFLARE_API_TOKEN and would publish, so it can
only run on main. The main run must still be watched and the live site
confirmed.
2026-08-09 03:06:21 +00:00
73f912c7ed Pin wrangler to the version that actually runs on the pinned node image
All checks were successful
check / check (push) Successful in 6s
Build and Deploy to Cloudflare Pages / build (push) Successful in 7s
probe / s1-build (push) Successful in 13s
Build and Deploy to Cloudflare Pages / deploy (push) Has been skipped
probe / s2-deploy-dryrun (push) Successful in 10s
Round 3 (07af755) cleared the artifact path and left one failure:

    check / check                        success   8s
    Build and Deploy .../ build          success   8s   <- green
    Build and Deploy .../ deploy         skipped        <- if: guard
    probe / r1-wrangler-only             failure   7s
    probe / r2a-upload-proven            success  12s
    probe / r2b-download-proven          success   2s
    probe / r3a-upload-node20            success   8s
    probe / r3b-download-node20          success   2s

r2a/r2b and r3a/r3b upload and download the real site tarball across the two
job containers, so the artifact round trip is sound. r1 does nothing but
install wrangler and invoke it, and it fails.

Reproduced locally in the pinned node image, which is faster than another CI
round:

    $ docker run --rm node@sha256:8f693eaa... sh -c \
        'npm install -g wrangler@4.120.0; wrangler --version'
    install exit=0            (with EBADENGINE warnings)
    Wrangler requires at least Node.js v22.0.0. You are using v20.20.2.
    version exit=1

npm treats engines as a warning on an explicit version, so the install step
would have passed and the deploy step would have failed -- a second break,
independent of the artifact one, in the same job nobody could run.

The instructive part is what the unpinned command it replaced was doing:

    $ docker run --rm node@sha256:8f693eaa... sh -c \
        'npm install -g wrangler; wrangler --version'
    `-- wrangler@4.86.0
    4.86.0

npm resolves a bare name to the newest version whose engines the running node
satisfies, so `npm install -g wrangler` on node 20 has been installing 4.86.0,
not the 4.120.0 that `latest` points at. Pinning 4.120.0 was therefore not
"pin the version we are already getting", it was an unnoticed major-ish bump
onto a node the container does not have.

So this pins wrangler 4.86.0 (engines: node >= 20.3.0, published 2026-04-28),
which is exactly the version that has been deploying this site, verified to
install and run on the pinned node 20 digest. The node image digest is left
alone. Bumping the container to node 22 to keep 4.120.0 is the alternative,
but that changes the deploy runtime for no benefit this issue asks for.

Round 4 replaces the probe jobs with a single end-to-end rehearsal: the build
job as written, then the deploy job as written with `wrangler pages deploy
--help` in place of the publish call.
2026-08-09 02:59:50 +00:00
07af755d1e Move the artifact pair to the exact commits @v3 was resolving to
Some checks failed
check / check (push) Successful in 8s
Build and Deploy to Cloudflare Pages / build (push) Successful in 8s
probe / r1-wrangler-only (push) Failing after 7s
probe / r2a-upload-proven (push) Successful in 12s
probe / r3a-upload-node20 (push) Successful in 8s
Build and Deploy to Cloudflare Pages / deploy (push) Has been skipped
probe / r2b-download-proven (push) Successful in 2s
probe / r3b-download-node20 (push) Successful in 2s
Round 2 (602fd60) put the build job green:

    check / check                        success   6s
    Build and Deploy .../ build          success  20s   <- green
    Build and Deploy .../ deploy         skipped        <- if: guard
    probe / q1-upload-v3-node16          success   7s
    probe / q2-upload-v3-node20          success  22s
    probe / q3-build-for-roundtrip       success  11s
    probe / q4-deploy-dryrun             failure  43s

Every v3 upload works and the build job is fixed. But q4 -- the deploy-side
rehearsal, which downloads the artifact in the pinned node container and
installs the pinned wrangler, stopping short of the publish call -- failed.
That is a break the deploy job would have hit on main, in a job nobody has
ever been able to run.

q4 bundled two things together, so round 3 splits them:

- r1 runs only the wrangler install and invocation. Worth measuring rather
  than assuming: wrangler 4.120.0 declares engines.node >= 22 and the deploy
  container is node 20, though the pre-issue deploy did run an unpinned
  wrangler on node:20 successfully.
- r2a/r2b run the artifact round trip with no wrangler at all.
- r3a/r3b do the same for the newer node20 artifact builds, so the choice
  between the two pairs is made on measurement.

deploy.yml meanwhile moves to the artifact commits that the mutable `@v3`
references were actually resolving to while this site was deploying, rather
than to the newest thing on the v3 line:

- upload-artifact   -> ff15f030 (v3.2.1)
- download-artifact -> 9bc31d5c (v3.0.2)

That is the conservative reading of what this issue is for: pin what is known
to work, do not take a version bump for free on the way past.
2026-08-09 02:55:51 +00:00
602fd609e7 Pin the artifact actions on v3: v4 does not work on this instance
Some checks failed
check / check (push) Successful in 6s
Build and Deploy to Cloudflare Pages / build (push) Successful in 20s
probe / q1-upload-v3-node16 (push) Successful in 7s
probe / q2-upload-v3-node20 (push) Successful in 22s
probe / q3-build-for-roundtrip (push) Successful in 11s
Build and Deploy to Cloudflare Pages / deploy (push) Has been skipped
probe / q4-deploy-dryrun (push) Failing after 43s
Round 1 of the branch probes reproduced the main failure and localised it.
Observed commit-status output for 2d328e7:

    check / check                        success  10s
    Build and Deploy .../ build          failure  15s   <- reproduced
    Build and Deploy .../ deploy         skipped        <- if: guard working
    probe / p1-bare-alpine-checkout      failure   3s
    probe / p2-alpine-apk-checkout       success   5s
    probe / p3-alpine-apk-build          success  15s
    probe / p4-alpine-apk-upload         failure  11s
    probe / p5-node20alpine-checkout     success   8s
    probe / p6-node20slim-checkout       success  11s

Reading that:

- p1 vs p2: act_runner does not supply node for JavaScript actions, so the
  `apk add --no-cache nodejs git tar` prerequisite step is genuinely required
  and genuinely sufficient. checkout then runs on musl.
- p3: script/bootstrap and script/test complete inside the Actions container
  on the pinned alpine digest. The mandated image replacement was never the
  problem.
- p2 vs p4: the only difference is a trailing upload-artifact v4 step, and it
  is the difference between success and failure.
- p5/p6: musl is not the issue -- checkout runs on both musl and glibc images.

So what broke the deploy was not the image swap that everyone reviewed, it was
the v3 -> v4 artifact bump that nobody questioned. Gitea 1.25.4's artifact
backend and this runner do not serve the v4 protocol; the workflow used v3
before this issue and that is what worked.

The artifact actions therefore move back to the v3 line, still pinned by full
commit SHA, which satisfies the hash-pinning requirement this issue is actually
about. Both are the node20 builds rather than the node16 defaults, so nothing
depends on a node16 runtime:

- upload-artifact  -> c6a3b2bd (v3.2.2-node20)
- download-artifact -> ad191675 (v3.1.0-node20)

Round 2 probes: the two fallback v3 builds in case the node20 ones do not
resolve, plus a producer/consumer pair that rehearses the deploy job -- same
pinned node image, same pinned download action, same pinned wrangler version,
stopping short of `wrangler pages deploy` so it touches nothing external.
2026-08-09 02:50:36 +00:00
2d328e759b Re-apply deploy.yml pinning behind a deploy guard, and probe the failure
Some checks failed
check / check (push) Successful in 10s
Build and Deploy to Cloudflare Pages / build (push) Failing after 15s
probe / p1-bare-alpine-checkout (push) Failing after 3s
probe / p2-alpine-apk-checkout (push) Successful in 5s
probe / p3-alpine-apk-build (push) Successful in 15s
probe / p4-alpine-apk-upload (push) Failing after 11s
probe / p5-node20alpine-checkout (push) Successful in 8s
probe / p6-node20slim-checkout (push) Successful in 11s
Build and Deploy to Cloudflare Pages / deploy (push) Has been skipped
Restores the hash-pinning work reverted in 3d17e22 (originally 3f91a7c and
b157bfd) verbatim -- all six pinned values were independently re-resolved and
confirmed correct twice, so they are reused, not re-derived.

What is different this time is that the path is observable before it reaches
main. The previous attempt broke the deploy because deploy.yml triggers only on
push to main, so every pre-merge check simulated the runner instead of being
it, and two adversarial reviews could not catch what neither could execute.

Three changes on top of the restored work:

- A temporary development-only branch trigger on on.push.branches, so the
  build job actually executes under act_runner. Removed before merge.
- if: github.ref_name == 'main' on the deploy job. Without it, a branch push
  would run wrangler pages deploy against the real Cloudflare project with the
  real token on every iteration. This guard is permanent: it is one line and it
  makes any future branch trigger, deliberate or accidental, unable to reach
  Cloudflare.
- A temporary .gitea/workflows/probe.yml, also deleted before merge. The
  Actions jobs and logs API is not readable by this account; the commit-status
  API is, and it reports one entry per job. So the diagnosis is encoded as job
  topology rather than log output: six jobs, each isolating one hypothesis
  about the 22s failure (bare alpine vs apk prerequisites, checkout vs site
  build vs artifact upload, musl node vs glibc node), each surfacing as its own
  status context so a single push tests them all in parallel.

make check is green. No pinned value is touched.
2026-08-09 02:46:29 +00:00
3d17e22385 Revert "Merge pull request '#17: Hash-pin every external reference in deploy.yml (closes #7)'"
All checks were successful
check / check (push) Successful in 4s
Build and Deploy to Cloudflare Pages / build (push) Successful in 5s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 19s
This reverts commit 74c28c1d71, reversing
changes made to 7cad989724.
2026-08-09 02:37:18 +00:00
74c28c1d71 Merge pull request '#17: Hash-pin every external reference in deploy.yml (closes #7)'
Some checks failed
check / check (push) Successful in 3s
Build and Deploy to Cloudflare Pages / build (push) Failing after 22s
Build and Deploy to Cloudflare Pages / deploy (push) Has been skipped
2026-08-09 04:28:21 +02:00
b157bfd52c Install runner prerequisites in the pinned build container (closes #7)
All checks were successful
check / check (push) Successful in 10s
Replacing klakegg/hugo:ext-alpine with the Dockerfile's pinned alpine
digest satisfied the pinning requirement but dropped the runtime the
Actions runner itself depends on, which would have broken the deploy:

- act_runner executes JavaScript actions with `node` inside the job
  container and does not inject one. Stock alpine has no node, so
  actions/checkout - the job's first step - would fail with
  "node: not found", and script/bootstrap (which installs node) is step
  2 and never runs. The build job fails, deploy is skipped for
  `needs: build`, and the site stops publishing.
- Steps default to `bash`, which stock alpine does not ship either.

Fixes, both scoped to keeping the mandated image replacement runnable:

- A pre-checkout inline `run:` step (`apk add --no-cache nodejs git tar`)
  installs what the runner needs before the first `uses:` step. An
  inline run needs only a shell, so it works on the bare image. git is
  there for checkout's `submodules: recursive`; without it checkout
  degrades to a tarball download that cannot do submodules.
- `defaults.run.shell: sh` on the build job, so the shell is stated
  rather than left to a bash-to-sh fallback.

No pinned value is touched. The apk packages resolve at run time and are
not hash-pinned; that gap is repo-wide (script/bootstrap has it too) and
is tracked in #19.

Also moves each version/date comment to sit directly above the pinned
line rather than above the step's `- name:`, matching check.yml, and
dates the actions/checkout pin 2026-02-28 as check.yml already does for
the same SHA.

Verified by running the build job's step sequence inside the pinned
alpine digest: bare, `node` and `bash` are absent and the pinned
checkout bundle dies with "node: not found"; after the new apk step,
node 22.23.2, git 2.47.3 and GNU tar 1.35 are present, that same
checkout bundle runs under node and gets as far as "GITHUB_WORKSPACE not
defined", and script/bootstrap, script/test and the tar step all
complete. make check and script/cibuild (with the build cache pruned, so
nothing was CACHED) are green.
2026-08-09 02:15:44 +00:00
3f91a7c273 Hash-pin every external reference in deploy.yml (closes #7)
All checks were successful
check / check (push) Successful in 7s
deploy.yml was the last file in the repo carrying mutable external
references. Every image is now pinned by digest and every action by a
full 40-hex commit SHA, each with a version/date comment on the line
above. All values were resolved from upstream and verified to resolve.

- build container: klakegg/hugo:ext-alpine (abandoned since 2021,
  mutable tag) replaced by the exact alpine 3.21 digest the Dockerfile
  already pins, with script/bootstrap to install hugo and script/test
  to build. One pinned base and one dependency list now serve both the
  check build and the deploy build.
- deploy container: node:20 -> node@sha256:8f693eaa... (node 20.20.2,
  bookworm).
- actions/checkout: v4 -> 11bd7190... (v4.2.2), the same SHA check.yml
  pins, so the two workflows agree.
- actions/upload-artifact: v3 -> ea165f8d... (v4.6.2); v3 is deprecated.
- actions/download-artifact: v3 -> d3f86a10... (v4.3.0); v3 is
  deprecated.
- npm install -g wrangler -> wrangler@4.120.0, so the deploy no longer
  executes whatever the wrangler tag happens to point at.

Also drops the dead feat/initial-site push trigger (that branch is fully
merged into main) and reindents the file to 4-space YAML to match
check.yml and .editorconfig.

The two jobs are deliberately left separate so a deploy regression can
be attributed unambiguously.

Verified: make check and script/cibuild both green; the workflow parses
as YAML with the expected job/step structure. The Cloudflare Pages
deploy path itself cannot be exercised from a branch (it runs only on
push to main and needs CLOUDFLARE_API_TOKEN), so the deploy run on main
must be watched after merge.
2026-08-09 01:49:21 +00:00
7cad989724 Add scripts-to-rule-them-all scaffold (closes #4)
All checks were successful
check / check (push) Successful in 4s
Build and Deploy to Cloudflare Pages / build (push) Successful in 5s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 18s
Adopt the Scripts to Rule Them All standard for this Hugo site:

- script/ POSIX-sh entrypoints (bootstrap, setup, projectname, test,
  lint, fmt, fmt-check, check, docker, cibuild, precommit,
  install-precommit). The correctness check (test/lint) is a clean
  `hugo --minify` production build; fmt/fmt-check run prettier over the
  repo's own top-level markdown only, leaving content/ untouched.
- Makefile targets reduced to thin shims that call script/NAME, plus a
  convenience serve target for `hugo server`.
- Dockerfile on a sha256-pinned alpine base that installs deps via
  script/bootstrap and runs `make check`, so the image build fails on
  any formatting or Hugo build error; .dockerignore added.
- .gitea/workflows/check.yml runs script/cibuild on push.
- README Entrypoints section documenting the scripts.
2026-07-25 18:22:52 +07:00
612d15587b Add standard Workflow section to TODO.md
All checks were successful
Build and Deploy to Cloudflare Pages / build (push) Successful in 5s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 19s
2026-07-06 21:06:42 +02:00
f1cab64bd4 Merge branch 'TODO'
All checks were successful
Build and Deploy to Cloudflare Pages / build (push) Successful in 7s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 22s
2026-07-06 20:51:15 +02:00
20c133ee61 Add TODO.md 2026-07-06 20:35:49 +02:00
f993d36f0c add contact link in footer
All checks were successful
Build and Deploy to Cloudflare Pages / build (push) Successful in 5s
Build and Deploy to Cloudflare Pages / deploy (push) Successful in 21s
2026-02-14 05:51:08 +01:00
21 changed files with 509 additions and 56 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
.git
public
resources
.hugo_build.lock

View File

@@ -0,0 +1,9 @@
name: check
on: [push]
jobs:
check:
runs-on: ubuntu-latest
steps:
# actions/checkout v4.2.2, 2026-02-28
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: script/cibuild

View File

@@ -3,28 +3,64 @@ name: Build and Deploy to Cloudflare Pages
on:
push:
branches:
- feat/initial-site
- main
jobs:
build:
runs-on: ubuntu-latest
container:
image: klakegg/hugo:ext-alpine
# Same digest the Dockerfile pins: one pinned base image and the
# same dependency list (script/bootstrap) for both the check build
# and the deploy build. The one extra thing this job needs on top
# of the Dockerfile is the Actions runner's own prerequisites --
# see the first step.
# alpine 3.21, 2026-02-28
image: alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
defaults:
run:
# The default step shell is bash; this image has only busybox
# sh, so say so explicitly rather than rely on a fallback.
shell: sh
steps:
# This image is bare busybox+musl. act_runner executes JavaScript
# actions (checkout, upload-artifact) with `node` *inside* the job
# container and does not inject one, so node has to exist before
# the first `uses:` step -- script/bootstrap runs too late. git is
# needed for checkout's `submodules: recursive` (without it
# checkout degrades to a tarball download that cannot do
# submodules). An inline `run:` needs only a shell, so this step
# works on the bare image. These apk packages resolve at run time
# and are not hash-pinned; that gap is repo-wide (script/bootstrap
# has it too) and is tracked in #19.
- name: Install runner prerequisites
run: apk add --no-cache nodejs git tar
- name: Checkout
uses: actions/checkout@v4
# actions/checkout v4.2.2, 2026-02-28
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
submodules: recursive
- name: Install build dependencies
run: script/bootstrap
- name: Build site
run: hugo --minify
run: script/test
- name: Archive site
run: tar -czf site.tar.gz public
# v3, not v4: artifacts v4 is a different wire protocol and this
# Gitea Actions instance does not serve it. That is what broke the
# deploy in run 25 -- measured by running two otherwise identical
# jobs on a branch, one ending in upload-artifact v4 (failed) and
# one without that step (passed). Tracked in #20. This SHA is the
# exact commit the mutable `@v3` used to resolve to, i.e. the code
# that was already deploying this site, now pinned rather than
# floating.
- name: Upload artifact
uses: actions/upload-artifact@v3
# actions/upload-artifact v3.2.1, 2026-08-09
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: site
path: site.tar.gz
@@ -32,19 +68,41 @@ jobs:
deploy:
runs-on: ubuntu-latest
needs: build
# Publishing guard. This job spends CLOUDFLARE_API_TOKEN and creates a
# real Cloudflare Pages deployment, so it must never run off main --
# not even if a branch is added to the push trigger above, deliberately
# or by accident. Costs one line; the build job stays exercisable from
# a branch without this job touching anything external.
if: github.ref_name == 'main'
container:
image: node:20
# node 20.20.2-bookworm, 2026-08-09
image: node@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5
steps:
# Must match the upload-artifact major above -- v4 artifacts and
# v3 artifacts are different protocols and do not interoperate.
# Like the upload above, this is the exact commit `@v3` used to
# resolve to.
- name: Download artifact
uses: actions/download-artifact@v3
# actions/download-artifact v3.0.2, 2026-08-09
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
with:
name: site
- name: Extract site
run: tar -xzf site.tar.gz
# 4.86.0, not the 4.120.0 that `latest` points at: wrangler
# 4.120.0 requires node >= 22 and refuses to start on this
# container's node 20. Note that the unpinned `npm install -g
# wrangler` this replaces was never installing `latest` either --
# npm picks the newest version whose engines the running node
# satisfies, which on node 20 is exactly 4.86.0. So this pins the
# version that has actually been deploying this site, rather than
# silently changing it. Moving the container to node 22 so the
# wrangler pin can advance is tracked in #21.
- name: Install Wrangler
run: npm install -g wrangler
# wrangler 4.86.0, 2026-08-09
run: npm install -g wrangler@4.86.0
- name: Deploy to Cloudflare Pages
run: wrangler pages deploy public --project-name=lora-vegas --branch=${{ github.ref_name }}

View File

@@ -1,13 +0,0 @@
name: Security Recon
on:
push:
branches:
- security-audit
jobs:
recon:
runs-on: ubuntu-latest
steps:
- name: Placeholder
run: echo "Security audit complete. See issue #3."

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
# Hugo static-site build image. The build runs `make check` (a clean
# `hugo --minify` production build, the `--printPathWarnings` lint
# build, then the read-only prettier docs check), so the image build
# fails on any formatting or Hugo build error. This is what CI
# (script/cibuild) runs on every push.
# alpine 3.21, 2026-02-28
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
WORKDIR /src
# Install build dependencies first so the layer caches until the
# scripts change (script/bootstrap installs git, make, hugo, node/npm).
COPY script/ script/
RUN script/bootstrap
COPY . .
# Run all checks - build fails if any check fails.
RUN make check

31
Makefile Normal file
View File

@@ -0,0 +1,31 @@
.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks serve
bootstrap:
@script/bootstrap
setup:
@script/setup
test:
@script/test
lint:
@script/lint
fmt:
@script/fmt
fmt-check:
@script/fmt-check
check:
@script/check
docker:
@script/docker
hooks:
@script/install-precommit
serve:
@hugo server

View File

@@ -4,7 +4,8 @@ Las Vegas Meshtastic and LoRa community website.
## About
This site provides information about the Las Vegas mesh networking community, including:
This site provides information about the Las Vegas mesh networking community,
including:
- Mesh channel configurations
- Community coordination (Discord, Signal)
@@ -13,11 +14,13 @@ This site provides information about the Las Vegas mesh networking community, in
## Contributing
To contribute to this site, contact **sneak@sneak.berlin** for git repository access.
To contribute to this site, contact **sneak@sneak.berlin** for git repository
access.
## Technical Details
This is a static site built with Hugo. The site is deployed automatically via GitHub Actions.
This is a static site built with Hugo. The site is deployed automatically via
GitHub Actions.
### Local Development
@@ -35,6 +38,33 @@ hugo
Output will be in the `public/` directory.
## Entrypoints
This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard: normalized scripts in `script/` are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call them. We
provide:
- `script/bootstrap` — install all build dependencies (git, make, hugo,
node/npm) idempotently
- `script/setup` — prepare a fresh clone: run `script/bootstrap` and install the
git pre-commit hook
- `script/test` — the correctness check: a clean `hugo --minify` production
build
- `script/lint` — a clean build that surfaces broken links and path collisions
- `script/fmt` — format the repo's own top-level markdown docs with prettier
- `script/fmt-check` — check that formatting (read-only)
- `script/check` — run `script/test`, `script/lint`, then `script/fmt-check`;
modifies no tracked files
- `script/docker` — build the Docker image tagged with the project name
- `script/cibuild` — the CI build (`docker build .`); the Dockerfile runs
`make check`
- `script/install-precommit` — install the git pre-commit hook that runs
`script/check`
A convenience `make serve` target runs `hugo server` for local preview.
## License
Content is provided as-is for community use.

78
TODO.md Normal file
View File

@@ -0,0 +1,78 @@
# Workflow
- branch (from `main`)
- do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (`TODO.md` changes in the same commit as the work)
- merge to `main` if the branch is not protected, otherwise open a PR
- push
# Status
pre-1.0
No git tags. The site is live and now has the scripts-to-rule-them-all scaffold
(`Makefile`, `script/`, `Dockerfile`, `check.yml`); still missing `LICENSE` and
policy files. Every external reference in the repo is now pinned by
cryptographic hash (or, for the wrangler CLI install, an exact version).
# Next Step
Add the remaining policy scaffold: `LICENSE`, `REPO_POLICIES.md`,
`.editorconfig`, and prettier config files (`.prettierrc`, `.prettierignore`).
Update `README.md` accordingly.
# Completed Steps
- 2026-08-09: made `script/check` run `script/lint` (closes #9). It previously
ran only `fmt-check` then `test`, so `script/lint` executed nowhere — not in
`make check`, not in the pre-commit hook, and not in CI, even though the
`Dockerfile` runs `make check` and `script/cibuild` builds it. It now runs
`test`, `lint`, `fmt-check` in the canonical order, so the
`hugo --printPathWarnings` render-target-collision signal is no longer
discarded. `README.md`'s Entrypoints line was corrected to match
- 2026-08-09: hash-pinned every external reference in
`.gitea/workflows/deploy.yml` (closes #7): both job container images are
pinned by digest, all three `uses:` are pinned by 40-hex commit SHA, and the
wrangler install is pinned to an exact version. The abandoned
`klakegg/hugo:ext-alpine` image is gone: the build job now runs on the same
pinned `alpine` digest the `Dockerfile` uses, with a pre-checkout
`apk add nodejs git tar` step (the Actions runner needs `node` inside the job
container to execute JavaScript actions), an explicit `shell: sh` default,
then `script/bootstrap` and `script/test`. The `deploy` job is guarded with
`if: github.ref_name == 'main'` so it can never publish from a branch. Also
dropped the dead `feat/initial-site` push trigger and reindented the file to
4-space YAML to match `check.yml`. This is the second attempt; the first broke
the deploy and was reverted, so this one was verified by temporarily
triggering the workflow on the PR branch and iterating until the `build` job
ran green for real
- 2026-07-25: added the scripts-to-rule-them-all scaffold (closes #4): `script/`
entrypoints, `Makefile` shims, a Hugo `Dockerfile` (sha256-pinned alpine) plus
`.dockerignore` that runs `make check`, `.gitea/workflows/check.yml` running
`script/cibuild`, and a README Entrypoints section. `test`/`lint` are a clean
`hugo --minify` build; `fmt`/`fmt-check` run prettier over the repo's own
top-level markdown only
- 2026-02-10: design pass: minimal light theme with inline CSS, grey wells for
mesh channels and signal groups, horizontal overflow fix, body width tuning,
map link update
- 2026-02-10: added README and footer contribute link
- 2026-02-10: added Gitea workflow that builds the site and deploys to
Cloudflare Pages
- 2026-02-08: initial Hugo static site for lora.vegas
# Future Steps
- Move the artifact actions to v4 once this Gitea Actions instance serves the v4
artifact protocol; they are pinned on the deprecated v3 line because v4 fails
here (#20)
- Move the deploy container to a pinned node 22 so the wrangler pin can advance
past 4.86.0 (#21)
- Rework README.md into the standard sections: Description, Getting Started,
Rationale, Design, TODO, License, Author (currently About, Contributing,
Technical Details, License)
- Replace the "content is provided as-is" README note with the text of the
committed LICENSE
- Expand .gitignore beyond Hugo outputs (OS and editor files)
- Verify the Cloudflare Pages deploy still works after the workflow changes
- Keep mesh channel and signal group listings current

76
script/bootstrap Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop
# this Hugo site, idempotently. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes NOTHING is present (not git,
# make, hugo, or node). Installs hugo (the site build) and node/npm
# (prettier, used to format the repo's own markdown docs). Every install
# is guarded by a check so already-installed tools are skipped.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PKGMGR=""
SUDO=""
detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0
if command -v nix-env >/dev/null 2>&1; then
PKGMGR="nix"
elif command -v apt-get >/dev/null 2>&1; then
PKGMGR="apt"
elif command -v brew >/dev/null 2>&1; then
PKGMGR="brew"
elif command -v apk >/dev/null 2>&1; then
PKGMGR="apk"
else
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
exit 1
fi
if [ "$PKGMGR" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
if [ "$(id -u)" != "0" ]; then
SUDO="sudo"
fi
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
fi
}
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
pkg_install() {
detect_pkgmgr
case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;;
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;;
esac
}
missing() {
! command -v "$1" >/dev/null 2>&1
}
main() {
cd "$ROOT"
# Base tooling.
if missing git; then pkg_install git git git git; fi
if missing make; then pkg_install gnumake make make make; fi
# The theme is vendored in-repo, but initialise submodules if any
# are ever added so a fresh clone is buildable.
if [ -f .gitmodules ]; then
git submodule update --init --recursive
fi
# Site build.
if missing hugo; then pkg_install hugo hugo hugo hugo; fi
# node/npm provide prettier (via npx) for formatting the docs.
if missing node; then pkg_install nodejs nodejs node nodejs; fi
if missing npx; then pkg_install nodejs npm npm npm; fi
echo "bootstrap complete"
}
main "$@"

16
script/check Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# script/check: run all checks. Our own extension to
# scripts-to-rule-them-all. Must not modify any tracked files. Runs the
# canonical order: the clean production build, then the lint build that
# reports path warnings, then the read-only formatting check.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/test"
"$SCRIPT_DIR/lint"
"$SCRIPT_DIR/fmt-check"
}
main "$@"

14
script/cibuild Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs `make check`,
# so a successful build implies all checks pass. The Gitea workflow
# runs this on push.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
docker build .
}
main "$@"

14
script/docker Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# script/docker: build the Docker image tagged with the project name.
# The tag comes from script/projectname.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
docker build -t "$("$SCRIPT_DIR/projectname")" .
}
main "$@"

18
script/fmt Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
# script/fmt: format the repo's own top-level markdown docs (README.md,
# TODO.md, ...) with prettier, using our standard settings. Scope is
# deliberately limited to top-level docs: site content under content/
# is left untouched so rendered output cannot change.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PRETTIER_VERSION="3.4.2"
main() {
cd "$ROOT"
npx --yes "prettier@${PRETTIER_VERSION}" --write \
'*.md' --tab-width 4 --prose-wrap always
}
main "$@"

17
script/fmt-check Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
# script/fmt-check: check the formatting of the repo's own top-level
# markdown docs (read-only). Same scope as script/fmt, but fails
# instead of writing.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PRETTIER_VERSION="3.4.2"
main() {
cd "$ROOT"
npx --yes "prettier@${PRETTIER_VERSION}" --check \
'*.md' --tab-width 4 --prose-wrap always
}
main "$@"

15
script/install-precommit Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# script/install-precommit: install the git pre-commit hook that runs
# script/precommit. Our own extension to scripts-to-rule-them-all.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "pre-commit hook installed: runs script/precommit"
}
main "$@"

15
script/lint Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# script/lint: this Hugo site has no dedicated linter, so the lint gate
# is a clean build that surfaces broken internal links and template
# path problems. It is a real check: `hugo` fails on build errors, and
# --printPathWarnings reports render-target collisions.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
hugo --minify --printPathWarnings
}
main "$@"

12
script/precommit Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/precommit: run by the git pre-commit hook; fails the commit if
# checks fail. Our own extension to scripts-to-rule-them-all.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/check"
}
main "$@"

12
script/projectname Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/projectname: output the name of this project. Our own
# extension to scripts-to-rule-them-all. Other scripts that need the
# name (e.g. script/docker) call this, so they can stay identical
# across all repos.
set -eu
main() {
echo "lora.vegas"
}
main "$@"

13
script/setup Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# script/setup: set up the repo for development after a fresh clone:
# installs dependencies (script/bootstrap) and the git pre-commit hook.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/bootstrap"
"$SCRIPT_DIR/install-precommit"
}
main "$@"

14
script/test Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# script/test: the correctness check for this static site is a clean
# production build. `hugo --minify` exits non-zero on any template,
# content, or config error, so a green build is a passing test.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
hugo --minify
}
main "$@"

View File

@@ -12,6 +12,7 @@
<body>
{{ block "main" . }}{{ end }}
<footer>
<p>this site is a project by <a href="https://sneak.berlin">@sneak</a>.</p>
<p>lora.vegas &mdash; Las Vegas Meshtastic community <a href="https://git.eeqj.de/sneak/lora.vegas" class="contribute-link">[Contribute]</a></p>
</footer>
</body>