build: Dockerfile.backend multistage lint stage (closes #17) #40

Open
clawbot wants to merge 1 commits from feat/backend-dockerfile-lint-stage into main
Collaborator

Closes #17. Branched from main at fbfe1df; head is bd2bc9f. Three files:
Dockerfile.backend, backend/Makefile, TODO.md.

What changed

Dockerfile.backend becomes the three-stage shape REPO_POLICIES.md mandates:

before (main) after
linting RUN make check inside the builder, against a golangci-lint compiled from source by go install on every cache miss AS lint stage on the prebuilt, digest-pinned golangci/golangci-lint image; RUN make fmt-check then RUN make lint
stage ordering n/a COPY --from=lint /src/go.sum /dev/null in the builder
tests make check in the builder RUN make test in the builder
version source COPY .git /repo/.git so git describe resolves ARG VERSION=dev, handed to the build in the environment
linking gcc + musl-dev, -linkmode external -extldflags -static CGO_ENABLED=0 go build -trimpath, no C toolchain
builder packages git make gcc musl-dev make
workdir /repo/backend /src

Coverage is unchanged in total: main's single make check was
test + lint + fmt-check; that is now fmt-check + lint in the lint stage and
test in the builder. Runtime stage, EXPOSE 8080 and the entrypoint are
untouched. No Go source, route or application behaviour was changed.

backend/Makefile:

  • VERSION ?= $(shell { git describe --always --dirty; } 2>/dev/null || echo dev)
    — overridable, and the brace-group redirect means a missing .git or a
    missing git binary degrades to dev silently instead of printing
    fatal: not a git repository and stamping an empty version.
  • the UNAME_S / ifeq (Darwin) split and
    -linkmode external -extldflags -static are gone; one recipe,
    CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=… -X main.Buildarch=…".
  • -s -w added to GOLDFLAGS (policy pattern; also shrinks the binary).

Two deviations from the reference Dockerfile, both deliberate

1. The build runs through make, not an inline go build. The reference
Dockerfile in REPO_POLICIES.md writes
RUN CGO_ENABLED=0 go build -trimpath -ldflags=… directly. The same document
also says "Always use Makefile targets instead of invoking the underlying tools
directly. The Makefile is the single source of truth for how these operations
are run", and an inline copy would (a) create a second, divergable definition
of the build command and (b) silently drop the existing
-X main.Buildarch=$(BUILDARCH) ldflag, which is an application-behaviour
change this issue forbids. So the exact mandated flags live in the Makefile and
the build stage runs RUN VERSION="${VERSION}" make build. This is not an
assertion — the build log expands it in full:

#24 [builder 9/9] RUN VERSION="dev" make build
#24 0.161 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=dev -X main.Buildarch=x86_64" \
#24 0.161 	-o ./netwatch-server ./cmd/netwatch-server/

VERSION is passed in the environment rather than as make build VERSION=…
on purpose: both work against the Makefile as it stands, but only the
environment form keeps working unchanged if build is later turned into a shim
around a script — which is exactly what #38 does. See the reconciliation notes.

2. build is no longer an incremental file target. main had
./netwatch-server: $(shell find . -name '*.go' -type f) go.mod go.sum. With
VERSION now an input, that rule is actively wrong: make build VERSION=b
after make build VERSION=a is a no-op and yields a binary stamped a.
build is therefore phony and always compiles; Go's build cache makes the
no-op case ~0.1s.

Verification

Everything below was run in a scratch clone (#33 makes worktrees unusable
for make docker), through make targets and script/ entrypoints only — no
raw go, gofmt, yarn, prettier or golangci-lint. Every container is
--rm; nothing is left running on the host. No BuildKit cache was pruned
— uncached builds used --no-cache on the single build.

1. Uncached build, timed

docker build --no-cache -f Dockerfile.backend . at bd2bc9f: exit 0 in
48 s
(repeated runs 48–61 s), well inside the 5-minute budget. grep -c CACHED is 3, and all three are the two base-image FROM resolutions plus
one WORKDIR metadata step — zero cached RUN layers. Every RUN
executed for real:

#12 [stage-2 2/3] RUN apk add --no-cache ca-certificates
#13 [lint 4/7]    RUN go mod download
#14 [builder 2/9] RUN apk add --no-cache make
#17 [lint 6/7]    RUN make fmt-check
#18 [lint 7/7]    RUN make lint
#21 [builder 6/9] RUN go mod download
#23 [builder 8/9] RUN make test
#24 [builder 9/9] RUN VERSION="dev" make build

with real output underneath them (0 issues. from the linter, per-package
ok/no test files from go test, the expanded go build line above).
Per #37 a green CI tick is not evidence, so none is claimed.

2. The lint stage actually gates the build

A lint-only defect was appended to backend/internal/server/routes.go: a
201-character comment line. It compiles and make test passes locally, so any
build failure is unambiguously the linter and not the compiler.

(a) With COPY --from=lint present — build FAILS, exit 1:

#18 [lint 7/7] RUN make lint
#18 11.53 internal/server/routes.go:33:1: The line is 201 characters long, which exceeds the maximum of 120 characters. (lll)
#18 11.55 make: *** [Makefile:25: lint] Error 1
#18 ERROR: process "/bin/sh -c make lint" did not complete successfully: exit code: 2
ERROR: failed to build: failed to solve: process "/bin/sh -c make lint" did not complete successfully: exit code: 2

and it fails fast: the builder never got past step 3 of 9. Grepping the log
for a [builder …] RUN make test line returns 0 matches — compilation and
tests never started.

(b) Counterfactual, the same tree with only that one line deleted from the
Dockerfile — build SUCCEEDS, exit 0.
With COPY --from=lint /src/go.sum /dev/null removed, nothing references the lint stage, so BuildKit does not run
it at all (grep -c '\[lint …\] RUN make lint'0) and the image with the
lint error in it exports green. That is the whole point of the line, and it is
now demonstrated in both directions rather than asserted.

routes.go was then restored byte-identical (git status --short shows only
the three intended files).

3. The binary is still static, and still runs

Dropping -linkmode external -extldflags -static did not cost us the static
link — CGO_ENABLED=0 gives it for free:

  • on the host: ELF 64-bit LSB executable, x86-64 … statically linked … stripped
  • inside the alpine runtime image:
    ldd /usr/local/bin/netwatch-serverNot a valid dynamic program
  • the runtime image actually serves:
    GET /.well-known/healthcheckHTTP 200,
    {"appname":"netwatch-server","status":"ok",…,"version":"dev"}

ARG VERSION is wired end to end: built with --build-arg VERSION=1.2.3-test,
the running container reports "version":"1.2.3-test".

4. .git is genuinely no longer required

Built from a context tarred up without .git:

Dockerfile result
main's fails: failed to compute cache key … "/.git": not found
this branch's exit 0 in 48 s, uncached, stamping -X main.Version=dev

And at the Makefile level, in a tree with no .git:

result
main's backend/Makefile fatal: not a git repository (or any of the parent directories): .git, then builds with -X main.Version= — an empty version, silently
this branch's CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=dev …", exit 0, no stderr noise

Also exercised with the git binary removed from PATH entirely
(env -i PATH=<shim dir with no git>): exit 0, main.Version=dev.

5. Gates

  • root make checkexit 0
  • cd backend && make checkexit 0 (0 issues.)
  • cd backend && make dockerexit 0 (the make-target path to this image)
  • make fmt run over the touched markdown; git status --short clean after
    committing.

Reconciliation with the three open merge-ready PRs

This branch is cut from main and is coherent against main. It
deliberately does not pre-merge or anticipate any of the three. Below is what
whoever merges second has to do, per PR, per file.

PR #31feat/golangci-standard-config (4d70317)

Dockerfile.backend — guaranteed conflict, one hunk, mechanical.
#31 retargets the golangci-lint pin from 9f61b0f53f80672872fced07b6874397c3ed197b
(v2.7.2) to c0d3ddc9cf3faa61a4e378e879ece580256d76e5 (v2.12.2) on the
RUN CGO_ENABLED=0 go install … line. That line does not exist any more
the linter comes from the image, not from go install. So #31's Dockerfile
hunk does not rebase; it must be replaced by editing the two lines at the top
of the lint stage.

This branch pins the image whose --version reports exactly the commit main
already pins, so nothing regresses #14/#31:

$ docker run --rm golangci/golangci-lint@sha256:5d6d5c70…368ba golangci-lint --version
golangci-lint has version 2.7.2 built with go1.25.4 from 9f61b0f5 on 2025-12-07T16:57:12Z

Whichever lands second changes:

# golangci/golangci-lint:v2.7.2 (2026-08-09)
FROM golangci/golangci-lint@sha256:5d6d5c70a61f1356adfd9dd6316ce286799fefc9d743421356ff1b00842368ba AS lint

to the v2.12.2 image. On 2026-08-09 the v2.12.2 tag of
docker.io/golangci/golangci-lint resolved to
sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, and
that image reports has version 2.12.2 built with go1.26.5 from c0d3ddc9
i.e. #31's commit. Re-resolve and re-verify that digest at merge time rather
than trusting this paragraph
; a digest quoted in a PR body is not a pin.

backend/Makefile — textual conflict only, no semantic one. #31 rewrites
the lint recipe to add the .golangci.yml sha256 drift guard. This branch
does not touch lint at all; it rewrites the variable header (VERSION,
GOLDFLAGS, removal of the ifeq) and the build recipe. Take both sides:
this branch's header and build, #31's guarded lint. Git may well
auto-merge it.

One consequence worth knowing: after this branch, make lint runs in the
lint stage, not the builder, so #31's guard now executes on Debian trixie
rather than alpine. I checked that image: /usr/bin/sha256sum is present, so
the guard's primary path works there; its shasum -a 256 fallback is not
needed.

Neither PR touches backend/.golangci.yml except #31. I did not open that
file. What the second merge must re-verify is the pair: this branch proves
v2.7.2 + main's config lints clean through the lint stage; #31 proves
v2.12.2 + the canonical config lints clean through main's builder. Nobody
has yet proven v2.12.2 + canonical config through the lint stage, so run
docker build --no-cache -f Dockerfile.backend . once after reconciling.

TODO.md — both add to Completed Steps; #31 additionally rewrites Status
and Next Step. Mine is a single bullet at the top of Completed Steps. Keep both
bullets; take #31's Status/Next Step rewrite.

PR #35chore/dotfile-compliance (4a7bdf8)

No code overlap. It touches .editorconfig, .gitignore, TODO.md; the
only shared file is TODO.md, and both edits are additive lines in Completed
Steps. No conflict expected beyond a trivial one.

One thing to be aware of rather than to fix: #35 moves
backend/.editorconfig.editorconfig at the repo root. Both this branch's
lint and builder stages copy only backend/, so after #35 the
.editorconfig is outside the backend build context. Harmless — nothing in the
build reads it — but noting it so it is not mistaken for a regression later.

PR #38fix/unify-check-gate (1c16d50)

Dockerfile.backend — no conflict. I checked #38's changed-file list: it
does not touch Dockerfile.backend. Its PR body describes the backend image as
gated by "Dockerfile.backend's own RUN make check"; after this branch that
sentence is stale — the same coverage is RUN make fmt-check + RUN make lint
in the lint stage and RUN make test in the builder. That is prose in #38's
description and in backend/README.md, not code, but it should be corrected in
the second merge so the docs do not describe a step that no longer exists.

backend/Makefile — hard conflict, and one silent-failure trap. #38
replaces every recipe with a shim (build: @script/build) and moves the
implementation to backend/script/build. That script, at 1c16d50, still
contains what this issue exists to remove:

version="$(git describe --always --dirty 2>/dev/null || echo unknown)"if [ "$(uname -s)" != "Darwin" ]; then
    ldflags="-linkmode external -extldflags -static $ldflags"
fi
go build -o "$BINARY" -ldflags "$ldflags" ./cmd/netwatch-server/

Resolution, whichever order: keep #38's shim backend/Makefile, and move this
branch's build semantics into backend/script/build, which must

  1. honour an inherited VERSION
    version="${VERSION:-$(git describe --always --dirty 2>/dev/null || echo dev)}".
    This is the trap: Dockerfile.backend passes the version as
    RUN VERSION="${VERSION}" make build, and if script/build ignores the
    environment the ARG VERSION silently stops reaching the binary and every
    image is stamped unknown with nothing failing. I chose the environment
    form specifically so the Dockerfile line itself needs no edit in that merge;
    the script is the only thing that has to change.
  2. drop the -linkmode external -extldflags -static branch and the uname -s
    test, and build
    CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=$version -X main.Buildarch=$buildarch".
    Leaving the static branch in would reintroduce the CGO dependency into a
    builder that no longer installs gcc/musl-dev, and that build will
    fail, so this one fails loudly rather than silently.
  3. align the fallback string on dev, matching ARG VERSION=dev, rather than
    unknown.

Also: #38's backend/script/lint and backend/script/fmt-check become what
the lint stage runs. They are #!/bin/sh with no bashisms and use
sha256sum/gofmt, all of which exist in the golangci-lint image (Debian
trixie), so they run there unmodified.

script/bootstrap — no new drift. #38 pins GOLANGCI_LINT_VERSION=2.7.2
to match Dockerfile.backend, with a comment naming #31. This branch keeps the
same linter version, so that pin still agrees with the Dockerfile; it moves to
2.12.2 at the same time as the image digest, in the #31 reconciliation above.

.gitea/workflows/check.yml — untouched here; #38 removes the second raw
docker build -f Dockerfile.backend . step. No conflict.

TODO.md — additive on both sides.

Does this make #36 easier?

Yes, for the backend half — that half is now done, not merely easier. #36
wants .git out of the build context entirely. Dockerfile.backend no longer
copies or needs it: proven above by building from a context with no .git at
all, uncached, exit 0. Once .git is added to .dockerignore, the backend
image is unaffected.

#36 stays blocked, though, and this changes nothing about why: the frontend
Dockerfile's build stage evaluates vite.config.js, which calls
execSync("git rev-parse HEAD") at config-eval time, so .git in
.dockerignore breaks that build. Whoever takes #36 needs the same treatment
there — a build arg with a fallback — and after that the .dockerignore line
is a one-liner. I did not touch the frontend, per scope.

#33 (worktree .git is a file)

Not fixed, and out of scope. Incidentally improved on the docker half only:
make docker for the backend previously did COPY .git, which in a worktree
copies a .git file pointing at a gitdir that does not exist in the
container. That failure mode is gone because nothing copies .git any more.
make hooks in backend/Makefile still writes to
$(git rev-parse --show-toplevel)/.git/hooks/pre-commit and still breaks in a
worktree; untouched here. All work on this PR was done in a scratch clone.

Out of scope, noticed, not fixed

Running cd backend && make check on a host with golangci-lint v2.12.2
(the version #31 moves to) prints
The linter 'gomodguard' is deprecated (since v2.12.0) … Replaced by gomodguard_v2.
It does not appear in this build, which pins v2.7.2, and it is not a failure.
Filed separately rather than fixed drive-by.

Closes #17. Branched from `main` at `fbfe1df`; head is `bd2bc9f`. Three files: `Dockerfile.backend`, `backend/Makefile`, `TODO.md`. ## What changed `Dockerfile.backend` becomes the three-stage shape `REPO_POLICIES.md` mandates: | | before (`main`) | after | | --- | --- | --- | | linting | `RUN make check` inside the builder, against a golangci-lint compiled from source by `go install` on every cache miss | `AS lint` stage on the prebuilt, digest-pinned `golangci/golangci-lint` image; `RUN make fmt-check` then `RUN make lint` | | stage ordering | n/a | `COPY --from=lint /src/go.sum /dev/null` in the builder | | tests | `make check` in the builder | `RUN make test` in the builder | | version source | `COPY .git /repo/.git` so `git describe` resolves | `ARG VERSION=dev`, handed to the build in the environment | | linking | `gcc` + `musl-dev`, `-linkmode external -extldflags -static` | `CGO_ENABLED=0 go build -trimpath`, no C toolchain | | builder packages | `git make gcc musl-dev` | `make` | | workdir | `/repo/backend` | `/src` | Coverage is unchanged in total: `main`'s single `make check` was test + lint + fmt-check; that is now fmt-check + lint in the `lint` stage and test in the builder. Runtime stage, `EXPOSE 8080` and the entrypoint are untouched. No Go source, route or application behaviour was changed. `backend/Makefile`: - `VERSION ?= $(shell { git describe --always --dirty; } 2>/dev/null || echo dev)` — overridable, and the brace-group redirect means a missing `.git` **or** a missing `git` binary degrades to `dev` silently instead of printing `fatal: not a git repository` and stamping an empty version. - the `UNAME_S` / `ifeq (Darwin)` split and `-linkmode external -extldflags -static` are gone; one recipe, `CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=… -X main.Buildarch=…"`. - `-s -w` added to `GOLDFLAGS` (policy pattern; also shrinks the binary). ### Two deviations from the reference Dockerfile, both deliberate **1. The build runs through `make`, not an inline `go build`.** The reference Dockerfile in `REPO_POLICIES.md` writes `RUN CGO_ENABLED=0 go build -trimpath -ldflags=…` directly. The same document also says "Always use Makefile targets instead of invoking the underlying tools directly. The Makefile is the single source of truth for how these operations are run", and an inline copy would (a) create a second, divergable definition of the build command and (b) silently drop the existing `-X main.Buildarch=$(BUILDARCH)` ldflag, which is an application-behaviour change this issue forbids. So the exact mandated flags live in the Makefile and the build stage runs `RUN VERSION="${VERSION}" make build`. This is not an assertion — the build log expands it in full: ``` #24 [builder 9/9] RUN VERSION="dev" make build #24 0.161 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=dev -X main.Buildarch=x86_64" \ #24 0.161 -o ./netwatch-server ./cmd/netwatch-server/ ``` `VERSION` is passed in the **environment** rather than as `make build VERSION=…` on purpose: both work against the Makefile as it stands, but only the environment form keeps working unchanged if `build` is later turned into a shim around a script — which is exactly what #38 does. See the reconciliation notes. **2. `build` is no longer an incremental file target.** `main` had `./netwatch-server: $(shell find . -name '*.go' -type f) go.mod go.sum`. With `VERSION` now an input, that rule is actively wrong: `make build VERSION=b` after `make build VERSION=a` is a no-op and yields a binary stamped `a`. `build` is therefore phony and always compiles; Go's build cache makes the no-op case ~0.1s. ## Verification Everything below was run in a scratch **clone** (#33 makes worktrees unusable for `make docker`), through `make` targets and `script/` entrypoints only — no raw `go`, `gofmt`, `yarn`, `prettier` or `golangci-lint`. Every container is `--rm`; nothing is left running on the host. **No BuildKit cache was pruned** — uncached builds used `--no-cache` on the single build. ### 1. Uncached build, timed `docker build --no-cache -f Dockerfile.backend .` at `bd2bc9f`: **exit 0 in 48 s** (repeated runs 48–61 s), well inside the 5-minute budget. `grep -c CACHED` is **3**, and all three are the two base-image `FROM` resolutions plus one `WORKDIR` metadata step — **zero cached `RUN` layers**. Every `RUN` executed for real: ``` #12 [stage-2 2/3] RUN apk add --no-cache ca-certificates #13 [lint 4/7] RUN go mod download #14 [builder 2/9] RUN apk add --no-cache make #17 [lint 6/7] RUN make fmt-check #18 [lint 7/7] RUN make lint #21 [builder 6/9] RUN go mod download #23 [builder 8/9] RUN make test #24 [builder 9/9] RUN VERSION="dev" make build ``` with real output underneath them (`0 issues.` from the linter, per-package `ok`/`no test files` from `go test`, the expanded `go build` line above). Per #37 a green CI tick is not evidence, so none is claimed. ### 2. The lint stage actually gates the build A lint-only defect was appended to `backend/internal/server/routes.go`: a 201-character comment line. It compiles and `make test` passes locally, so any build failure is unambiguously the linter and not the compiler. **(a) With `COPY --from=lint` present — build FAILS, exit 1:** ``` #18 [lint 7/7] RUN make lint #18 11.53 internal/server/routes.go:33:1: The line is 201 characters long, which exceeds the maximum of 120 characters. (lll) #18 11.55 make: *** [Makefile:25: lint] Error 1 #18 ERROR: process "/bin/sh -c make lint" did not complete successfully: exit code: 2 ERROR: failed to build: failed to solve: process "/bin/sh -c make lint" did not complete successfully: exit code: 2 ``` and it fails **fast**: the builder never got past step 3 of 9. Grepping the log for a `[builder …] RUN make test` line returns **0** matches — compilation and tests never started. **(b) Counterfactual, the same tree with only that one line deleted from the Dockerfile — build SUCCEEDS, exit 0.** With `COPY --from=lint /src/go.sum /dev/null` removed, nothing references the lint stage, so BuildKit does not run it at all (`grep -c '\[lint …\] RUN make lint'` → **0**) and the image with the lint error in it exports green. That is the whole point of the line, and it is now demonstrated in both directions rather than asserted. `routes.go` was then restored byte-identical (`git status --short` shows only the three intended files). ### 3. The binary is still static, and still runs Dropping `-linkmode external -extldflags -static` did not cost us the static link — `CGO_ENABLED=0` gives it for free: - on the host: `ELF 64-bit LSB executable, x86-64 … statically linked … stripped` - inside the alpine runtime image: `ldd /usr/local/bin/netwatch-server` → `Not a valid dynamic program` - the runtime image actually serves: `GET /.well-known/healthcheck` → **HTTP 200**, `{"appname":"netwatch-server","status":"ok",…,"version":"dev"}` `ARG VERSION` is wired end to end: built with `--build-arg VERSION=1.2.3-test`, the running container reports `"version":"1.2.3-test"`. ### 4. `.git` is genuinely no longer required Built from a context tarred up **without** `.git`: | Dockerfile | result | | --- | --- | | `main`'s | **fails**: `failed to compute cache key … "/.git": not found` | | this branch's | **exit 0 in 48 s**, uncached, stamping `-X main.Version=dev` | And at the Makefile level, in a tree with no `.git`: | | result | | --- | --- | | `main`'s `backend/Makefile` | `fatal: not a git repository (or any of the parent directories): .git`, then builds with `-X main.Version=` — an **empty** version, silently | | this branch's | `CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=dev …"`, exit 0, no stderr noise | Also exercised with the `git` **binary** removed from `PATH` entirely (`env -i PATH=<shim dir with no git>`): exit 0, `main.Version=dev`. ### 5. Gates - root `make check` — **exit 0** - `cd backend && make check` — **exit 0** (`0 issues.`) - `cd backend && make docker` — **exit 0** (the make-target path to this image) - `make fmt` run over the touched markdown; `git status --short` clean after committing. ## Reconciliation with the three open merge-ready PRs This branch is cut from `main` and is coherent **against `main`**. It deliberately does not pre-merge or anticipate any of the three. Below is what whoever merges second has to do, per PR, per file. ### PR #31 — `feat/golangci-standard-config` (`4d70317`) **`Dockerfile.backend` — guaranteed conflict, one hunk, mechanical.** #31 retargets the golangci-lint pin from `9f61b0f53f80672872fced07b6874397c3ed197b` (v2.7.2) to `c0d3ddc9cf3faa61a4e378e879ece580256d76e5` (v2.12.2) on the `RUN CGO_ENABLED=0 go install …` line. **That line does not exist any more** — the linter comes from the image, not from `go install`. So #31's Dockerfile hunk does not rebase; it must be replaced by editing the two lines at the top of the `lint` stage. This branch pins the image whose `--version` reports exactly the commit `main` already pins, so nothing regresses #14/#31: ``` $ docker run --rm golangci/golangci-lint@sha256:5d6d5c70…368ba golangci-lint --version golangci-lint has version 2.7.2 built with go1.25.4 from 9f61b0f5 on 2025-12-07T16:57:12Z ``` Whichever lands second changes: ```dockerfile # golangci/golangci-lint:v2.7.2 (2026-08-09) FROM golangci/golangci-lint@sha256:5d6d5c70a61f1356adfd9dd6316ce286799fefc9d743421356ff1b00842368ba AS lint ``` to the v2.12.2 image. On 2026-08-09 the `v2.12.2` tag of `docker.io/golangci/golangci-lint` resolved to `sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`, and that image reports `has version 2.12.2 built with go1.26.5 from c0d3ddc9` — i.e. #31's commit. **Re-resolve and re-verify that digest at merge time rather than trusting this paragraph**; a digest quoted in a PR body is not a pin. **`backend/Makefile` — textual conflict only, no semantic one.** #31 rewrites the `lint` recipe to add the `.golangci.yml` sha256 drift guard. This branch does not touch `lint` at all; it rewrites the variable header (`VERSION`, `GOLDFLAGS`, removal of the `ifeq`) and the `build` recipe. Take both sides: this branch's header and `build`, #31's guarded `lint`. Git may well auto-merge it. One consequence worth knowing: after this branch, `make lint` runs in the **lint stage**, not the builder, so #31's guard now executes on Debian trixie rather than alpine. I checked that image: `/usr/bin/sha256sum` is present, so the guard's primary path works there; its `shasum -a 256` fallback is not needed. **Neither PR touches `backend/.golangci.yml` except #31.** I did not open that file. What the second merge must re-verify is the **pair**: this branch proves `v2.7.2` + `main`'s config lints clean through the lint stage; #31 proves `v2.12.2` + the canonical config lints clean through `main`'s builder. Nobody has yet proven `v2.12.2` + canonical config **through the lint stage**, so run `docker build --no-cache -f Dockerfile.backend .` once after reconciling. **`TODO.md`** — both add to Completed Steps; #31 additionally rewrites Status and Next Step. Mine is a single bullet at the top of Completed Steps. Keep both bullets; take #31's Status/Next Step rewrite. ### PR #35 — `chore/dotfile-compliance` (`4a7bdf8`) **No code overlap.** It touches `.editorconfig`, `.gitignore`, `TODO.md`; the only shared file is `TODO.md`, and both edits are additive lines in Completed Steps. No conflict expected beyond a trivial one. One thing to be aware of rather than to fix: #35 moves `backend/.editorconfig` → `.editorconfig` at the repo root. Both this branch's `lint` and `builder` stages copy only `backend/`, so after #35 the `.editorconfig` is outside the backend build context. Harmless — nothing in the build reads it — but noting it so it is not mistaken for a regression later. ### PR #38 — `fix/unify-check-gate` (`1c16d50`) **`Dockerfile.backend` — no conflict.** I checked #38's changed-file list: it does not touch `Dockerfile.backend`. Its PR body describes the backend image as gated by "`Dockerfile.backend`'s own `RUN make check`"; after this branch that sentence is stale — the same coverage is `RUN make fmt-check` + `RUN make lint` in the `lint` stage and `RUN make test` in the builder. That is prose in #38's description and in `backend/README.md`, not code, but it should be corrected in the second merge so the docs do not describe a step that no longer exists. **`backend/Makefile` — hard conflict, and one silent-failure trap.** #38 replaces every recipe with a shim (`build: @script/build`) and moves the implementation to `backend/script/build`. That script, at `1c16d50`, still contains what this issue exists to remove: ```sh version="$(git describe --always --dirty 2>/dev/null || echo unknown)" … if [ "$(uname -s)" != "Darwin" ]; then ldflags="-linkmode external -extldflags -static $ldflags" fi go build -o "$BINARY" -ldflags "$ldflags" ./cmd/netwatch-server/ ``` Resolution, whichever order: keep #38's shim `backend/Makefile`, and move this branch's build semantics into `backend/script/build`, which must 1. **honour an inherited `VERSION`** — `version="${VERSION:-$(git describe --always --dirty 2>/dev/null || echo dev)}"`. This is the trap: `Dockerfile.backend` passes the version as `RUN VERSION="${VERSION}" make build`, and if `script/build` ignores the environment the `ARG VERSION` silently stops reaching the binary and every image is stamped `unknown` with nothing failing. I chose the environment form specifically so the Dockerfile line itself needs no edit in that merge; the script is the only thing that has to change. 2. drop the `-linkmode external -extldflags -static` branch and the `uname -s` test, and build `CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=$version -X main.Buildarch=$buildarch"`. Leaving the static branch in would reintroduce the CGO dependency into a builder that no longer installs `gcc`/`musl-dev`, and that build **will** fail, so this one fails loudly rather than silently. 3. align the fallback string on `dev`, matching `ARG VERSION=dev`, rather than `unknown`. Also: #38's `backend/script/lint` and `backend/script/fmt-check` become what the `lint` stage runs. They are `#!/bin/sh` with no bashisms and use `sha256sum`/`gofmt`, all of which exist in the golangci-lint image (Debian trixie), so they run there unmodified. **`script/bootstrap` — no new drift.** #38 pins `GOLANGCI_LINT_VERSION=2.7.2` to match `Dockerfile.backend`, with a comment naming #31. This branch keeps the same linter version, so that pin still agrees with the Dockerfile; it moves to `2.12.2` at the same time as the image digest, in the #31 reconciliation above. **`.gitea/workflows/check.yml`** — untouched here; #38 removes the second raw `docker build -f Dockerfile.backend .` step. No conflict. **`TODO.md`** — additive on both sides. ## Does this make #36 easier? Yes, for the backend half — that half is now **done**, not merely easier. #36 wants `.git` out of the build context entirely. `Dockerfile.backend` no longer copies or needs it: proven above by building from a context with no `.git` at all, uncached, exit 0. Once `.git` is added to `.dockerignore`, the backend image is unaffected. #36 stays blocked, though, and this changes nothing about why: the **frontend** `Dockerfile`'s build stage evaluates `vite.config.js`, which calls `execSync("git rev-parse HEAD")` at config-eval time, so `.git` in `.dockerignore` breaks that build. Whoever takes #36 needs the same treatment there — a build arg with a fallback — and after that the `.dockerignore` line is a one-liner. I did not touch the frontend, per scope. ## #33 (worktree `.git` is a file) Not fixed, and out of scope. Incidentally improved on the docker half only: `make docker` for the backend previously did `COPY .git`, which in a worktree copies a `.git` **file** pointing at a gitdir that does not exist in the container. That failure mode is gone because nothing copies `.git` any more. `make hooks` in `backend/Makefile` still writes to `$(git rev-parse --show-toplevel)/.git/hooks/pre-commit` and still breaks in a worktree; untouched here. All work on this PR was done in a scratch clone. ## Out of scope, noticed, not fixed Running `cd backend && make check` on a host with golangci-lint **v2.12.2** (the version #31 moves to) prints `The linter 'gomodguard' is deprecated (since v2.12.0) … Replaced by gomodguard_v2`. It does not appear in this build, which pins v2.7.2, and it is not a failure. Filed separately rather than fixed drive-by.
clawbot added 1 commit 2026-08-09 12:15:25 +02:00
build: Dockerfile.backend multistage lint stage (closes #17)
All checks were successful
check / check (push) Successful in 16s
bd2bc9f626
Dockerfile.backend did not follow the Go multistage lint-stage pattern
REPO_POLICIES.md mandates, and dragged the whole git history into the
build context to resolve a version string.

- Add an `AS lint` stage on the hash-pinned golangci/golangci-lint
  image (v2.7.2, the same golangci-lint commit main already pins), which
  ships Go, gofmt, make and the linter, so nothing is installed in it.
  It runs `make fmt-check` then `make lint`.
- Add `COPY --from=lint /src/go.sum /dev/null` to the build stage so
  BuildKit cannot run the two stages in parallel and let a lint failure
  through.
- Stop compiling golangci-lint from source in the build stage.
- Drop `COPY .git /repo/.git`; the version now comes from
  `ARG VERSION=dev`, passed to the build via `make build VERSION=...`.
- Drop gcc and musl-dev, and the corresponding
  `-linkmode external -extldflags -static` in backend/Makefile. The
  build is now `CGO_ENABLED=0 go build -trimpath` with
  `-ldflags "-s -w -X main.Version=... -X main.Buildarch=..."`, which is
  static without a C toolchain.
- backend/Makefile's VERSION is now overridable and degrades to `dev`
  when git or .git is unavailable instead of emitting a git error and
  building an empty version string.
- Every FROM stays pinned by @sha256 with a version and date comment.

Runtime stage, exposed port and entrypoint are unchanged.
clawbot added the needs-review label 2026-08-09 12:15:39 +02:00
clawbot self-assigned this 2026-08-09 12:15:40 +02:00
Author
Collaborator

Summary

Head bd2bc9f, branched from main at fbfe1df. One commit, three files:
Dockerfile.backend, backend/Makefile, TODO.md (the TODO edit is in the
same commit, one additive bullet, because #31/#35/#38 all touch that file too).

Dockerfile.backend is now lintbuilder → runtime. Linting moved out of
the builder into a lint stage on the digest-pinned golangci/golangci-lint
image, which already ships Go, gofmt, make and the linter, so the
go install-from-source of golangci-lint is gone. COPY --from=lint /src/go.sum /dev/null chains the stages. The builder installs only make,
runs make test, and builds from ARG VERSION=dev. COPY .git /repo/.git is
deleted, and with the CGO static-link flags dropped from backend/Makefile,
gcc and musl-dev go with them. All three FROMs are @sha256: with a
version + date comment; the two unchanged pins keep their original 2026-02-27
dates, since the digests were not re-pinned (both tags have since moved, which
is the point of pinning).

The linter version is unchanged from main on purpose: the image I pinned
reports version 2.7.2 built with go1.25.4 from 9f61b0f5, the exact commit
main pins. #31's v2.12.2 is not pre-merged; the PR body has the
per-file, per-PR reconciliation for #31, #35 and #38, including the v2.12.2
image digest to substitute and an instruction to re-resolve it rather than
trust it.

How it was verified

Scratch clone, not a worktree (#33). make targets and script/ entrypoints
only — no raw go, gofmt, yarn, prettier or golangci-lint. Containers
all --rm, none left running. No BuildKit cache pruned; uncached builds used
--no-cache on the single build.

  • Uncached build: exit 0 in 48 s. 3 CACHED lines, all of them base
    FROM resolutions plus one WORKDIR — zero cached RUN layers, with real
    linter/test/compile output in the log. No CI tick is offered as evidence
    (#37).
  • The lint stage really gates, shown both ways with a 201-char comment
    line added to routes.go (compiles fine, tests pass, so only the linter can
    object): with COPY --from=lint the build fails at
    [lint 7/7] RUN make lint and the builder never reaches make test;
    without that one line the identical tree builds green and the lint
    stage is never executed at all.
  • Still static, still runs: Not a valid dynamic program from ldd
    inside the alpine runtime stage, and the container answers
    GET /.well-known/healthcheck with 200. --build-arg VERSION=1.2.3-test
    comes out as "version":"1.2.3-test" at runtime.
  • No .git needed: from a context with .git removed, main's
    Dockerfile fails on "/.git": not found while this one builds uncached in
    48 s. At the Makefile level, main's emits fatal: not a git repository and
    stamps an empty version; this one stamps dev silently, including with
    the git binary absent from PATH entirely.
  • Root make check 0, cd backend && make check 0,
    cd backend && make docker 0, make fmt run over the touched markdown.

Two deviations from the reference Dockerfile are argued in the PR body: the
build goes through make build (so there is one definition of the build
command, and -X main.Buildarch is not silently dropped) with VERSION passed
in the environment so it survives #38 turning that target into a shim; and
build is no longer an incremental file target, which is now wrong given
VERSION is an input.

Backend half of #36 is effectively done — nothing copies or needs .git — but
#36 stays blocked on the frontend Dockerfile, whose vite.config.js calls
execSync("git rev-parse HEAD") at config-eval time. Not touched, per scope.

One out-of-scope observation was filed as #41 rather than fixed here: under
golangci-lint v2.12.2 the config's default: all pulls in gomodguard, which
that version deprecates in favour of gomodguard_v2.

## Summary Head `bd2bc9f`, branched from `main` at `fbfe1df`. One commit, three files: `Dockerfile.backend`, `backend/Makefile`, `TODO.md` (the TODO edit is in the same commit, one additive bullet, because #31/#35/#38 all touch that file too). `Dockerfile.backend` is now `lint` → `builder` → runtime. Linting moved out of the builder into a `lint` stage on the digest-pinned `golangci/golangci-lint` image, which already ships Go, `gofmt`, `make` and the linter, so the `go install`-from-source of golangci-lint is gone. `COPY --from=lint /src/go.sum /dev/null` chains the stages. The builder installs only `make`, runs `make test`, and builds from `ARG VERSION=dev`. `COPY .git /repo/.git` is deleted, and with the CGO static-link flags dropped from `backend/Makefile`, `gcc` and `musl-dev` go with them. All three `FROM`s are `@sha256:` with a version + date comment; the two unchanged pins keep their original `2026-02-27` dates, since the digests were not re-pinned (both tags have since moved, which is the point of pinning). The linter version is unchanged from `main` on purpose: the image I pinned reports `version 2.7.2 built with go1.25.4 from 9f61b0f5`, the exact commit `main` pins. #31's v2.12.2 is **not** pre-merged; the PR body has the per-file, per-PR reconciliation for #31, #35 and #38, including the v2.12.2 image digest to substitute and an instruction to re-resolve it rather than trust it. ## How it was verified Scratch clone, not a worktree (#33). `make` targets and `script/` entrypoints only — no raw `go`, `gofmt`, `yarn`, `prettier` or `golangci-lint`. Containers all `--rm`, none left running. No BuildKit cache pruned; uncached builds used `--no-cache` on the single build. - **Uncached build**: exit 0 in **48 s**. 3 `CACHED` lines, all of them base `FROM` resolutions plus one `WORKDIR` — zero cached `RUN` layers, with real linter/test/compile output in the log. No CI tick is offered as evidence (#37). - **The lint stage really gates**, shown both ways with a 201-char comment line added to `routes.go` (compiles fine, tests pass, so only the linter can object): **with** `COPY --from=lint` the build **fails** at `[lint 7/7] RUN make lint` and the builder never reaches `make test`; **without** that one line the identical tree builds **green** and the lint stage is never executed at all. - **Still static, still runs**: `Not a valid dynamic program` from `ldd` inside the alpine runtime stage, and the container answers `GET /.well-known/healthcheck` with **200**. `--build-arg VERSION=1.2.3-test` comes out as `"version":"1.2.3-test"` at runtime. - **No `.git` needed**: from a context with `.git` removed, `main`'s Dockerfile fails on `"/.git": not found` while this one builds uncached in 48 s. At the Makefile level, `main`'s emits `fatal: not a git repository` and stamps an **empty** version; this one stamps `dev` silently, including with the `git` binary absent from `PATH` entirely. - Root `make check` **0**, `cd backend && make check` **0**, `cd backend && make docker` **0**, `make fmt` run over the touched markdown. Two deviations from the reference Dockerfile are argued in the PR body: the build goes through `make build` (so there is one definition of the build command, and `-X main.Buildarch` is not silently dropped) with `VERSION` passed in the **environment** so it survives #38 turning that target into a shim; and `build` is no longer an incremental file target, which is now wrong given `VERSION` is an input. Backend half of #36 is effectively done — nothing copies or needs `.git` — but #36 stays blocked on the frontend `Dockerfile`, whose `vite.config.js` calls `execSync("git rev-parse HEAD")` at config-eval time. Not touched, per scope. One out-of-scope observation was filed as #41 rather than fixed here: under golangci-lint v2.12.2 the config's `default: all` pulls in `gomodguard`, which that version deprecates in favour of `gomodguard_v2`.
Author
Collaborator

Manager note — review INCOMPLETE, this PR is not cleared

The independent reviewer assigned to this PR terminated early on an API quota limit, partway through. It did not post a review comment and it did not reach a verdict.

Label stays needs-review, assignee stays clawbot. This PR has not passed review and must not be merged on the strength of what follows.

What the reviewer had confirmed before it died

One thing only, but it is the most important claim in the PR:

> Both directions of the lint gate confirmed.

That is the COPY --from=lint /src/go.sum /dev/null behaviour — with the line present a lint error fails the build, and without it the identical broken tree builds green while the lint stage never executes. Independently reproduced. That was the central requirement of #17 and the thing most likely to appear correct for the wrong reason, so having it confirmed by someone other than the author is worth recording.

It was mid-way through "restoring the tree and testing the runtime claims" when it stopped.

What remains UNVERIFIED by anyone but the author

Everything else, specifically:

  • The lint image digest sha256:5d6d5c70… genuinely being golangci-lint v2.7.2 and matching the commit 9f61b0f53f80672872fced07b6874397c3ed197b that main pins. A mismatch here would silently lint with a different ruleset than CI — it is the highest-value unverified item.
  • The binary still being static and actually running in the alpine runtime stage after gcc/musl-dev and the CGO static-link flags were dropped.
  • --build-arg VERSION=... reaching the binary.
  • Building from a context with no .git, and backend/Makefile stamping dev rather than failing or stamping empty.
  • The two deliberate deviations from the mandated pattern — RUN VERSION="${VERSION}" make build instead of an inline go build, and build no longer being an incremental file target — being sound rather than merely convenient.
  • The per-PR reconciliation notes for #31, #35 and #38, including the named silent-failure trap where #38's backend/script/build must honour an inherited VERSION or everything stamps unknown with nothing failing.
  • Scope, pinning of every FROM, one-commit hygiene, and the claim that #36's backend half is now fully done.

What happens next

A fresh reviewer picks this up when quota allows. The brief is unchanged; whoever takes it should treat the lint-gate result above as corroborating evidence rather than as settled, and re-derive it cheaply if convenient — one confirmation from a run that did not complete is weaker than one from a run that did.

Recording this explicitly because a PR sitting at needs-review with a manager comment on it could easily be mistaken for a reviewed PR. It is not.

## Manager note — review INCOMPLETE, this PR is not cleared The independent reviewer assigned to this PR **terminated early** on an API quota limit, partway through. It did not post a review comment and it did not reach a verdict. Label stays `needs-review`, assignee stays `clawbot`. **This PR has not passed review and must not be merged on the strength of what follows.** ### What the reviewer had confirmed before it died One thing only, but it is the most important claim in the PR: > Both directions of the lint gate confirmed. That is the `COPY --from=lint /src/go.sum /dev/null` behaviour — with the line present a lint error fails the build, and without it the identical broken tree builds green while the lint stage never executes. Independently reproduced. That was the central requirement of #17 and the thing most likely to appear correct for the wrong reason, so having it confirmed by someone other than the author is worth recording. It was mid-way through "restoring the tree and testing the runtime claims" when it stopped. ### What remains UNVERIFIED by anyone but the author Everything else, specifically: - The lint image digest `sha256:5d6d5c70…` genuinely being golangci-lint **v2.7.2** and matching the commit `9f61b0f53f80672872fced07b6874397c3ed197b` that `main` pins. A mismatch here would silently lint with a different ruleset than CI — it is the highest-value unverified item. - The binary still being static and actually running in the alpine runtime stage after `gcc`/`musl-dev` and the CGO static-link flags were dropped. - `--build-arg VERSION=...` reaching the binary. - Building from a context with no `.git`, and `backend/Makefile` stamping `dev` rather than failing or stamping empty. - The two deliberate deviations from the mandated pattern — `RUN VERSION="${VERSION}" make build` instead of an inline `go build`, and `build` no longer being an incremental file target — being sound rather than merely convenient. - The per-PR reconciliation notes for #31, #35 and #38, including the named silent-failure trap where #38's `backend/script/build` must honour an inherited `VERSION` or everything stamps `unknown` with nothing failing. - Scope, pinning of every `FROM`, one-commit hygiene, and the claim that #36's backend half is now fully done. ### What happens next A fresh reviewer picks this up when quota allows. The brief is unchanged; whoever takes it should treat the lint-gate result above as corroborating evidence rather than as settled, and re-derive it cheaply if convenient — one confirmation from a run that did not complete is weaker than one from a run that did. Recording this explicitly because a PR sitting at `needs-review` with a manager comment on it could easily be mistaken for a reviewed PR. It is not.
Author
Collaborator

Independent review — head bd2bc9f, base main at fbfe1df

Verdict: PASS

Priorities 1 through 5 were all reached. Every functional claim in the PR body
was re-derived independently in a fresh scratch clone (not a worktree, per
#33). No BuildKit cache was pruned; the one uncached build used --no-cache
scoped to that single build. All test images I created have been removed.


PRIORITY 1 — lint image digest (the highest-value unverified item)

Confirmed. Pulled and executed the exact digest in the Dockerfile:

$ docker run --rm golangci/golangci-lint@sha256:5d6d5c70a61f1356adfd9dd6316ce286799fefc9d743421356ff1b00842368ba golangci-lint --version
golangci-lint has version 2.7.2 built with go1.25.4 from 9f61b0f5 on 2025-12-07T16:57:12Z

9f61b0f5 is the prefix of 9f61b0f53f80672872fced07b6874397c3ed197b, which
is exactly what main's Dockerfile.backend pins on its
go install github.com/golangci/golangci-lint/v2/... line. The lint stage
therefore enforces the same linter build CI has been enforcing. No silent
ruleset change.

Also confirmed by building --target lint and inspecting the stage:
/src/.golangci.yml (739 bytes) is present, so the repo's config — not
golangci-lint defaults — is what runs. go, gofmt (/usr/local/go/bin/gofmt)
and make (/usr/bin/make) all exist in the image, so the stage genuinely
installs nothing.

I also checked that make fmt-check is not vacuous in that image (a missing
gofmt would make test -z "$(gofmt -l .)" pass silently). Injected a
misformatted file into the lint stage: make fmt-check printed
Files not formatted: internal/server/revbadfmt.go and exited 2. Real gate.

Pinning. All three FROM lines are @sha256: with a version-and-date
comment above them (golangci/golangci-lint:v2.7.2 (2026-08-09),
golang:1.25-alpine (2026-02-27), alpine:3.23 (2026-02-27)). The two
carried-over pins keep their original digests and dates, which is correct.
No unpinned or mutable reference anywhere in the diff.

PRIORITY 2 — functional claims

Uncached build. docker build --no-cache -f Dockerfile.backend --build-arg VERSION=1.2.3-rev40 .
exit 0 in 43.9 s, comfortably inside the 5-minute budget. CACHED
appears 3 times and all three are non-executing steps: the two base FROM
resolutions (#7, #9) and one WORKDIR metadata step (#8). Zero cached
RUN layers.
Every RUN produced real output — 0 issues. from the linter
(13.2 s), per-package ok/[no test files] from go test with no
(cached) markers
, and the fully expanded build line:

#24 [builder 9/9] RUN VERSION="1.2.3-rev40" make build
#24 0.182 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=1.2.3-rev40 -X main.Buildarch=x86_64" \
#24 0.182 	-o ./netwatch-server ./cmd/netwatch-server/

CI is green on bd2bc9f (check / check (push), 16 s) but per #37 that is
not offered as evidence and nothing here rests on it.

Static binary, and it runs. Inside the alpine runtime stage:
ldd /usr/local/bin/netwatch-serverNot a valid dynamic program
(exit 1). The ELF header is e_type = 2 (ET_EXEC), not ET_DYN. Ran the
image with --rm on a loopback-bound port:

GET /.well-known/healthcheck -> 200
{"appname":"netwatch-server","status":"ok",...,"version":"1.2.3-rev40"}

--build-arg VERSION reaches the binary. Confirmed by the 1.2.3-rev40
above — that string was supplied only as a build arg and came back out of the
running container.

No .git needed. Exported both trees with git archive into contexts
containing no .git at all:

Dockerfile result
this branch exit 0
main exit 1: failed to compute cache key ... "/.git": not found

At the Makefile level, in a tree with no .git in it or any parent:

result
this branch -ldflags "-s -w -X main.Version=dev ...", exit 0, no stderr noise
main fatal: not a git repository, then -X main.Version=empty, exit 0

The fix is real and main genuinely fails the same test.

Gates. cd backend && make check → exit 0 (0 issues.). Root
make check → exit 0 (after make bootstrap; the first attempt failed
only because the fresh clone had no node_modules, which is not attributable
to this change). prettier --check clean, so make fmt is clean on the
touched markdown.

Lint gate, re-derived (both directions). Not taken on trust from the
aborted review. Added a lint-only defect (a 188-character comment line in a new
internal/server file — compiles fine, so only the linter can object):

  • with COPY --from=lint /src/go.sum /dev/null: build fails, exit 1,
    at [lint 7/7] RUN make lint with ... (lll). Grep for
    [builder ...] RUN make test in the log returns 0 — compilation and
    tests never started.
  • without that single line, same broken tree: build succeeds, exit 0,
    and [lint ...] RUN make lint appears 0 times — BuildKit never runs the
    stage.

Independently reproduced. The manager note's recorded finding stands.

PRIORITY 3 — the two deviations

1. RUN VERSION="${VERSION}" make build instead of an inline go build.
Accepted. The expansion in the build log (quoted above) is byte-for-byte the
flag set the issue mandates — CGO_ENABLED=0, -trimpath, -s -w,
-X main.Version=${VERSION} — plus the pre-existing -X main.Buildarch, which
an inline copy would have silently dropped (an application-behaviour change the
issue forbids). It also honours REPO_POLICIES.md's "always use Makefile
targets instead of invoking the underlying tools directly" and keeps one
definition of the build command. The environment form rather than
make build VERSION=... is the right call and is load-bearing for the #38
merge (see below).

2. build is no longer an incremental file target. The stated
justification is verified against main:

$ make build VERSION=aaa      # builds, binary stamped aaa
$ make build VERSION=bbb
make: Nothing to be done for 'build'.

main's file rule does not list VERSION as a prerequisite, so a version
change is a silent no-op that ships a binary stamped with the previous
version. Making build phony is a correctness fix, not a convenience. The
no-op rebuild cost is ~0.1 s via the Go build cache.

PRIORITY 4 — reconciliation notes for the unmerged PRs

#38 does not touch Dockerfile.backend. Verified against its changed-file
list at 1c16d50: 30 files, including the frontend Dockerfile, but
Dockerfile.backend is not among them. The note is correct.

The named trap is real, and it is silent. backend/script/build at
1c16d50 reads:

version="$(git describe --always --dirty 2>/dev/null || echo unknown)"

It never consults the environment, so an inherited VERSION is ignored. In
this branch's builder there is no .git and no git binary (only make is
installed), so git describe fails, stderr is discarded, echo unknown
succeeds, and the script exits 0 with every image stamped unknown. Nothing
fails, nothing is logged. The PR body's mitigation
(version="${VERSION:-$(git describe ... || echo dev)}") is the right fix and
must be applied in whichever merge lands second.

The -linkmode claim is also correct, and that one fails loudly. The same
script still carries:

if [ "$(uname -s)" != "Darwin" ]; then
    ldflags="-linkmode external -extldflags -static $ldflags"
fi

Against a builder with no gcc/musl-dev that cannot link, so it breaks the
build rather than degrading quietly.

#31 spot-check. Its file list at 4d70317 does include Dockerfile.backend
(+2/-2) and backend/Makefile (+18/-0), consistent with the described
conflict. The backend/Makefile side is purely additive, which supports the
prediction that git will likely auto-merge it. The instruction to re-resolve
and re-verify the v2.12.2 digest at merge time rather than trusting the quoted
one is the correct posture and should be followed literally.

PRIORITY 5 — hygiene

  • Exactly one commit on the branch. Title:
    build: Dockerfile.backend multistage lint stage (closes #17) — ends with
    the required (closes #17).
  • Diff is exactly three files: Dockerfile.backend, backend/Makefile,
    TODO.md (+46/-20). No scope creep.
  • TODO.md is one additive bullet at the top of Completed Steps, in the same
    commit.
  • Cleanly mergeable: test-merged bd2bc9f onto current origin/main
    (fbfe1df) — no conflicts.
  • Build time 43.9 s uncached, well under 5 minutes.
  • gomodguard was not fixed drive-bybackend/.golangci.yml is untouched
    by this diff; #41 carries it. Correct.
  • No Claude/Anthropic references and no attribution trailers anywhere in
    the diff or the commit message. Grep over fbfe1df..bd2bc9f and over the
    full commit body: clean.
  • Naming, comment style and idiom match the surrounding files. No
    non-inclusive terminology.
  • #36 backend half: the claim is accurate. The backend image builds from a
    context with no .git, so adding .git to .dockerignore will not affect
    it. #36 remains blocked on the frontend: vite.config.js lines 5-6 call
    execSync("git rev-parse --short HEAD") and execSync("git rev-parse HEAD")
    at config-eval time.

Findings

No blocking defects. Three non-blocking items, none of which should hold up the
merge:

  1. Minor — commit message contradicts the code on a deliberate design point.
    The commit body says the version is "passed to the build via
    make build VERSION=..."
    , but the Dockerfile uses the environment form
    RUN VERSION="${VERSION}" make build. The PR body argues at length that the
    environment form is specifically chosen so it survives #38 turning build
    into a shim — i.e. this is load-bearing, not incidental. The commit message
    is the durable record and currently describes the form that was rejected.
    Acceptable would be VERSION=... make build in that bullet.

  2. Minor — an explicitly empty build arg still stamps an empty version
    silently.
    Verified:
    docker build --build-arg VERSION= -f Dockerfile.backend . produces
    RUN VERSION="" make build and then
    -ldflags "-s -w -X main.Version= -X main.Buildarch=x86_64". GNU make treats
    an environment variable that is defined-but-empty as defined, so VERSION ?=
    does not fall back to the git describe/dev shell. This is the same
    empty-version failure mode the PR removes for the missing-.git case,
    reached through a different door, and it requires someone to explicitly pass
    an empty build arg — so it is a robustness nit, not a defect against #17's
    definition of done. Acceptable would be a non-empty guard, e.g. an
    $(if $(VERSION),...) form or a test -n assertion in the recipe.

  3. Informational — GOLDFLAGS += inherits any GOLDFLAGS already in the
    environment.
    Pre-existing on main, not introduced here, and not worth a
    change in this PR.

Housekeeping

Every image I created was removed. Four images from the earlier aborted review
are still on the host and were not created by this run — I left them alone
rather than deleting another session's artifacts: rev40-nogit-head:test,
rev40-gate-b:test, rev40-pr40-head:test, rev40-pr40-ver:test. No
containers left running. No BuildKit cache pruned.

Coverage statement

Completed: Priorities 1, 2, 3, 4 and 5 in full. Nothing was left unverified
except the future-state #31 v2.12.2 digest, which the PR body itself instructs
the next merger to re-resolve rather than trust — I did not resolve it, and
that is correct scope for this PR.

## Independent review — head `bd2bc9f`, base `main` at `fbfe1df` ### Verdict: PASS Priorities 1 through 5 were all reached. Every functional claim in the PR body was re-derived independently in a fresh scratch clone (not a worktree, per \#33). No BuildKit cache was pruned; the one uncached build used `--no-cache` scoped to that single build. All test images I created have been removed. --- ## PRIORITY 1 — lint image digest (the highest-value unverified item) **Confirmed.** Pulled and executed the exact digest in the Dockerfile: ``` $ docker run --rm golangci/golangci-lint@sha256:5d6d5c70a61f1356adfd9dd6316ce286799fefc9d743421356ff1b00842368ba golangci-lint --version golangci-lint has version 2.7.2 built with go1.25.4 from 9f61b0f5 on 2025-12-07T16:57:12Z ``` `9f61b0f5` is the prefix of `9f61b0f53f80672872fced07b6874397c3ed197b`, which is exactly what `main`'s `Dockerfile.backend` pins on its `go install github.com/golangci/golangci-lint/v2/...` line. The lint stage therefore enforces the same linter build CI has been enforcing. No silent ruleset change. Also confirmed by building `--target lint` and inspecting the stage: `/src/.golangci.yml` (739 bytes) is present, so the repo's config — not golangci-lint defaults — is what runs. `go`, `gofmt` (`/usr/local/go/bin/gofmt`) and `make` (`/usr/bin/make`) all exist in the image, so the stage genuinely installs nothing. I also checked that `make fmt-check` is not vacuous in that image (a missing `gofmt` would make `test -z "$(gofmt -l .)"` pass silently). Injected a misformatted file into the lint stage: `make fmt-check` printed `Files not formatted: internal/server/revbadfmt.go` and exited 2. Real gate. **Pinning.** All three `FROM` lines are `@sha256:` with a version-and-date comment above them (`golangci/golangci-lint:v2.7.2 (2026-08-09)`, `golang:1.25-alpine (2026-02-27)`, `alpine:3.23 (2026-02-27)`). The two carried-over pins keep their original digests and dates, which is correct. No unpinned or mutable reference anywhere in the diff. ## PRIORITY 2 — functional claims **Uncached build.** `docker build --no-cache -f Dockerfile.backend --build-arg VERSION=1.2.3-rev40 .` → **exit 0 in 43.9 s**, comfortably inside the 5-minute budget. `CACHED` appears 3 times and all three are non-executing steps: the two base `FROM` resolutions (`#7`, `#9`) and one `WORKDIR` metadata step (`#8`). **Zero cached `RUN` layers.** Every `RUN` produced real output — `0 issues.` from the linter (13.2 s), per-package `ok`/`[no test files]` from `go test` with **no `(cached)` markers**, and the fully expanded build line: ``` #24 [builder 9/9] RUN VERSION="1.2.3-rev40" make build #24 0.182 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=1.2.3-rev40 -X main.Buildarch=x86_64" \ #24 0.182 -o ./netwatch-server ./cmd/netwatch-server/ ``` CI is green on `bd2bc9f` (`check / check (push)`, 16 s) but per \#37 that is not offered as evidence and nothing here rests on it. **Static binary, and it runs.** Inside the alpine runtime stage: `ldd /usr/local/bin/netwatch-server` → `Not a valid dynamic program` (exit 1). The ELF header is `e_type = 2` (`ET_EXEC`), not `ET_DYN`. Ran the image with `--rm` on a loopback-bound port: ``` GET /.well-known/healthcheck -> 200 {"appname":"netwatch-server","status":"ok",...,"version":"1.2.3-rev40"} ``` **`--build-arg VERSION` reaches the binary.** Confirmed by the `1.2.3-rev40` above — that string was supplied only as a build arg and came back out of the running container. **No `.git` needed.** Exported both trees with `git archive` into contexts containing no `.git` at all: | Dockerfile | result | | --- | --- | | this branch | **exit 0** | | `main` | **exit 1**: `failed to compute cache key ... "/.git": not found` | At the Makefile level, in a tree with no `.git` in it or any parent: | | result | | --- | --- | | this branch | `-ldflags "-s -w -X main.Version=dev ..."`, exit 0, no stderr noise | | `main` | `fatal: not a git repository`, then `-X main.Version=` — **empty**, exit 0 | The fix is real and `main` genuinely fails the same test. **Gates.** `cd backend && make check` → exit 0 (`0 issues.`). Root `make check` → exit 0 (after `make bootstrap`; the first attempt failed only because the fresh clone had no `node_modules`, which is not attributable to this change). `prettier --check` clean, so `make fmt` is clean on the touched markdown. **Lint gate, re-derived (both directions).** Not taken on trust from the aborted review. Added a lint-only defect (a 188-character comment line in a new `internal/server` file — compiles fine, so only the linter can object): - **with** `COPY --from=lint /src/go.sum /dev/null`: build **fails**, exit 1, at `[lint 7/7] RUN make lint` with `... (lll)`. Grep for `[builder ...] RUN make test` in the log returns **0** — compilation and tests never started. - **without** that single line, same broken tree: build **succeeds**, exit 0, and `[lint ...] RUN make lint` appears **0** times — BuildKit never runs the stage. Independently reproduced. The manager note's recorded finding stands. ## PRIORITY 3 — the two deviations **1. `RUN VERSION="${VERSION}" make build` instead of an inline `go build`.** Accepted. The expansion in the build log (quoted above) is byte-for-byte the flag set the issue mandates — `CGO_ENABLED=0`, `-trimpath`, `-s -w`, `-X main.Version=${VERSION}` — plus the pre-existing `-X main.Buildarch`, which an inline copy would have silently dropped (an application-behaviour change the issue forbids). It also honours `REPO_POLICIES.md`'s "always use Makefile targets instead of invoking the underlying tools directly" and keeps one definition of the build command. The environment form rather than `make build VERSION=...` is the right call and is load-bearing for the \#38 merge (see below). **2. `build` is no longer an incremental file target.** The stated justification is **verified against `main`**: ``` $ make build VERSION=aaa # builds, binary stamped aaa $ make build VERSION=bbb make: Nothing to be done for 'build'. ``` `main`'s file rule does not list `VERSION` as a prerequisite, so a version change is a silent no-op that ships a binary stamped with the *previous* version. Making `build` phony is a correctness fix, not a convenience. The no-op rebuild cost is ~0.1 s via the Go build cache. ## PRIORITY 4 — reconciliation notes for the unmerged PRs **\#38 does not touch `Dockerfile.backend`.** Verified against its changed-file list at `1c16d50`: 30 files, including the frontend `Dockerfile`, but `Dockerfile.backend` is not among them. The note is correct. **The named trap is real, and it is silent.** `backend/script/build` at `1c16d50` reads: ```sh version="$(git describe --always --dirty 2>/dev/null || echo unknown)" ``` It never consults the environment, so an inherited `VERSION` is ignored. In this branch's builder there is no `.git` **and** no `git` binary (only `make` is installed), so `git describe` fails, stderr is discarded, `echo unknown` succeeds, and the script exits 0 with every image stamped `unknown`. Nothing fails, nothing is logged. The PR body's mitigation (`version="${VERSION:-$(git describe ... || echo dev)}"`) is the right fix and must be applied in whichever merge lands second. **The `-linkmode` claim is also correct, and that one fails loudly.** The same script still carries: ```sh if [ "$(uname -s)" != "Darwin" ]; then ldflags="-linkmode external -extldflags -static $ldflags" fi ``` Against a builder with no `gcc`/`musl-dev` that cannot link, so it breaks the build rather than degrading quietly. **\#31 spot-check.** Its file list at `4d70317` does include `Dockerfile.backend` (+2/-2) and `backend/Makefile` (+18/-0), consistent with the described conflict. The `backend/Makefile` side is purely additive, which supports the prediction that git will likely auto-merge it. The instruction to re-resolve and re-verify the v2.12.2 digest at merge time rather than trusting the quoted one is the correct posture and should be followed literally. ## PRIORITY 5 — hygiene - **Exactly one commit** on the branch. Title: `build: Dockerfile.backend multistage lint stage (closes #17)` — ends with the required ` (closes #17)`. - **Diff is exactly three files**: `Dockerfile.backend`, `backend/Makefile`, `TODO.md` (+46/-20). No scope creep. - `TODO.md` is one additive bullet at the top of Completed Steps, in the same commit. - **Cleanly mergeable**: test-merged `bd2bc9f` onto current `origin/main` (`fbfe1df`) — no conflicts. - **Build time** 43.9 s uncached, well under 5 minutes. - **gomodguard was not fixed drive-by** — `backend/.golangci.yml` is untouched by this diff; \#41 carries it. Correct. - **No Claude/Anthropic references and no attribution trailers** anywhere in the diff or the commit message. Grep over `fbfe1df..bd2bc9f` and over the full commit body: clean. - Naming, comment style and idiom match the surrounding files. No non-inclusive terminology. - **\#36 backend half: the claim is accurate.** The backend image builds from a context with no `.git`, so adding `.git` to `.dockerignore` will not affect it. \#36 remains blocked on the frontend: `vite.config.js` lines 5-6 call `execSync("git rev-parse --short HEAD")` and `execSync("git rev-parse HEAD")` at config-eval time. --- ## Findings No blocking defects. Three non-blocking items, none of which should hold up the merge: 1. **Minor — commit message contradicts the code on a deliberate design point.** The commit body says the version is *"passed to the build via `make build VERSION=...`"*, but the Dockerfile uses the environment form `RUN VERSION="${VERSION}" make build`. The PR body argues at length that the environment form is specifically chosen so it survives \#38 turning `build` into a shim — i.e. this is load-bearing, not incidental. The commit message is the durable record and currently describes the form that was rejected. Acceptable would be `VERSION=... make build` in that bullet. 2. **Minor — an explicitly empty build arg still stamps an empty version silently.** Verified: `docker build --build-arg VERSION= -f Dockerfile.backend .` produces `RUN VERSION="" make build` and then `-ldflags "-s -w -X main.Version= -X main.Buildarch=x86_64"`. GNU make treats an environment variable that is defined-but-empty as defined, so `VERSION ?=` does not fall back to the `git describe`/`dev` shell. This is the same empty-version failure mode the PR removes for the missing-`.git` case, reached through a different door, and it requires someone to explicitly pass an empty build arg — so it is a robustness nit, not a defect against \#17's definition of done. Acceptable would be a non-empty guard, e.g. an `$(if $(VERSION),...)` form or a `test -n` assertion in the recipe. 3. **Informational — `GOLDFLAGS +=` inherits any `GOLDFLAGS` already in the environment.** Pre-existing on `main`, not introduced here, and not worth a change in this PR. ## Housekeeping Every image I created was removed. Four images from the earlier aborted review are still on the host and were **not** created by this run — I left them alone rather than deleting another session's artifacts: `rev40-nogit-head:test`, `rev40-gate-b:test`, `rev40-pr40-head:test`, `rev40-pr40-ver:test`. No containers left running. No BuildKit cache pruned. ## Coverage statement Completed: Priorities 1, 2, 3, 4 and 5 in full. Nothing was left unverified except the future-state \#31 v2.12.2 digest, which the PR body itself instructs the next merger to re-resolve rather than trust — I did not resolve it, and that is correct scope for this PR.
clawbot added merge-ready and removed needs-review labels 2026-08-09 16:14:00 +02:00
clawbot removed their assignment 2026-08-09 16:14:06 +02:00
sneak was assigned by clawbot 2026-08-09 16:14:06 +02:00
Author
Collaborator

Manager note — merge-ready

The relaunched review completed all five priorities and returned PASS with no blocking findings. Relabelled merge-ready and assigned to @sneak. This supersedes my earlier note recording the aborted review.

The item that most needed checking came back clean

Nobody but the author had verified that the digest-pinned lint image is the linter it claims to be. If it were not, this PR would have silently swapped the ruleset CI enforces — the worst realistic outcome here, and invisible.

$ docker run --rm golangci/golangci-lint@sha256:5d6d5c70…368ba golangci-lint --version
golangci-lint has version 2.7.2 built with go1.25.4 from 9f61b0f5

9f61b0f5 is the prefix of 9f61b0f53f80672872fced07b6874397c3ed197b — exactly what main pins via go install. Same linter, same ruleset, different delivery mechanism.

The reviewer went one better and checked the lint stage is not vacuous: injecting a misformatted file made make fmt-check print Files not formatted: and exit 2 inside the stage. Given this repo's history of gates that pass without checking anything, that was the right instinct.

Everything else verified

  • Uncached build 43.9s, three CACHED lines all non-executing, zero cached RUN layers, no (cached) in go test.
  • Runtime binary is ET_EXEC, lddNot a valid dynamic program, container returns 200 with "version":"1.2.3-rev40" supplied only via --build-arg.
  • No-.git context: this branch exit 0 stamping dev; main exit 1 on "/.git": not found, and at Makefile level main stamps an empty version after a fatal:.
  • Lint gate re-derived in both directions rather than inherited from the aborted run.
  • Deviation 2 confirmed against main: make build VERSION=bbb after VERSION=aaa prints Nothing to be done for 'build'. — a silent no-op shipping a stale version stamp. Making build phony is a correctness fix, not a preference.

Three non-blocking findings

  1. The commit message describes the rejected form. It says version is "passed to the build via make build VERSION=…", but the Dockerfile uses the environment form RUN VERSION="${VERSION}" make build. The PR body argues correctly that the environment form is load-bearing for the #38 merge — it is what lets build become a shim without editing the Dockerfile — so the durable record contradicts the reasoning. I am accepting it rather than forcing an amend: amending the message would change the head SHA and invalidate a review that just completed, to fix a nit whose correct version is already documented in two places. Recording the correction here so it is durable: the Dockerfile passes VERSION through the environment, deliberately.
  2. --build-arg VERSION= (explicitly empty) yields -X main.Version=. VERSION ?= does not fall back, because make treats a defined-but-empty environment variable as defined. Requires deliberately passing an empty arg. Noted on #39, since whoever does the #38 reconciliation will be in exactly that code.
  3. Informational: GOLDFLAGS += inherits an environment GOLDFLAGS. Pre-existing on main, not introduced here.

Merge order — this PR moves ahead of #31

#35#40#31#38.

This PR deletes the RUN CGO_ENABLED=0 go install … line that #31 edits. Landing #40 first turns #31's Dockerfile change into a two-line lint-stage digest swap; landing #31 first leaves it rebasing a hunk against a line that no longer exists. Same destination, less friction.

The reconciliation notes in the PR body were spot-checked and are accurate, including the trap: #38's backend/script/build hardcodes git describe … || echo unknown, ignores the environment, and exits 0 — so if it is not taught to honour an inherited VERSION, every image silently stamps unknown with nothing failing. Its -linkmode external -extldflags -static branch is also still present and would fail loudly against the gcc-less builder this PR creates. Both are documented.

Housekeeping

No BuildKit cache pruned; --no-cache scoped to single builds. The reviewer removed all seven images it created. Four images from the aborted earlier review remain (rev40-nogit-head:test, rev40-gate-b:test, rev40-pr40-head:test, rev40-pr40-ver:test) — correctly left alone rather than deleted, since another session's artifacts are not ours to remove. Harmless; worth a sweep if disk matters.

## Manager note — merge-ready The relaunched review completed all five priorities and returned **PASS** with no blocking findings. Relabelled `merge-ready` and assigned to @sneak. This supersedes my earlier note recording the aborted review. ### The item that most needed checking came back clean Nobody but the author had verified that the digest-pinned lint image is the linter it claims to be. If it were not, this PR would have silently swapped the ruleset CI enforces — the worst realistic outcome here, and invisible. ``` $ docker run --rm golangci/golangci-lint@sha256:5d6d5c70…368ba golangci-lint --version golangci-lint has version 2.7.2 built with go1.25.4 from 9f61b0f5 ``` `9f61b0f5` is the prefix of `9f61b0f53f80672872fced07b6874397c3ed197b` — exactly what `main` pins via `go install`. Same linter, same ruleset, different delivery mechanism. The reviewer went one better and checked the lint stage is not *vacuous*: injecting a misformatted file made `make fmt-check` print `Files not formatted:` and exit 2 inside the stage. Given this repo's history of gates that pass without checking anything, that was the right instinct. ### Everything else verified - Uncached build 43.9s, three `CACHED` lines all non-executing, **zero cached `RUN` layers**, no `(cached)` in `go test`. - Runtime binary is `ET_EXEC`, `ldd` → `Not a valid dynamic program`, container returns **200** with `"version":"1.2.3-rev40"` supplied only via `--build-arg`. - No-`.git` context: this branch exit 0 stamping `dev`; `main` exit 1 on `"/.git": not found`, and at Makefile level `main` stamps an **empty** version after a `fatal:`. - Lint gate re-derived in both directions rather than inherited from the aborted run. - Deviation 2 confirmed against `main`: `make build VERSION=bbb` after `VERSION=aaa` prints `Nothing to be done for 'build'.` — a silent no-op shipping a stale version stamp. Making `build` phony is a correctness fix, not a preference. ### Three non-blocking findings 1. **The commit message describes the rejected form.** It says version is "passed to the build via `make build VERSION=…`", but the Dockerfile uses the environment form `RUN VERSION="${VERSION}" make build`. The PR body argues correctly that the environment form is load-bearing for the #38 merge — it is what lets `build` become a shim without editing the Dockerfile — so the *durable* record contradicts the reasoning. I am accepting it rather than forcing an amend: amending the message would change the head SHA and invalidate a review that just completed, to fix a nit whose correct version is already documented in two places. **Recording the correction here so it is durable: the Dockerfile passes `VERSION` through the environment, deliberately.** 2. **`--build-arg VERSION=` (explicitly empty) yields `-X main.Version=`.** `VERSION ?=` does not fall back, because make treats a defined-but-empty environment variable as defined. Requires deliberately passing an empty arg. Noted on #39, since whoever does the #38 reconciliation will be in exactly that code. 3. **Informational**: `GOLDFLAGS +=` inherits an environment `GOLDFLAGS`. Pre-existing on `main`, not introduced here. ### Merge order — this PR moves ahead of #31 **#35 → #40 → #31 → #38.** This PR deletes the `RUN CGO_ENABLED=0 go install …` line that #31 edits. Landing #40 first turns #31's Dockerfile change into a two-line lint-stage digest swap; landing #31 first leaves it rebasing a hunk against a line that no longer exists. Same destination, less friction. The reconciliation notes in the PR body were spot-checked and are accurate, including the trap: #38's `backend/script/build` hardcodes `git describe … || echo unknown`, ignores the environment, and exits 0 — so if it is not taught to honour an inherited `VERSION`, every image silently stamps `unknown` with nothing failing. Its `-linkmode external -extldflags -static` branch is also still present and would fail loudly against the gcc-less builder this PR creates. Both are documented. ### Housekeeping No BuildKit cache pruned; `--no-cache` scoped to single builds. The reviewer removed all seven images it created. Four images from the **aborted** earlier review remain (`rev40-nogit-head:test`, `rev40-gate-b:test`, `rev40-pr40-head:test`, `rev40-pr40-ver:test`) — correctly left alone rather than deleted, since another session's artifacts are not ours to remove. Harmless; worth a sweep if disk matters.
All checks were successful
check / check (push) Successful in 16s
Required
Details
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/backend-dockerfile-lint-stage:feat/backend-dockerfile-lint-stage
git checkout feat/backend-dockerfile-lint-stage
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#40