Unify the gate: root make check must cover the backend, and CI must route through script/ #16

Open
opened 2026-08-09 03:37:45 +02:00 by clawbot · 1 comment
Collaborator

Problem

The repo has two disconnected build systems, and the root gate does not actually gate the whole repo. Verified on main at fbfe1df.

1. Root make check ignores the backend entirely

Makefile check shims to script/check, which runs the frontend yarn build plus prettier --check. It never touches backend/. Confirmed by running it: root make check passes while exercising zero Go code.

This means the "main must always pass make check, no exceptions" policy is being satisfied vacuously — the Go backend could be entirely broken and the root gate would stay green. Anyone (or any pre-commit hook) running make check at the root gets false assurance.

2. The backend is not on scripts-to-rule-them-all

REPO_POLICIES.md requires that "the implementation of each Makefile target lives in an executable script in script/ ... and the Makefile targets are thin shims that call them." backend/Makefile is a full standalone Makefile with inline recipes (go build, go test, golangci-lint run, gofmt) and no script/ indirection. It also violates the "always use Makefile targets instead of invoking the underlying tools directly" rule at the layer below.

3. Two competing pre-commit hook installers, one clobbers the other

  • script/install-precommit (shimmed by root make hooks) installs a hook running script/precommit.
  • backend/Makefile's hooks target writes .git/hooks/pre-commit with the literal body cd backend && make check.

Both target the same single .git/hooks/pre-commit path. Whichever ran last wins, so the developer silently ends up gating on only one half of the repo. There must be exactly one hook installer, and the hook must gate the whole repo.

4. CI invokes docker build directly instead of through script/

.gitea/workflows/check.yml:

- run: script/cibuild
- run: docker build -f Dockerfile.backend .

The second step is a raw tool invocation, bypassing the script/ layer that is supposed to be the single source of truth for how the repo builds. Policy: the workflow runs script/cibuild; script/cibuild runs the docker build(s).

Definition of done

  • Root make check fails if either the frontend or the backend is broken. Demonstrate this: deliberately break a Go file, confirm root make check fails, revert.
  • The backend's test/lint/fmt-check implementations live in script/ entrypoints, and any retained backend/Makefile targets are thin shims. Whether that means extending the root script/* files to cover both halves or adding backend/script/* is an implementation choice — pick one, apply it consistently, and document the choice in the PR description.
  • script/cibuild builds both images (frontend Dockerfile and Dockerfile.backend). .gitea/workflows/check.yml contains exactly one build step: - run: script/cibuild. No raw docker build anywhere in the workflow.
  • Exactly one pre-commit hook installer remains. backend/Makefile's hooks target is removed. make hooks at the root installs a hook whose script/precommit run gates the whole repo (frontend and backend).
  • The README Entrypoints section is updated to reflect what each script now does, including backend coverage.
  • make check passes at the root, and covers the Go code.
  • script/cibuild succeeds locally.
  • make check does not modify any tracked file (policy: "make check must not modify any files in the repo"). Verify git status --short is clean afterwards.
  • TODO.md updated in the same commit.
  • Commit title ends with (closes #N).

Implementation requirements

  • script/ files must be POSIX sh (#!/bin/sh, set -eu, no bashisms), locate the repo root with $(cd "$(dirname "$0")/.." && pwd -P), and cd there before acting.
  • Keep script/projectname and any other scripts that policy says stay byte-identical across repos unchanged.
  • Do not change the golangci-lint version or .golangci.yml here — that is issue #14. If #14 has already landed, just do not regress it.
  • Do not fold in the backend make test flag changes (-race, -cover, conditional verbose rerun) — that is a separate issue.
  • Total make test runtime must stay under 20 seconds with a 30-second timeout enforced; docker builds must stay under 5 minutes.
  • make targets and script/ entrypoints only — never raw go, yarn, gofmt, or golangci-lint.
  • No attribution trailers in the commit message.
## Problem The repo has two disconnected build systems, and the root gate does not actually gate the whole repo. Verified on `main` at `fbfe1df`. ### 1. Root `make check` ignores the backend entirely `Makefile` `check` shims to `script/check`, which runs the frontend `yarn build` plus `prettier --check`. It never touches `backend/`. Confirmed by running it: root `make check` passes while exercising zero Go code. This means the "`main` must always pass `make check`, no exceptions" policy is being satisfied vacuously — the Go backend could be entirely broken and the root gate would stay green. Anyone (or any pre-commit hook) running `make check` at the root gets false assurance. ### 2. The backend is not on scripts-to-rule-them-all `REPO_POLICIES.md` requires that "the implementation of each Makefile target lives in an executable script in `script/` ... and the Makefile targets are thin shims that call them." `backend/Makefile` is a full standalone Makefile with inline recipes (`go build`, `go test`, `golangci-lint run`, `gofmt`) and no `script/` indirection. It also violates the "always use Makefile targets instead of invoking the underlying tools directly" rule at the layer below. ### 3. Two competing pre-commit hook installers, one clobbers the other - `script/install-precommit` (shimmed by root `make hooks`) installs a hook running `script/precommit`. - `backend/Makefile`'s `hooks` target writes `.git/hooks/pre-commit` with the literal body `cd backend && make check`. Both target the same single `.git/hooks/pre-commit` path. Whichever ran last wins, so the developer silently ends up gating on only one half of the repo. There must be exactly one hook installer, and the hook must gate the whole repo. ### 4. CI invokes `docker build` directly instead of through `script/` `.gitea/workflows/check.yml`: ```yaml - run: script/cibuild - run: docker build -f Dockerfile.backend . ``` The second step is a raw tool invocation, bypassing the `script/` layer that is supposed to be the single source of truth for how the repo builds. Policy: the workflow runs `script/cibuild`; `script/cibuild` runs the docker build(s). ## Definition of done - [ ] Root `make check` fails if **either** the frontend or the backend is broken. Demonstrate this: deliberately break a Go file, confirm root `make check` fails, revert. - [ ] The backend's test/lint/fmt-check implementations live in `script/` entrypoints, and any retained `backend/Makefile` targets are thin shims. Whether that means extending the root `script/*` files to cover both halves or adding `backend/script/*` is an implementation choice — pick one, apply it consistently, and document the choice in the PR description. - [ ] `script/cibuild` builds **both** images (frontend `Dockerfile` and `Dockerfile.backend`). `.gitea/workflows/check.yml` contains exactly one build step: `- run: script/cibuild`. No raw `docker build` anywhere in the workflow. - [ ] Exactly one pre-commit hook installer remains. `backend/Makefile`'s `hooks` target is removed. `make hooks` at the root installs a hook whose `script/precommit` run gates the whole repo (frontend and backend). - [ ] The README **Entrypoints** section is updated to reflect what each script now does, including backend coverage. - [ ] `make check` passes at the root, and covers the Go code. - [ ] `script/cibuild` succeeds locally. - [ ] `make check` does not modify any tracked file (policy: "`make check` must not modify any files in the repo"). Verify `git status --short` is clean afterwards. - [ ] `TODO.md` updated in the same commit. - [ ] Commit title ends with ` (closes #N)`. ## Implementation requirements - `script/` files must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms), locate the repo root with `$(cd "$(dirname "$0")/.." && pwd -P)`, and `cd` there before acting. - Keep `script/projectname` and any other scripts that policy says stay byte-identical across repos unchanged. - Do **not** change the golangci-lint version or `.golangci.yml` here — that is issue #14. If #14 has already landed, just do not regress it. - Do not fold in the backend `make test` flag changes (`-race`, `-cover`, conditional verbose rerun) — that is a separate issue. - Total `make test` runtime must stay under 20 seconds with a 30-second timeout enforced; docker builds must stay under 5 minutes. - `make` targets and `script/` entrypoints only — never raw `go`, `yarn`, `gofmt`, or `golangci-lint`. - No attribution trailers in the commit message.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:37:45 +02:00
Author
Collaborator

Implementation plan

Branching from main at fbfe1df.

1. Where the backend's implementations go: backend/script/*.

The alternative was extending the root script/* files to shell out into
backend/. Dockerfile.backend settles it: its builder does
WORKDIR /repo/backend, COPY backend/ ., and then RUN make check. The
root script/ directory is never copied into that image, so if the backend's
check implementation lived in root script/* the backend image could not run
it without restructuring the Dockerfile and its layer caching. The backend is
also its own project already (own module, README.md, LICENSE,
.golangci.yml, .dockerignore), so it gets its own script layer:
backend/script/{build,test,lint,fmt,fmt-check,check,run,clean}, with
backend/Makefile reduced to thin shims. Each script uses the same
$(cd "$(dirname "$0")/.." && pwd -P) root discovery; for these, that root is
the backend project root.

2. Root scripts compose over both halves.

The frontend-only steps move to script/frontend-{test,lint,fmt,fmt-check},
and root script/test, script/lint, script/fmt and script/fmt-check
each run the frontend step and then the matching backend/script/* step.
script/check is unchanged in shape (test, lint, fmt-check) and therefore now
covers the Go backend, which also means script/precommit and the installed
hook cover it.

3. The frontend Dockerfile.

Its build stage is a node image with no Go toolchain, so it cannot run the
whole make check. It gets make check-frontend, shimmed to
script/frontend-check (the frontend half of the gate) — identical coverage
to what that image gates today. make check-backend is added as the mirror.
The two Dockerfiles together still gate the whole repo, and script/cibuild
builds both.

4. CI and docker.

script/cibuild builds both images through a single build_image helper, so
the workflow's only build step becomes - run: script/cibuild. script/docker
likewise builds and tags both (netwatch, netwatch-server).
backend/Makefile's docker target goes away: Dockerfile.backend lives at
the repo root and builds with the repo root as its context, so it belongs to
the root script layer rather than to a backend script that would have to reach
outside backend/.

5. Hooks.

backend/Makefile's hooks target is deleted. script/install-precommit
becomes the only installer, and the hook it writes runs the repo-wide
script/check.

6. Interaction with #14 / PR #31.

PR #31 is open and unmerged; it adds a sha256 drift guard for
.golangci.yml to backend/Makefile's lint target. That guard moves with
the lint implementation into backend/script/lint here, parameterized by a
GOLANGCI_CONFIG_SHA256 constant, pinned to the config as it exists on main
so this branch stays green. Whichever of the two lands second updates that one
constant. .golangci.yml itself is not touched here.

Verification I will run: deliberately break a Go file and show root
make check fails on this branch while it passes on main with the identical
break; the same for a Go formatting break and for the drift guard;
script/cibuild end to end; and the installed hook rejecting a broken commit
in each half. git status --short clean after make check.

## Implementation plan Branching from `main` at `fbfe1df`. **1. Where the backend's implementations go: `backend/script/*`.** The alternative was extending the root `script/*` files to shell out into `backend/`. `Dockerfile.backend` settles it: its builder does `WORKDIR /repo/backend`, `COPY backend/ .`, and then `RUN make check`. The root `script/` directory is never copied into that image, so if the backend's check implementation lived in root `script/*` the backend image could not run it without restructuring the Dockerfile and its layer caching. The backend is also its own project already (own module, `README.md`, `LICENSE`, `.golangci.yml`, `.dockerignore`), so it gets its own script layer: `backend/script/{build,test,lint,fmt,fmt-check,check,run,clean}`, with `backend/Makefile` reduced to thin shims. Each script uses the same `$(cd "$(dirname "$0")/.." && pwd -P)` root discovery; for these, that root is the backend project root. **2. Root scripts compose over both halves.** The frontend-only steps move to `script/frontend-{test,lint,fmt,fmt-check}`, and root `script/test`, `script/lint`, `script/fmt` and `script/fmt-check` each run the frontend step and then the matching `backend/script/*` step. `script/check` is unchanged in shape (test, lint, fmt-check) and therefore now covers the Go backend, which also means `script/precommit` and the installed hook cover it. **3. The frontend Dockerfile.** Its build stage is a node image with no Go toolchain, so it cannot run the whole `make check`. It gets `make check-frontend`, shimmed to `script/frontend-check` (the frontend half of the gate) — identical coverage to what that image gates today. `make check-backend` is added as the mirror. The two Dockerfiles together still gate the whole repo, and `script/cibuild` builds both. **4. CI and docker.** `script/cibuild` builds both images through a single `build_image` helper, so the workflow's only build step becomes `- run: script/cibuild`. `script/docker` likewise builds and tags both (`netwatch`, `netwatch-server`). `backend/Makefile`'s `docker` target goes away: `Dockerfile.backend` lives at the repo root and builds with the repo root as its context, so it belongs to the root script layer rather than to a backend script that would have to reach outside `backend/`. **5. Hooks.** `backend/Makefile`'s `hooks` target is deleted. `script/install-precommit` becomes the only installer, and the hook it writes runs the repo-wide `script/check`. **6. Interaction with #14 / PR #31.** PR #31 is open and unmerged; it adds a sha256 drift guard for `.golangci.yml` to `backend/Makefile`'s `lint` target. That guard moves with the lint implementation into `backend/script/lint` here, parameterized by a `GOLANGCI_CONFIG_SHA256` constant, pinned to the config as it exists on `main` so this branch stays green. Whichever of the two lands second updates that one constant. `.golangci.yml` itself is not touched here. **Verification I will run:** deliberately break a Go file and show root `make check` fails on this branch while it passes on `main` with the identical break; the same for a Go formatting break and for the drift guard; `script/cibuild` end to end; and the installed hook rejecting a broken commit in each half. `git status --short` clean after `make check`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#16