Ctrl K
ComunidadGuías…

Makefile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Zoomies
#
# The Go binary is fully usable without Node. Node is a build-time dependency
# only, wrapped by `make ui`, and the built assets are embedded with go:embed.

SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help

# Without the leading v: the tag is v1.2.3, the version the binary reports is
# 1.2.3, and the release workflow stamps the images the same way.
VERSION ?= $(patsubst v%,%,$(shell git describe --tags --always --dirty 2>/dev/null || echo dev))
COMMIT  ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE    ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

PKG      := github.com/eyupio/zoomies
LDFLAGS  := -s -w \
	-X $(PKG)/internal/version.Version=$(VERSION) \
	-X $(PKG)/internal/version.Commit=$(COMMIT) \
	-X $(PKG)/internal/version.Date=$(DATE)

BIN      := zoomies
DIST     := dist
UI_DIR   := web
UI_OUT   := internal/api/webdist
RUNNER_VERSION ?= 2.338.2

GO       ?= go
NPM      ?= npm
# Package-wide budget, including race instrumentation and SQLite migrations.
# Sized for the fleet runner's one-CPU share with -coverpkg, where the API
# package alone has taken 38 minutes and the controller package is larger:
# a package that hits this limit is hung, not slow.
GO_TEST_TIMEOUT ?= 120m

# The packages a test target runs. CI splits the suite by package across
# runners -- the controller, API and store packages are each a large share of
# the whole -- and this is how a shard says which share it is. Local runs
# leave it alone and get everything.
TEST_PKGS ?= ./...
# A -run regexp, for CI to run half of one package on each runner.
TEST_RUN ?=

# What `make bench` runs: every package that has a benchmark, found rather than
# listed so that a new one is not left out of a list that went stale, six runs
# of each so that benchstat can say how far apart they were, with the
# allocations beside the times. BENCH narrows it by benchmark name (a -bench
# regexp, matched a slash-separated part at a time, so BENCH=Decide/quiet works;
# leave out any $, which make would read as one of its own), BENCH_PKGS by
# package, and BENCH_COUNT sets the runs. go test runs the packages of a -bench
# run one at a time, because benchmarks that share the CPU contaminate each
# other; leave the machine alone while it works.
BENCH       ?= .
BENCH_COUNT ?= 6
BENCH_PKGS  ?= $(sort $(patsubst %/,./%,$(dir $(shell grep -rl --include='*_test.go' '^func Benchmark' cmd internal))))

##@ Build

.PHONY: build
build: ui ## Build the binary with the UI embedded
	CGO_ENABLED=0 $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN) ./cmd/zoomies

.PHONY: build-nogui
build-nogui: $(UI_OUT)/.placeholder ## Build without rebuilding the UI (fast inner loop)
	CGO_ENABLED=0 $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN) ./cmd/zoomies

# The placeholder alone, with nothing compiled: what CI needs before go vet,
# go test or a scanner can load internal/api. Building the binary to get it
# cost a cold compile of every dependency on an ephemeral runner, minutes of
# work in a step that only has to write one file.
.PHONY: ui-stub
ui-stub: $(UI_OUT)/.placeholder ## Write the placeholder UI that go:embed needs, and build nothing

$(UI_OUT)/.placeholder:
	@mkdir -p $(UI_OUT)
	@# go:embed fails on an empty directory, so keep a placeholder for builds
	@# that skip the UI. `make ui` overwrites the directory wholesale, and with
	@# it this marker, so the marker's absence does not mean the UI is absent:
	@# write the page only when there is none, or a test run after a real build
	@# would replace the built UI with the placeholder.
	@test -e $(UI_OUT)/index.html || printf '<!doctype html><title>Zoomies</title><p>The UI was not built. Run <code>make ui</code>.' > $(UI_OUT)/index.html
	@touch $@

.PHONY: ui
ui: ## Build the Svelte UI into the embed directory
	cd $(UI_DIR) && $(NPM) ci --no-audit --no-fund
	cd $(UI_DIR) && $(NPM) run build

.PHONY: ui-dev
ui-dev: | $(UI_DIR)/node_modules ## Run the Vite dev server against a controller on :8080
	cd $(UI_DIR) && $(NPM) run dev

.PHONY: dist
dist: ui ## Cross-compile release binaries into dist/
	@mkdir -p $(DIST)
	@for platform in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do \
		os=$${platform%/*}; arch=$${platform#*/}; \
		case $$os in windows) ext=.exe ;; *) ext= ;; esac; \
		echo "  building $$os/$$arch"; \
		CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -trimpath \
			-ldflags "$(LDFLAGS)" -o $(DIST)/$(BIN)_$${os}_$${arch}$$ext ./cmd/zoomies; \
	done
	cd $(DIST) && sha256sum $(BIN)_* > checksums.txt
	@echo "  wrote $(DIST)/checksums.txt"

##@ Test

.PHONY: test
test: | $(UI_OUT)/.placeholder ## Run Go unit and integration tests
	$(GO) test -race -count=1 -timeout $(GO_TEST_TIMEOUT) $(if $(TEST_RUN),-run '$(TEST_RUN)') $(TEST_PKGS)

.PHONY: test-short
test-short: | $(UI_OUT)/.placeholder ## Run only fast tests
	$(GO) test -short -count=1 ./...

.PHONY: bench
bench: | $(UI_OUT)/.placeholder ## Run the benchmarks, six runs each (BENCH, BENCH_PKGS, BENCH_COUNT narrow it); compare two runs with benchstat
	$(GO) test -run '^$$' -bench '$(BENCH)' -benchmem -count=$(BENCH_COUNT) -timeout $(GO_TEST_TIMEOUT) $(BENCH_PKGS)

.PHONY: cover
cover: | $(UI_OUT)/.placeholder ## Run tests with a coverage report
	# -coverpkg is what makes the number honest. Without it a package is only
	# credited for the lines its own _test.go files run, so code exercised
	# through the layer above it -- the controller's migration service, driven
	# entirely by internal/api's handler tests -- reports zero and sends
	# whoever reads the dashboard to write tests for the best-covered file in
	# the tree.
	$(GO) test -race -count=1 -timeout $(GO_TEST_TIMEOUT) -coverpkg=./... -coverprofile=coverage.out -covermode=atomic $(TEST_PKGS)
	$(GO) tool cover -func=coverage.out | tail -1

# One shard of the coverage run, in the binary format `go tool covdata` merges.
# A text profile cannot be joined to another one line by line -- the same block
# appears in each with its own count -- so a shard leaves its counters in
# COVERDIR and `cover-merge` turns the set into the one coverage.out that
# `make cover` writes directly.
.PHONY: cover-shard
cover-shard: | $(UI_OUT)/.placeholder ## Run one shard of the coverage run into COVERDIR (see cover-merge)
	@test -n "$(COVERDIR)" || { echo "cover-shard needs COVERDIR=<directory>"; exit 2; }
	@mkdir -p $(COVERDIR)
	$(GO) test -race -count=1 -timeout $(GO_TEST_TIMEOUT) -cover -coverpkg=./... -covermode=atomic $(if $(TEST_RUN),-run '$(TEST_RUN)') $(TEST_PKGS) -args -test.gocoverdir=$(abspath $(COVERDIR))

.PHONY: cover-merge
cover-merge: ## Merge every cover-shard directory under COVERDIRS into coverage.out
	@test -n "$(COVERDIRS)" || { echo "cover-merge needs COVERDIRS=<dir>,<dir>,..."; exit 2; }
	$(GO) tool covdata textfmt -i=$(COVERDIRS) -o coverage.out
	$(GO) tool cover -func=coverage.out | tail -1

.PHONY: test-unit
test-unit: | $(UI_DIR)/node_modules ## Run the UI's unit tests (Node's own runner, no browser, a few seconds)
	cd $(UI_DIR) && $(NPM) run test:unit

.PHONY: test-ui
test-ui: ## Run the Playwright suite (builds the binary first)
	# Built as CI builds it, which is a plain `go build` with no version stamp
	# and so the "dev" default. A working tree is normally dirty while the UI
	# is being worked on, and `git describe --dirty` then stamps a version that
	# is neither a release nor a main build -- so the controller correctly
	# declines to pin new hosts to a channel that does not exist, and the hosts
	# spec, which asserts the pin CI produces, fails for no reason anyone can
	# see from the diff they are testing.
	$(MAKE) build VERSION=dev
	cd $(UI_DIR) && $(NPM) run test:e2e

.PHONY: test-e2e
test-e2e: | $(UI_OUT)/.placeholder ## Docker-based end-to-end test; needs GitHub credentials, skipped without
	$(GO) test -count=1 -tags e2e -timeout 40m ./test/e2e/...

.PHONY: test-eli-eval
test-eli-eval: | $(UI_OUT)/.placeholder ## Play Eli's evaluation scenarios against a real provider; needs ELI_EVAL_URL, ELI_EVAL_KEY and ELI_EVAL_MODEL, skipped without
	$(GO) test -count=1 -v -run 'TestEliScenariosAgainstARealProvider' ./internal/api/

.PHONY: test-e2e-proxmox
test-e2e-proxmox: ## Live Proxmox qualification: needs a cluster and ZOOMIES_PROXMOX_*, skipped without. Hours, not minutes -- not in CI.
	$(GO) test -count=1 -v -tags e2e -timeout 10h ./test/e2e/proxmox/...

.PHONY: test-drill
test-drill: build-nogui ## Runtime drills: the built binary as a real controller and agent, against a fake GitHub
	$(GO) test -count=1 -v -tags drill -timeout 20m ./test/drill/...

# Both binaries land under dist/, which is already ignored: a fixed directory
# rather than a mktemp -d so a failed run leaves the two binaries that produced
# it where they can be run again by hand.
UPGRADE_DIR := $(DIST)/upgrade

.PHONY: test-upgrade
test-upgrade: build-nogui ## Install the last published release, then upgrade it in place to this build; then run the update helper against a fake systemctl
	@mkdir -p $(UPGRADE_DIR)/old
	$(GO) build -o $(UPGRADE_DIR)/new/zoomies ./cmd/zoomies
	sh install.sh --no-init --yes --prefix $(UPGRADE_DIR)/old
	sh test/upgrade/upgrade-check.sh $(UPGRADE_DIR)/old/zoomies $(UPGRADE_DIR)/new/zoomies
	GO=$(GO) sh test/upgrade/helper-check.sh

# The load measurement writes evidence, not an exit code: the figures are
# recorded and compared with the last run, never turned into a threshold a
# slower runner would fail a pull request on.
# Absolute, because `go test` runs in the package's own directory.
LOAD_RECORD ?= $(CURDIR)/roadmap/validation/load-$(shell git rev-parse --short HEAD).md

.PHONY: measure
measure: | $(UI_OUT)/.placeholder ## Build a fleet with a month of history and time the reads a page makes
	ZOOMIES_LOAD_RECORD=$(LOAD_RECORD) \
		$(GO) test -count=1 -v -tags load -timeout 20m ./test/load/...
	@echo "record: $(LOAD_RECORD)"

E2E_RESULTS ?= roadmap/validation/e2e

.PHONY: test-e2e-required
test-e2e-required: ## The same run, but a missing prerequisite is a failure and every scenario must pass
	rm -rf $(E2E_RESULTS)
	ZOOMIES_E2E_REQUIRED=1 ZOOMIES_E2E_RESULTS_DIR=$(E2E_RESULTS) \
	  $(GO) test -count=1 -v -tags e2e -timeout 40m ./test/e2e/... || true
	$(GO) run ./test/e2e/verify -dir $(E2E_RESULTS)

.PHONY: screenshots
screenshots: build ## Recapture docs/screenshots from the real UI (needs Pillow: pip install pillow)
	cd $(UI_DIR) && node tests/support/screenshots.mjs

.PHONY: screenshots-gallery
screenshots-gallery: | $(UI_DIR)/node_modules ## Recapture the component gallery in both styles and modes (needs Pillow)
	cd $(UI_DIR) && node tests/support/gallery-screenshots.mjs

# The UI's dependencies, as an order-only prerequisite of anything that checks
# the UI.
#
# Without them `prettier --check` fails with "Cannot find package
# 'prettier-plugin-svelte'", which reads like an npm hiccup and is easy to
# scroll past -- and the UI is then not checked at all. That is the one check
# that matters for a change to .svelte files, and it went unrun twice in a
# fresh worktree before anybody noticed. So a missing tree is installed rather
# than reported: the failure mode of this step should be slow, not silent.
#
# Order-only, so an install already there is never redone because something
# touched its mtime.
$(UI_DIR)/node_modules:
	cd $(UI_DIR) && $(NPM) ci --no-audit --no-fund

##@ Quality

.PHONY: lint
lint: | $(UI_DIR)/node_modules $(UI_OUT)/.placeholder ## Vet, format check, and staticcheck when available
	$(GO) vet ./...
	@$(MAKE) --no-print-directory vet-tags
	@out=$$(gofmt -l $$(git ls-files '*.go')); \
	 if [ -n "$$out" ]; then echo "gofmt needed:"; echo "$$out"; exit 1; fi
	@if command -v staticcheck >/dev/null 2>&1; then staticcheck ./...; \
	 else echo "  staticcheck not installed, skipping"; fi
	cd $(UI_DIR) && $(NPM) run lint

# The tagged suites. An ordinary `go vet ./...` never compiles them, which is how
# test/e2e sat with a non-test file referring to a symbol only its _test.go half
# defined: every gate was green and the package did not build under its own
# tag. Vet rather than build, because vet compiles the tests as well and it is
# the test halves that carry most of this code. CI runs this target too, and a
# test in internal/docs fails when a build tag is used that it does not cover.
.PHONY: vet-tags
vet-tags: | $(UI_OUT)/.placeholder ## Vet the suites behind a build tag (e2e, drill, load), which a plain go vet never compiles
	$(GO) vet -tags e2e ./...
	$(GO) vet -tags drill ./...
	$(GO) vet -tags load ./...

.PHONY: fmt
fmt: | $(UI_DIR)/node_modules ## Format Go and UI sources
	gofmt -w $$(git ls-files '*.go')
	cd $(UI_DIR) && $(NPM) run format

.PHONY: tidy
tidy: ## Tidy go.mod
	$(GO) mod tidy

##@ Run

.PHONY: run
run: build-nogui ## Run a controller against a local database
	./$(BIN) controller --config ./zoomies.yaml

.PHONY: dev
dev: build-nogui ## Run a controller with auth off, for local UI development
	ZOOMIES_DISABLE_AUTH=true ZOOMIES_LOG_FORMAT=text ZOOMIES_LOG_LEVEL=debug \
	ZOOMIES_DB_PATH=./zoomies.db ./$(BIN) controller

.PHONY: gallery
gallery: | $(UI_DIR)/node_modules ## Serve the development-only component gallery on :5173/gallery.html
	@echo "component gallery: http://127.0.0.1:5173/gallery.html?style=mission-control&mode=dark"
	cd $(UI_DIR) && $(NPM) run dev

##@ Images

.PHONY: image
image: ## Build the controller image (deploy/Dockerfile, target controller)
	docker build -f deploy/Dockerfile --target controller \
		--build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) --build-arg DATE=$(DATE) \
		-t ghcr.io/eyupio/zoomies:$(VERSION) -t ghcr.io/eyupio/zoomies:latest .

# The bounded Git worker conflict repair merges in. It is not published yet: no
# workflow names the target, so nothing pushes it to GHCR and nothing would
# notice if its base image moved. The Dockerfile's last step in the target runs
# the worker's own toolchain check, so a build that succeeds is a worker whose
# Git can merge.
.PHONY: image-gitworker
image-gitworker: ## Build the Git worker image (deploy/Dockerfile, target gitworker)
	docker build -f deploy/Dockerfile --target gitworker \
		--build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) --build-arg DATE=$(DATE) \
		-t ghcr.io/eyupio/zoomies-git:$(VERSION) .

.PHONY: image-nixpacks
image-nixpacks: ## Build the controller image from source with Nixpacks (needs the nixpacks CLI)
	@command -v nixpacks >/dev/null 2>&1 || { \
	  echo "nixpacks is not installed: https://nixpacks.com/docs/install"; exit 1; }
	nixpacks build . --name ghcr.io/eyupio/zoomies:$(VERSION)-nixpacks

# The runner image's variants. Each one is a base image, the package family
# deploy/Dockerfile.runner installs with, and the platform it claims to be.
#
# The rows below are generated from internal/naming's catalogue, which is the
# one place an operating system is added or swapped. Do not edit them by hand:
# run `make generate` and commit what it writes.
RUNNER_IMAGE   ?= ghcr.io/eyupio/zoomies-runner
RUNNER_VARIANT ?= $(RUNNER_VARIANT_DEFAULT)

# zoomies:catalogue-begin
RUNNER_VARIANTS := ubuntu-2404 ubuntu-2604 ubuntu-2204 debian-13 debian-12 fedora-42 rocky-9
RUNNER_VARIANT_DEFAULT := ubuntu-2404

# base | family | os | version
variant.ubuntu-2404 := ubuntu:24.04@sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254 apt ubuntu 24.04
variant.ubuntu-2604 := ubuntu:26.04@sha256:da6fc2be547864451aa253836dd926da33623312df4a9a243e35dc877c378a78 apt ubuntu 26.04
variant.ubuntu-2204 := ubuntu:22.04@sha256:829f6df217bcbae2b371026e81711d1a787c61b2967ad09d015063663ebafbf7 apt ubuntu 22.04
variant.debian-13   := debian:13-slim@sha256:a99cfc517144bc59b1978475ec53b46ecabec7e43635402ee5b77cc54cd1b20a apt debian 13
variant.debian-12   := debian:12-slim@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171 apt debian 12
variant.fedora-42   := fedora:42@sha256:99e203b80b1c3d8f7e161ec10a68fd02b081ef83a3963553e513c82846b97814 dnf fedora 42
variant.rocky-9     := rockylinux/rockylinux:9@sha256:8101994123cf3d0a8fee517bee7f39e555c7d92bd2d9eb3303cc988a0eeed00f dnf rocky 9
# zoomies:catalogue-end

# variant-args renders one variant's row as docker build arguments.
variant-args = \
	--build-arg BASE=$(word 1,$(variant.$(1))) \
	--build-arg OS_FAMILY=$(word 2,$(variant.$(1))) \
	--build-arg OS_ID=$(word 3,$(variant.$(1))) \
	--build-arg OS_VERSION=$(word 4,$(variant.$(1)))

.PHONY: check-variant
check-variant:
	@if [ -z "$(variant.$(RUNNER_VARIANT))" ]; then \
		echo "unknown RUNNER_VARIANT '$(RUNNER_VARIANT)'; one of: $(RUNNER_VARIANTS)" >&2; \
		exit 1; \
	fi

.PHONY: image-runner
image-runner: check-variant ## Build one runner variant for the host architecture (RUNNER_VARIANT=debian-12, RUNNER_TARGET=runner-docker for the Docker one)
	docker build -f deploy/Dockerfile.runner --target $(or $(RUNNER_TARGET),runner) \
		--build-arg RUNNER_VERSION=$(RUNNER_VERSION) \
		$(call variant-args,$(RUNNER_VARIANT)) \
		-t $(RUNNER_IMAGE):$(RUNNER_VARIANT) .

.PHONY: images-runner
images-runner: ## Build every runner variant for the host architecture
	@for v in $(RUNNER_VARIANTS); do \
		echo "  building $(RUNNER_IMAGE):$$v"; \
		$(MAKE) --no-print-directory image-runner RUNNER_VARIANT=$$v; \
	done

.PHONY: check-image-runner
check-image-runner: check-variant ## Check a locally built runner variant has what Zoomies relies on (RUNNER_VARIANT=debian-12 RUNNER_TARGET=runner-docker)
	RUNNER_VERSION=$(RUNNER_VERSION) test/runner-image/check.sh $(RUNNER_IMAGE):$(RUNNER_VARIANT) \
		$(word 3,$(variant.$(RUNNER_VARIANT))) $(word 4,$(variant.$(RUNNER_VARIANT))) $(or $(RUNNER_TARGET),runner)

.PHONY: image-runner-multiarch
image-runner-multiarch: check-variant ## Build one runner variant for amd64 and arm64 (needs buildx)
	docker buildx build --platform linux/amd64,linux/arm64 \
		-f deploy/Dockerfile.runner --build-arg RUNNER_VERSION=$(RUNNER_VERSION) \
		$(call variant-args,$(RUNNER_VARIANT)) \
		-t $(RUNNER_IMAGE):$(RUNNER_VARIANT) .

.PHONY: images-multiarch
images-multiarch: ## Build the controller and every runner variant for amd64 and arm64 (needs buildx)
	docker buildx build --platform linux/amd64,linux/arm64 \
		-f deploy/Dockerfile --target controller \
		--build-arg VERSION=$(VERSION) -t ghcr.io/eyupio/zoomies:$(VERSION) .
	@for v in $(RUNNER_VARIANTS); do \
		echo "  building $(RUNNER_IMAGE):$$v for amd64 and arm64"; \
		$(MAKE) --no-print-directory image-runner-multiarch RUNNER_VARIANT=$$v; \
	done

##@ Housekeeping

.PHONY: openapi
openapi: | $(UI_DIR)/node_modules ## Regenerate both copies of the contract: the embedded Go source and the TypeScript client
	@# Two files are generated from api/openapi.yaml and a test fails on either
	@# when it is stale (the embedded copy in internal/api, the client in CI),
	@# so one command does both. The Go half reads the spec with the standard
	@# library only and does not need the UI.
	$(GO) run internal/api/gen_openapi.go
	cd $(UI_DIR) && $(NPM) run generate:api

.PHONY: generate
generate: | $(UI_OUT)/.placeholder ## Rewrite everything generated from a table in Go: the image catalogue, the problem and check catalog, the command and MCP references
	$(GO) run internal/naming/gen_catalogue.go
	$(GO) run internal/catalog/gen_catalog.go
	mkdir -p skills/zoomies && $(GO) run ./cmd/zoomies commands --output markdown > skills/zoomies/reference.md
	$(GO) run ./cmd/zoomies mcp --describe --output markdown > skills/zoomies/mcp-reference.md

.PHONY: runner-bases
runner-bases: ## Move each runner image's base-image pin to what its tag points at today, and rewrite what is derived from it (needs network)
	@# Only what the table feeds: not `make generate`, which also restamps the
	@# problem catalogue and rewrites the skill references, and would put that
	@# noise into the weekly pull request that runs this.
	$(GO) run internal/naming/gen_base_digests.go
	$(GO) run internal/naming/gen_catalogue.go

.PHONY: marketplace-lock
marketplace-lock: ## Re-resolve deploy/marketplace/images.lock against the registry (needs network)
	$(GO) run deploy/marketplace/gen_lock.go

.PHONY: clean
clean: ## Remove build output
	rm -rf $(BIN) $(DIST) $(UI_OUT) coverage.out
	rm -rf $(UI_DIR)/node_modules $(UI_DIR)/dist

.PHONY: help
help: ## Show this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nZoomies -- off the lead, on the job.\n\nUsage:\n  make \033[36m<target>\033[0m\n"} \
	 /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-18s\033[0m %s\n", $$1, $$2 } \
	 /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)