From ce241a1b2bdfa470f9cf44b81512eb9ba15ff6a6 Mon Sep 17 00:00:00 2001 From: cjimti Date: Sun, 10 May 2026 23:39:12 -0700 Subject: [PATCH 1/2] guard against stale SPA embed dir at verify and build time ui-verify now diffs internal/ui/dist/ against a fresh ui/dist/ build and fails if they differ (excluding .gitkeep). build gains a check-ui-embed prerequisite that refuses to compile when the embed dir is empty. Closes a class of bug where `make verify` passes on current source but the binary embeds a stale SPA bundle, silently shipping the previous UI to operators. --- Makefile | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 7ce6c97..d468657 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ CODEQL_RESULT := $(BUILD_DIR)/codeql-results.sarif .PHONY: all build build-all test test-short bench fmt fmt-check vet tidy \ mod-tidy-check mod-verify clean help dev-secrets \ - ui ui-dev ui-clean ui-verify embed-clean \ + ui ui-dev ui-clean ui-verify check-ui-embed embed-clean \ lint security gosec govulncheck semgrep \ coverage coverage-gate coverage-report \ integration codeql require-docker require-codeql require-semgrep require-jq require-node \ @@ -67,12 +67,34 @@ CODEQL_RESULT := $(BUILD_DIR)/codeql-results.sarif all: build test lint ## build: Build the binary into ./bin/api-test -build: +## Refuses to build when the SPA embed dir is empty — the Go +## //go:embed directive would silently produce a binary that +## serves a JSON stub instead of the portal. Run `make ui` first +## (or `make all` which chains them in order). +build: check-ui-embed @echo "Building $(BINARY_NAME)..." @mkdir -p $(BUILD_DIR) $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_DIR) @echo "Binary built: $(BUILD_DIR)/$(BINARY_NAME)" +## check-ui-embed: Verify $(UI_EMBED_DIR) has a built SPA. Used as a +## build-time prerequisite so a freshly cloned tree +## (only $(UI_EMBED_DIR)/.gitkeep present) doesn't +## produce a UI-less binary by accident. +check-ui-embed: + @if [ ! -f $(UI_EMBED_DIR)/index.html ]; then \ + echo ""; \ + echo "FAIL: $(UI_EMBED_DIR)/index.html missing."; \ + echo " The Go binary embeds $(UI_EMBED_DIR) via //go:embed."; \ + echo " Without a built SPA there, the portal serves a JSON"; \ + echo " stub instead of the React UI. Run:"; \ + echo ""; \ + echo " make ui"; \ + echo ""; \ + echo " and re-run `make build`."; \ + exit 1; \ + fi + ## build-all: go build -v ./... (mirrors CI's Build job; catches build paths ## that `go test` would skip — packages without tests, etc.) build-all: @@ -86,14 +108,40 @@ ui: require-node cd $(UI_DIR) && pnpm install --frozen-lockfile && pnpm build @rm -rf $(UI_EMBED_DIR) @cp -R $(UI_DIR)/dist $(UI_EMBED_DIR) + @# Re-add the .gitkeep so the directory stays tracked in git even + @# though the bundle itself is gitignored. Fresh clones rely on the + @# .gitkeep so the //go:embed directive in internal/ui/embed.go has + @# a directory to point at before `make ui` runs. + @touch $(UI_EMBED_DIR)/.gitkeep @echo "UI built and copied to $(UI_EMBED_DIR)." -## ui-verify: TypeScript + Vite build of the SPA without copying to the -## embed dir. Mirrored from CI's frontend job — catches type -## errors and broken imports without rebuilding the Go binary. +## ui-verify: TypeScript + Vite build of the SPA, then fail if the +## embedded copy in $(UI_EMBED_DIR) does not match the fresh +## build under $(UI_DIR)/dist. The Go binary embeds the latter +## via //go:embed, so a stale embed silently ships old UI +## even when source is current. This gate makes that failure +## mode loud (same posture as fmt-check / mod-tidy-check). +## To fix a failure: run `make ui` and commit $(UI_EMBED_DIR). ui-verify: require-node @echo "Verifying UI (typecheck + build)..." cd $(UI_DIR) && pnpm install --frozen-lockfile && pnpm build + @echo "Checking embedded SPA bundle is in sync with fresh build..." + @# .gitkeep lives in the embed dir only (to keep the gitignored + @# directory tracked); exclude it from the comparison. + @if ! diff -r --brief --exclude=.gitkeep $(UI_DIR)/dist $(UI_EMBED_DIR) > /dev/null 2>&1; then \ + echo ""; \ + echo "FAIL: $(UI_EMBED_DIR) is stale relative to $(UI_DIR)/dist."; \ + echo " The Go binary embeds $(UI_EMBED_DIR); without refreshing"; \ + echo " it, the binary ships an outdated SPA even when source is"; \ + echo " current. Run:"; \ + echo ""; \ + echo " make ui"; \ + echo " git add $(UI_EMBED_DIR)"; \ + echo ""; \ + echo " and commit before re-running verify."; \ + exit 1; \ + fi + @echo "Embedded SPA bundle is in sync." ## ui-dev: Run Vite dev server (proxies /api to localhost:8080). ui-dev: From 14ba9f5199281ae504681101551604a250d5e1e1 Mon Sep 17 00:00:00 2001 From: cjimti Date: Mon, 11 May 2026 09:56:03 -0700 Subject: [PATCH 2/2] codeql: switch from autobuild to manual go build to skip make gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL's autobuild step runs the default make target on a fresh checkout. With this PR's new build: check-ui-embed prerequisite, that fails because internal/ui/dist/ is gitignored except for the .gitkeep and `make ui` hasn't run — exactly the failure mode the gate is designed to surface to developers. CodeQL doesn't need a working portal; it needs Go to compile for type tracing. Switched to build-mode: manual + `go build -v ./...`, matching the CI workflow's Build job (.github/workflows/ci.yml:114). CodeQL no longer touches Make, so future Makefile prerequisite changes won't silently break the security scan. Observed failure on PR #6's CodeQL run: Trying build command make [] FAIL: ./internal/ui/dist/index.html missing. The Go binary embeds ./internal/ui/dist via //go:embed. Without a built SPA there, the portal serves a JSON stub instead of the React UI. Run: make ui /bin/bash: fork: retry: Resource temporarily unavailable The fork: retry line is the runner exhausting itself retrying past the 30-minute timeout — not a flake. --- .github/workflows/codeql.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3cbc485..ac8ba58 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,13 +41,23 @@ jobs: uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: go + # Manual build mode. Autobuild runs `make` (default target = + # `all` = `build test lint`), which on a fresh CI checkout + # tripped the make build → check-ui-embed gate because + # internal/ui/dist/ is gitignored except for .gitkeep and + # `make ui` hasn't run. CodeQL only needs Go to compile for + # type tracing; it doesn't need a working portal. Building + # via `go build ./...` matches what the CI Build job does + # (ci.yml step "Build") and decouples CodeQL from any + # future Makefile prerequisite changes. + build-mode: manual # security-and-quality bundles the security pack with style / # correctness rules. Project-specific query exclusions live # in the config-file; findings post to the repo's Security tab. config-file: ./.github/codeql/codeql-config.yml - - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + - name: Build + run: go build -v ./... - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2