build: Check Go and Node before building and move Docker to Alpine 3.24 #22

Merged
linus merged 4 commits from ash/toolchain-preflight-21 into develop 2026-10-18 15:04:08 +00:00
Collaborator

Closes #21

Why

Justin's forge rule (2026-10-10): any toolchain that can only have one version installed at a time gets a preflight that stops the build before it does anything and prints found / required / fix. helix-proxy's make build ran npm and Go with no check, had no .nvmrc, and shipped on alpine:3.20 (EOL 2026-04-01) with a floating golang:1.27-alpine builder. Standard: forge-standards/forge-toolchain.md.

What changed

  • scripts/check-toolchain.sh: the forge-standard preflight, byte-identical to the forge-standards copy and bucket#21. It checks Go against go.mod (toolchain line wins), Node against .nvmrc, and that go.mod's go/toolchain lines match HEAD.
  • Makefile: new check-toolchain target. build, ui, ui-build and run depend on it. ui-build used to depend on clean, which deletes the binary, ui/dist and data/. clean now runs as ui-build's first recipe line, after the preflight, so a failed check leaves the tree alone even under make -j. test*, fmt, clean and docker don't depend on it.
  • go.mod: toolchain go1.27.2 under go 1.27.0.
  • .nvmrc: 24.
  • CI (ci.yaml, both jobs) and release.yaml: a Check toolchain step right after Setup Node. Action versions are unchanged.
  • Dockerfile: builder golang:1.27.2-alpine3.24, runtime alpine:3.24. node:24-alpine is unchanged.
  • README.md (Development section): what the preflight checks and how to fix a mismatch.

Proof (jumpbox, my own runs at b0a3f1b)

Right toolchain (go1.27.2 via GOTOOLCHAIN=auto, Node v24.21.0 / npm 11.19.0, GNU Make 4.4.1):

  • make check-toolchain, make ui, make ui-build, make build, make -j4 build: all rc=0. The binary built, and make run started the proxy (stopped by a 6s timeout). git status stayed clean.

Wrong toolchain, make build: each case stopped at check-toolchain (rc=2). Binary mtime and ui/dist were unchanged and git status stayed clean:

toolchain: Node does not match .nvmrc        found: 26.1.0 (shim)   required: 24   fix: nvm install 24 && nvm use 24
toolchain: Node does not match .nvmrc        found: none            required: 24
toolchain: Go version does not match go.mod  found: go1.26.5 (shim) required: go1.27.2  fix: install Go 1.27.2 from https://go.dev/dl/ (or unset GOTOOLCHAIN=local so Go fetches it)
toolchain: Go version does not match go.mod  found: go1.27.1 (GOTOOLCHAIN=local) required: go1.27.2
toolchain: go.mod toolchain pins changed since the last commit  found: go 1.27.0 toolchain go1.27.1  required: go 1.27.0 toolchain go1.27.2  fix: git checkout -- go.mod, then fix the toolchain above
toolchain: go.mod toolchain pins changed since the last commit  found: go 1.27.1 toolchain go1.27.2  required: go 1.27.0 toolchain go1.27.2

make -j4 build, make run and make ui with Node 26 also stopped at the preflight.

Tests: go test -count=1 ./... passed 233 tests and subtests (0 fail, 0 skip) across 8 packages. go test -race ./... passed. gofmt -l . is clean. go mod verify OK. No .go files are touched.

Docker: there's no docker on the jumpbox, so I couldn't run docker build. I checked that golang:1.27.2-alpine3.24 and alpine:3.24 exist on Docker Hub (amd64 and arm64).

Risk / rollback

Build-only. A dev machine without Node 24 or Go 1.27.2 now gets a clear stop instead of a build. With GOTOOLCHAIN=auto (the default), Go fetches 1.27.2 itself. Rollback: git revert the merge.

Waterfall

Step 1: OpenCode on the jumpbox (lemonade/gemma-26b-censored, one run of about 3 minutes, no stalls) wrote every file change. I committed it and ran all the proofs above myself.

Base is develop, per CONTRIBUTING.md ("Open a pull request against develop"). develop and master are at the same tip, 4054ea2.

Closes #21 ## Why Justin's forge rule (2026-10-10): any toolchain that can only have one version installed at a time gets a preflight that stops the build before it does anything and prints found / required / fix. helix-proxy's `make build` ran npm and Go with no check, had no `.nvmrc`, and shipped on `alpine:3.20` (EOL 2026-04-01) with a floating `golang:1.27-alpine` builder. Standard: forge-standards/forge-toolchain.md. ## What changed - `scripts/check-toolchain.sh`: the forge-standard preflight, byte-identical to the forge-standards copy and bucket#21. It checks Go against go.mod (toolchain line wins), Node against `.nvmrc`, and that go.mod's `go`/`toolchain` lines match HEAD. - `Makefile`: new `check-toolchain` target. `build`, `ui`, `ui-build` and `run` depend on it. `ui-build` used to depend on `clean`, which deletes the binary, `ui/dist` and `data/`. `clean` now runs as ui-build's first recipe line, after the preflight, so a failed check leaves the tree alone even under `make -j`. `test*`, `fmt`, `clean` and `docker` don't depend on it. - `go.mod`: `toolchain go1.27.2` under `go 1.27.0`. - `.nvmrc`: `24`. - CI (`ci.yaml`, both jobs) and `release.yaml`: a `Check toolchain` step right after Setup Node. Action versions are unchanged. - `Dockerfile`: builder `golang:1.27.2-alpine3.24`, runtime `alpine:3.24`. `node:24-alpine` is unchanged. - `README.md` (Development section): what the preflight checks and how to fix a mismatch. ## Proof (jumpbox, my own runs at b0a3f1b) Right toolchain (go1.27.2 via GOTOOLCHAIN=auto, Node v24.21.0 / npm 11.19.0, GNU Make 4.4.1): - `make check-toolchain`, `make ui`, `make ui-build`, `make build`, `make -j4 build`: all rc=0. The binary built, and `make run` started the proxy (stopped by a 6s timeout). `git status` stayed clean. Wrong toolchain, `make build`: each case stopped at `check-toolchain` (rc=2). Binary mtime and `ui/dist` were unchanged and `git status` stayed clean: ``` toolchain: Node does not match .nvmrc found: 26.1.0 (shim) required: 24 fix: nvm install 24 && nvm use 24 toolchain: Node does not match .nvmrc found: none required: 24 toolchain: Go version does not match go.mod found: go1.26.5 (shim) required: go1.27.2 fix: install Go 1.27.2 from https://go.dev/dl/ (or unset GOTOOLCHAIN=local so Go fetches it) toolchain: Go version does not match go.mod found: go1.27.1 (GOTOOLCHAIN=local) required: go1.27.2 toolchain: go.mod toolchain pins changed since the last commit found: go 1.27.0 toolchain go1.27.1 required: go 1.27.0 toolchain go1.27.2 fix: git checkout -- go.mod, then fix the toolchain above toolchain: go.mod toolchain pins changed since the last commit found: go 1.27.1 toolchain go1.27.2 required: go 1.27.0 toolchain go1.27.2 ``` `make -j4 build`, `make run` and `make ui` with Node 26 also stopped at the preflight. Tests: `go test -count=1 ./...` passed 233 tests and subtests (0 fail, 0 skip) across 8 packages. `go test -race ./...` passed. `gofmt -l .` is clean. `go mod verify` OK. No `.go` files are touched. Docker: there's no docker on the jumpbox, so I couldn't run `docker build`. I checked that `golang:1.27.2-alpine3.24` and `alpine:3.24` exist on Docker Hub (amd64 and arm64). ## Risk / rollback Build-only. A dev machine without Node 24 or Go 1.27.2 now gets a clear stop instead of a build. With GOTOOLCHAIN=auto (the default), Go fetches 1.27.2 itself. Rollback: `git revert` the merge. ## Waterfall Step 1: OpenCode on the jumpbox (`lemonade/gemma-26b-censored`, one run of about 3 minutes, no stalls) wrote every file change. I committed it and ran all the proofs above myself. Base is `develop`, per CONTRIBUTING.md ("Open a pull request against `develop`"). develop and master are at the same tip, 4054ea2.
build: Check Go and Node before building and move Docker to Alpine 3.24
All checks were successful
Format / gofmt (push) Successful in 18s
CI / Build (push) Successful in 36s
Format / gofmt (pull_request) Successful in 21s
CI / Build (pull_request) Successful in 33s
CI / Go Tests (push) Successful in 1m18s
CI / Go Tests (pull_request) Successful in 1m19s
b0a3f1be7d
Add scripts/check-toolchain.sh (forge standard) and make build, ui,
ui-build and run depend on a check-toolchain target that runs before
anything else, including the clean that ui-build does. Pin toolchain
go1.27.2 in go.mod, add .nvmrc 24, run the preflight in CI and release
after the setup steps, move the Docker builder to golang:1.27.2-alpine3.24
and the runtime to alpine:3.24, and document the check in the README.

Closes #21
ash requested review from eva 2026-10-17 14:58:27 +00:00
eva left a comment

The code is good. Base is the current develop tip (4054ea2), the preflight matches the bucket#21 copy, and the Makefile reorder fixes the old problem where ui-build ran clean before anything was checked. The go.mod toolchain pin and the .nvmrc line up with the CI setup-go/setup-node steps, and the README note is enough.

Two things before this merges:

  1. CI has to go green on b0a3f1b. Go Tests and Build are still pending.
  2. This changes both Docker base images (builder golang:1.27.2-alpine3.24, runtime alpine:3.24), and CI doesn't build the image. Only the release job does. Same as #20, I'm getting Jerry to run a throwaway docker build --no-cache --pull on the lab with nothing pushed. I'll post the result here.

Linus: hold the merge until both are done and I've posted the docker pass. This touches CI config and the release image, so it goes the normal way and isn't a low-risk auto-merge. Ash: nothing for you to do unless CI or the docker build fails.

The code is good. Base is the current develop tip (4054ea2), the preflight matches the bucket#21 copy, and the Makefile reorder fixes the old problem where `ui-build` ran `clean` before anything was checked. The go.mod toolchain pin and the `.nvmrc` line up with the CI setup-go/setup-node steps, and the README note is enough. Two things before this merges: 1. CI has to go green on b0a3f1b. Go Tests and Build are still pending. 2. This changes both Docker base images (builder `golang:1.27.2-alpine3.24`, runtime `alpine:3.24`), and CI doesn't build the image. Only the release job does. Same as #20, I'm getting Jerry to run a throwaway `docker build --no-cache --pull` on the lab with nothing pushed. I'll post the result here. Linus: hold the merge until both are done and I've posted the docker pass. This touches CI config and the release image, so it goes the normal way and isn't a low-risk auto-merge. Ash: nothing for you to do unless CI or the docker build fails.
Contributor

Throwaway build and smoke of b0a3f1b on dockge.lab.rrd: passes.

  • sudo docker compose build --no-cache --pull succeeded in 10s. Bases: node:24-alpine@sha256:44fce696f38b…, golang:1.27.2-alpine3.24@sha256:f92b6ef800e4…, alpine:3.24@sha256:294b683cb724… (runtime image is Alpine 3.24.2).
  • Started on 18080/18443/19081, healthcheck went healthy in about 10s. GET /api returned 200 {"status":"OK"}, admin UI / returned 200, a bad login returned 401, and the proxy port answered 200.
  • Torn down: container, network and image removed, ports free, other stacks untouched. Nothing pushed or deployed.

One note for the smoke only: the bind-mounted data/ has to be owned by uid 1000, or the app exits with mkdir /app/data/certs: permission denied. That is not a regression from this PR.

Throwaway build and smoke of b0a3f1b on dockge.lab.rrd: passes. - `sudo docker compose build --no-cache --pull` succeeded in 10s. Bases: node:24-alpine@sha256:44fce696f38b…, golang:1.27.2-alpine3.24@sha256:f92b6ef800e4…, alpine:3.24@sha256:294b683cb724… (runtime image is Alpine 3.24.2). - Started on 18080/18443/19081, healthcheck went healthy in about 10s. `GET /api` returned 200 `{"status":"OK"}`, admin UI `/` returned 200, a bad login returned 401, and the proxy port answered 200. - Torn down: container, network and image removed, ports free, other stacks untouched. Nothing pushed or deployed. One note for the smoke only: the bind-mounted `data/` has to be owned by uid 1000, or the app exits with `mkdir /app/data/certs: permission denied`. That is not a regression from this PR.
Owner

Thanks @jerry. That clears the hold from my review 617: the image builds on the new Alpine 3.24 bases and the smoke passed. CI is green on b0a3f1b, so this is GO from me. @linus, it's yours to merge.

The uid 1000 note on data/ predates this PR, so it isn't blocking. It's worth a line in the compose docs some time.

Thanks @jerry. That clears the hold from my review 617: the image builds on the new Alpine 3.24 bases and the smoke passed. CI is green on b0a3f1b, so this is GO from me. @linus, it's yours to merge. The uid 1000 note on `data/` predates this PR, so it isn't blocking. It's worth a line in the compose docs some time.
Add workflows to .forgejo/workflows
All checks were successful
Format / gofmt (push) Successful in 38s
CI / Build (push) Successful in 1m19s
Format / gofmt (pull_request) Successful in 35s
CI / Build (pull_request) Successful in 1m23s
CI / Go Tests (pull_request) Successful in 2m4s
CI / Go Tests (push) Successful in 2m5s
53f75d00d9
The previous commit removed them from .gitea/workflows but did not include
the .forgejo copies.
eva approved these changes 2026-10-18 14:55:36 +00:00
Dismissed
eva left a comment

APPROVE on head 53f75d00d9.

Delta since b0a3f1be7d (my review 617, which Jerry's docker smoke test passed on) is two commits and nothing else:

  • .gitea/workflows/{ci,fmt,release}.yaml removed, .forgejo/workflows/{ci,fmt,release}.yaml added.
  • Content diff old vs new: ci.yaml and fmt.yaml identical; release.yaml only changes gitea.ref_name, gitea.server_url, gitea.repository, gitea.actor to github.*.
  • No Dockerfile, Makefile, go.mod, script or code changes, so the docker smoke result carries over because the Dockerfile is unchanged.

CI is green on this exact head (gofmt, Build, Go Tests, push and pull_request). PR is open, mergeable, and not behind the base tip 48849fc078.

APPROVE on head 53f75d00d972ad6bfe091347a30614088b03f5d5. Delta since b0a3f1be7d (my review 617, which Jerry's docker smoke test passed on) is two commits and nothing else: - `.gitea/workflows/{ci,fmt,release}.yaml` removed, `.forgejo/workflows/{ci,fmt,release}.yaml` added. - Content diff old vs new: ci.yaml and fmt.yaml identical; release.yaml only changes `gitea.ref_name`, `gitea.server_url`, `gitea.repository`, `gitea.actor` to `github.*`. - No Dockerfile, Makefile, go.mod, script or code changes, so the docker smoke result carries over because the Dockerfile is unchanged. CI is green on this exact head (gofmt, Build, Go Tests, push and pull_request). PR is open, mergeable, and not behind the base tip 48849fc07833.
eva requested changes 2026-10-18 14:55:49 +00:00
Dismissed
eva left a comment

REQUEST_CHANGES on head 53f75d00d9. This supersedes my APPROVE (review 642) from a minute ago, which was wrong: I said the head was not behind base, and it is.

develop is now 48849fc078, and the head is 4 commits behind it: #23 and #24 already moved the workflows to .forgejo/workflows, and #25 merged master into develop. The two workflow commits on this branch (58d2214aca, 53f75d00d9) now duplicate what develop has. The delta itself was clean (workflow move plus gitea.* to github.* only, no Dockerfile or code change, so the docker smoke on b0a3f1b still carries over), but a head behind base can't merge, however green its old CI was.

Please rebase this branch onto current develop and drop the two workflow commits, leaving only b0a3f1b's content, on this same branch. Check the Dockerfile and Makefile are unchanged by the rebase, then push. I will re-review the new head; if the Dockerfile is byte-identical to b0a3f1b's, the smoke result carries over.

REQUEST_CHANGES on head 53f75d00d972ad6bfe091347a30614088b03f5d5. This supersedes my APPROVE (review 642) from a minute ago, which was wrong: I said the head was not behind base, and it is. develop is now 48849fc07833, and the head is 4 commits behind it: #23 and #24 already moved the workflows to .forgejo/workflows, and #25 merged master into develop. The two workflow commits on this branch (58d2214aca, 53f75d00d9) now duplicate what develop has. The delta itself was clean (workflow move plus gitea.* to github.* only, no Dockerfile or code change, so the docker smoke on b0a3f1b still carries over), but a head behind base can't merge, however green its old CI was. Please rebase this branch onto current develop and drop the two workflow commits, leaving only b0a3f1b's content, on this same branch. Check the Dockerfile and Makefile are unchanged by the rebase, then push. I will re-review the new head; if the Dockerfile is byte-identical to b0a3f1b's, the smoke result carries over.
Merge remote-tracking branch 'origin/develop' into ash/toolchain-preflight-21
All checks were successful
Format / gofmt (push) Successful in 19s
CI / Build (push) Successful in 43s
CI / Build (pull_request) Successful in 40s
Format / gofmt (pull_request) Successful in 19s
CI / Go Tests (pull_request) Successful in 1m22s
CI / Go Tests (push) Successful in 1m28s
f2c5db6d3d
eva requested changes 2026-10-18 15:02:02 +00:00
Dismissed
eva left a comment

REQUEST_CHANGES on head f2c5db6d3d. The code is right now, but the branch history still needs the cleanup I asked for in my last review.

Merging develop in fixed the behind-base problem, and the net diff against develop 48849fc is exactly the toolchain change: the preflight script, the check steps in the two .forgejo workflows, .nvmrc, the go.mod toolchain line, the Makefile order and the Alpine 3.24 bases. CI is green on it. The Dockerfile hasn't changed since the head Jerry's docker build passed on.

The problem is what lands in develop with it. Our merge lever only does merge commits, so 58d2214 and 53f75d0 would go in as a second copy of the workflow move that develop already got from #23 to #25, plus a "Merge remote-tracking branch" commit. Anyone reading the log later sees the move twice and has to work out which one counted.

The quickest way to get one clean commit, since the merge commit already has the tree we want:

git fetch origin
git checkout -B ash/toolchain-preflight-21 origin/develop
git checkout f2c5db6 -- .
git commit -C b0a3f1b
git diff f2c5db6 --stat   # should print nothing
git push --force-with-lease origin ash/toolchain-preflight-21

That leaves a single "build: Check Go and Node before building and move Docker to Alpine 3.24" commit on top of develop with the same content. Wait for CI to go green on it and I'll approve.

REQUEST_CHANGES on head f2c5db6d3d. The code is right now, but the branch history still needs the cleanup I asked for in my last review. Merging develop in fixed the behind-base problem, and the net diff against develop 48849fc is exactly the toolchain change: the preflight script, the check steps in the two .forgejo workflows, .nvmrc, the go.mod toolchain line, the Makefile order and the Alpine 3.24 bases. CI is green on it. The Dockerfile hasn't changed since the head Jerry's docker build passed on. The problem is what lands in develop with it. Our merge lever only does merge commits, so 58d2214 and 53f75d0 would go in as a second copy of the workflow move that develop already got from #23 to #25, plus a "Merge remote-tracking branch" commit. Anyone reading the log later sees the move twice and has to work out which one counted. The quickest way to get one clean commit, since the merge commit already has the tree we want: ```sh git fetch origin git checkout -B ash/toolchain-preflight-21 origin/develop git checkout f2c5db6 -- . git commit -C b0a3f1b git diff f2c5db6 --stat # should print nothing git push --force-with-lease origin ash/toolchain-preflight-21 ``` That leaves a single "build: Check Go and Node before building and move Docker to Alpine 3.24" commit on top of develop with the same content. Wait for CI to go green on it and I'll approve.
eva approved these changes 2026-10-18 15:03:27 +00:00
eva left a comment

APPROVE on head f2c5db6d3d. This supersedes my REQUEST_CHANGES 649; I was wrong to ask for a history cleanup. A merge commit in the branch history is fine, and no force push is needed.

Checked on this exact head:

  • Net diff against develop 48849fc is 8 files, +87/-9: the two .forgejo workflows (a "Check toolchain" step each), .nvmrc, Dockerfile, Makefile, README, go.mod toolchain line and scripts/check-toolchain.sh. There is no .gitea directory and no duplicate workflow move in the net diff.
  • Between b0a3f1b (the head Jerry's docker build passed on) and this head, the only changes are the workflow move that develop already carries. The Dockerfile, Makefile, go.mod, .nvmrc and script are untouched, so Jerry's docker smoke CLEAR carries over.
  • CI on f2c5db6d3d is green: Format / gofmt (push and pull_request), CI / Build (push and pull_request), CI / Go Tests (pull_request).
  • The head is not behind develop.

Linus: clear to merge at this head.

APPROVE on head f2c5db6d3d05d97a2dc7161408180d479d2a6009. This supersedes my REQUEST_CHANGES 649; I was wrong to ask for a history cleanup. A merge commit in the branch history is fine, and no force push is needed. Checked on this exact head: - Net diff against develop 48849fc is 8 files, +87/-9: the two .forgejo workflows (a "Check toolchain" step each), .nvmrc, Dockerfile, Makefile, README, go.mod toolchain line and scripts/check-toolchain.sh. There is no .gitea directory and no duplicate workflow move in the net diff. - Between b0a3f1b (the head Jerry's docker build passed on) and this head, the only changes are the workflow move that develop already carries. The Dockerfile, Makefile, go.mod, .nvmrc and script are untouched, so Jerry's docker smoke CLEAR carries over. - CI on f2c5db6d3d05 is green: Format / gofmt (push and pull_request), CI / Build (push and pull_request), CI / Go Tests (pull_request). - The head is not behind develop. Linus: clear to merge at this head.
linus merged commit 708b445d4e into develop 2026-10-18 15:04:08 +00:00
linus deleted branch ash/toolchain-preflight-21 2026-10-18 15:04:08 +00:00
Sign in to join this conversation.
No reviewers
eva
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
s1d3sw1ped/helix-proxy!22
No description provided.