# Go Bananas!! # # The Makefile is the primary interface to this repository. Every task a developer # or a CI job needs is a target here, and each target is safe to run repeatedly. # # Conventions: # * `make help` lists everything. # * Targets are grouped by phase and the phases run in order for `make all`. # * Anything that changes the machine (installing a tool, pushing an image) is # marked ##@destructive so it is obvious in `make help`. # * Variables at the top are overridable: `make IMAGE_TAG=v2 build`. # # The `##@` markers are read by the self-documenting help target, which is why # they are inline comments rather than a separate block. SHELL := /usr/bin/env bash .SHELLFLAGS := -eu -o pipefail -c .DEFAULT_GOAL := help .DELETE_ON_ERROR: .SUFFIXES: # --------------------------------------------------------------- variables ---- BINARY := go-bananas CMD := ./cmd/go-bananas BIN_DIR := bin DIST_DIR := dist WEB_DIR := web SDK_DIR := sdk VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) REGISTRY ?= nexus.urpi.be/docker-hosted IMAGE_NAME ?= $(REGISTRY)/$(BINARY) IMAGE_TAG ?= $(VERSION) PLATFORM ?= linux/amd64,linux/arm64 GO ?= go NPM ?= npm # Development tool versions, pinned so that every contributor and every CI run use # the same linter. They are pinned here rather than in a separate tools module # because `go install pkg@version` resolves the version itself and refuses a # -modfile, which would make a tools.mod decorative rather than binding. `make # tools-upgrade` bumps all three in one place. AIR_VERSION ?= v1.67.4 GOLANGCI_VERSION ?= v2.14.0 SQLC_VERSION ?= v1.31.1 HELM ?= helm DOCKER ?= docker # Module cache and build cache live outside the tree so that a bind-mounted # working directory does not slowly fill the developer's disk. GO_CACHE ?= $(CURDIR)/.cache/go-build GOMODCACHE ?= $(CURDIR)/.cache/go-mod # The Go packages belonging to this module, excluding the JavaScript trees. # # Go does not ignore node_modules: any directory under the module root with Go # files in it is part of `./...`, and a dependency that happens to ship a stray # package would then be compiled and linted as if it were ours. `./internal/...` # and `./cmd/...` are listed instead of filtering, so the omission is visible here # rather than hidden in a pipe. GO_PACKAGES = ./cmd/... ./internal/... # The full set of ldflags. Kept in one variable so that `go build`, `go install` # and goreleaser cannot drift apart and produce differently-identified binaries. LDFLAGS := -s -w \ -X github.com/urpi/go-bananas/internal/app.Version=$(VERSION) \ -X github.com/urpi/go-bananas/internal/app.Commit=$(COMMIT) \ -X github.com/urpi/go-bananas/internal/app.BuildDate=$(BUILD_DATE) GOFLAGS_BUILD := -trimpath -ldflags "$(LDFLAGS)" .DEFAULT_COLOR := \033[0m .BOLD := \033[1m .DIM := \033[2m .GREEN := \033[32m .YELLOW := \033[33m .RED := \033[31m ##@ General .PHONY: help help: ## Show this help @printf "$(BOLD)go-bananas$(DEFAULT_COLOR) — runtime design-token service\n\n" @printf "$(DIM)Version$(DEFAULT_COLOR) $(VERSION) ($(COMMIT))\n\n" @# The awk pass reads both the "##@" group markers and the "##" target help in @# one scan so that groups appear in file order rather than in two separate @# alphabetical lists. A trailing "##@destructive" is stripped from the @# description and rendered as a marker instead. @awk 'BEGIN {FS = ":.*##"} \ /^##@/ { printf "\n$(BOLD)%s$(DEFAULT_COLOR)\n", substr($$0, 5); next } \ /^[a-zA-Z0-9_-]+:.*?##/ { \ help = $$0; sub(/^[^:]*:.*## /, "", help); \ mark = ""; \ if (help ~ /##@destructive/) { mark = "$(YELLOW) [modifies this machine]$(DEFAULT_COLOR)"; \ sub(/ *##@destructive/, "", help) } \ printf " $(BOLD)%-20s$(DEFAULT_COLOR) %s%s\n", $$1, help, mark \ }' $(MAKEFILE_LIST) @printf "\n$(DIM)Override any variable, e.g. make IMAGE_TAG=v2.1.0 image-push$(DEFAULT_COLOR)\n" .PHONY: version version: ## Print the version that would be built @echo "$(VERSION) ($(COMMIT)) built $(BUILD_DATE)" ##@ Development .PHONY: install install: ## Install Go and Node dependencies $(GO) mod download cd $(WEB_DIR) && $(NPM) install cd $(SDK_DIR) && $(NPM) install .PHONY: dev dev: ## Run the API with hot reload (Ctrl-C to stop) @command -v air >/dev/null 2>&1 || { \ echo "air is not installed; run 'make tools' or see docs/development.md"; exit 1; } @BANANAS_ENVIRONMENT=development \ BANANAS_AUTH_MODE=dev \ BANANAS_HTTP_ADDR=:8080 \ BANANAS_DATABASE_PATH=./data/go-bananas.db \ BANANAS_STATIC_DIR=$(WEB_DIR)/dist \ BANANAS_SDK_DIR=$(SDK_DIR)/dist \ air -c .air.toml .PHONY: dev-api dev-api: ## Run the API (no SPA); Vite serves the UI, this serves the runtime SDK @BANANAS_ENVIRONMENT=development \ BANANAS_AUTH_MODE=dev \ BANANAS_HTTP_ADDR=:8080 \ BANANAS_DATABASE_PATH=./data/go-bananas.db \ BANANAS_STATIC_DIR= \ BANANAS_SDK_DIR=$(SDK_DIR)/dist \ $(GO) run -ldflags "$(LDFLAGS)" $(CMD) .PHONY: dev-web dev-web: ## Run the Vite dev server for the admin UI, proxying the API cd $(WEB_DIR) && $(NPM) run dev .PHONY: dev-all dev-all: build-sdk ## Run the API and the frontend dev server together @echo "API on http://localhost:8080, admin UI on http://localhost:5173" @trap 'kill 0' EXIT; \ $(MAKE) dev-api & \ $(MAKE) dev-web & \ wait .PHONY: tools tools: ## Install the pinned development tools (air, golangci-lint, sqlc) ##@destructive $(GO) install github.com/air-verse/air@$(AIR_VERSION) $(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) $(GO) install github.com/sqlc-dev/sqlc/cmd/sqlc@$(SQLC_VERSION) @echo "Tools installed into $$( $(GO) env GOBIN )" @echo "golangci-lint $(GOLANGCI_VERSION), air $(AIR_VERSION), sqlc $(SQLC_VERSION)" .PHONY: tools-upgrade tools-upgrade: ## Bump the pinned development tools to their latest releases ##@destructive @for tool in \ github.com/air-verse/air:AIR_VERSION \ github.com/golangci/golangci-lint/v2:GOLANGCI_VERSION \ github.com/sqlc-dev/sqlc:SQLC_VERSION; do \ pkg=$${tool%%:*}; var=$${tool##*:}; \ latest=$$($(GO) list -m -versions $$pkg | tr ' ' '\n' | tail -1); \ [ -n "$$latest" ] || { echo "could not resolve $$pkg"; exit 1; }; \ sed -i.bak "s|^$$var .*|$$var ?= $$latest|" Makefile && rm -f Makefile.bak; \ echo "$$var -> $$latest"; \ done @echo "Run 'make tools' to install them and commit the Makefile change." .PHONY: clean clean: ## Remove build output and local data rm -rf $(BIN_DIR) $(DIST_DIR) data .cache cd $(WEB_DIR) && rm -rf dist cd $(SDK_DIR) && rm -rf dist @echo "Cleaned." .PHONY: clean-cache clean-cache: ## Remove every build cache, including the module cache ##@destructive $(GO) clean -cache -modcache rm -rf .cache cd $(WEB_DIR) && rm -rf node_modules/.vite cd $(SDK_DIR) && rm -rf node_modules/.vite ##@ Build .PHONY: build build: build-go build-web build-sdk ## Build every artefact .PHONY: build-go build-go: ## Build the server binary into bin/ @mkdir -p $(BIN_DIR) CGO_ENABLED=0 $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(BINARY) $(CMD) @echo "Built $(BIN_DIR)/$(BINARY)" .PHONY: build-web build-web: ## Build the admin UI into web/dist cd $(WEB_DIR) && $(NPM) run build .PHONY: build-sdk build-sdk: ## Build the runtime SDK bundles into sdk/dist cd $(SDK_DIR) && $(NPM) run build .PHONY: run run: build-go ## Build and run the server with the current assets @mkdir -p data BANANAS_ENVIRONMENT=development \ BANANAS_AUTH_MODE=dev \ BANANAS_STATIC_DIR=$(WEB_DIR)/dist \ BANANAS_SDK_DIR=$(SDK_DIR)/dist \ ./$(BIN_DIR)/$(BINARY) .PHONY: release release: ## Build release binaries for every supported platform ##@destructive @command -v goreleaser >/dev/null 2>&1 || { \ echo "goreleaser is not installed; see docs/development.md"; exit 1; } goreleaser release --clean .PHONY: release-snapshot release-snapshot: ## Build release binaries without publishing ##@destructive goreleaser release --snapshot --clean --skip=publish .PHONY: sbom sbom: ## Generate a CycloneDX SBOM for the current version ##@destructive @command -v syft >/dev/null 2>&1 || { \ echo "syft is not installed"; exit 1; } syft dir:. -o cyclonedx-json=sbom-$(VERSION).json ##@ Quality .PHONY: check check: fmt-check lint test ## Run every check .PHONY: fmt fmt: ## Format the Go code $(GO) fmt ./... .PHONY: fmt-check fmt-check: ## Fail if the Go code is not formatted @unformatted="$$(gofmt -l . | grep -v '^vendor/' || true)"; \ if [ -n "$$unformatted" ]; then \ echo "These files are not gofmt'd:"; echo "$$unformatted"; exit 1; \ fi @echo "Go formatting is clean." .PHONY: lint lint: lint-go lint-web lint-sdk ## Run every linter .PHONY: lint-go lint-go: ## Run golangci-lint @command -v golangci-lint >/dev/null 2>&1 || { \ echo "golangci-lint is not installed; run 'make tools'"; exit 1; } golangci-lint run $(GO_PACKAGES) .PHONY: lint-web lint-web: ## Lint and typecheck the admin UI cd $(WEB_DIR) && $(NPM) run lint cd $(WEB_DIR) && $(NPM) run typecheck .PHONY: lint-sdk lint-sdk: ## Lint and typecheck the runtime SDK cd $(SDK_DIR) && $(NPM) run typecheck .PHONY: fmt-web fmt-web: ## Format the frontend and SDK sources cd $(WEB_DIR) && $(NPM) run format cd $(SDK_DIR) && $(NPM) exec prettier --write . .PHONY: tidy tidy: ## Tidy the Go module $(GO) mod tidy ##@ Test .PHONY: test test: test-go test-web test-sdk ## Run every test suite .PHONY: test-go test-go: ## Run the Go tests $(GO) test -race -count=1 $(GO_PACKAGES) .PHONY: test-cover test-cover: ## Run the Go tests and write a coverage profile $(GO) test -race -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PACKAGES) $(GO) tool cover -func=coverage.out | tail -1 @echo "Profile written to coverage.out" .PHONY: test-web test-web: ## Run the admin UI tests cd $(WEB_DIR) && $(NPM) test .PHONY: test-sdk test-sdk: ## Run the runtime SDK tests cd $(SDK_DIR) && $(NPM) test .PHONY: bench bench: ## Run the token engine benchmarks $(GO) test -bench=. -benchmem ./internal/tokenengine/ ##@ Docker .PHONY: image image: ## Build the container image for the host architecture $(DOCKER) buildx build \ --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg BUILD_DATE=$(BUILD_DATE) \ --tag $(IMAGE_NAME):$(IMAGE_TAG) \ --tag $(IMAGE_NAME):latest \ --load \ . .PHONY: image-multi image-multi: ## Build the multi-architecture image without pushing ##@destructive $(DOCKER) buildx build \ --platform $(PLATFORM) \ --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg BUILD_DATE=$(BUILD_DATE) \ --tag $(IMAGE_NAME):$(IMAGE_TAG) \ . .PHONY: image-push image-push: ## Build and push the multi-architecture image ##@destructive $(DOCKER) buildx build \ --platform $(PLATFORM) \ --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg BUILD_DATE=$(BUILD_DATE) \ --push \ --tag $(IMAGE_NAME):$(IMAGE_TAG) \ . .PHONY: image-run image-run: ## Run the image locally on port 8080 $(DOCKER) run --rm -it \ -p 8080:8080 \ -e BANANAS_ENVIRONMENT=development \ -e BANANAS_AUTH_MODE=dev \ -v go-bananas-data:/srv/data \ $(IMAGE_NAME):$(IMAGE_TAG) .PHONY: compose-up compose-up: ## Start the local stack with docker compose $(DOCKER) compose up --build .PHONY: compose-down compose-down: ## Stop the local stack ##@destructive $(DOCKER) compose down .PHONY: compose-logs compose-logs: ## Follow the local stack logs $(DOCKER) compose logs -f .PHONY: compose-shell compose-shell: ## Open a shell in the running service ##@destructive $(DOCKER) compose exec go-bananas /bin/sh ##@ Helm .PHONY: helm-lint helm-lint: ## Lint the chart $(HELM) lint deploy/helm/go-bananas --strict .PHONY: helm-matrix helm-matrix: ## Render the chart across a matrix of valid and invalid configurations ./hack/helm-matrix.sh .PHONY: helm-template helm-template: ## Render the chart with the default values $(HELM) template go-bananas deploy/helm/go-bananas .PHONY: helm-template-auth helm-template-auth: ## Render the chart configured for OIDC $(HELM) template go-bananas deploy/helm/go-bananas \ --set identity.mode=oidc \ --set identity.oidc.issuer=https://auth.lcl-urpi.be/realms/urpi \ --set identity.oidc.clientId=go-bananas \ --set identity.oidc.generateSecret=false \ --set identity.oidc.existingSecret=go-bananas-oidc \ --set config.publicBaseUrl=https://themes-dev.example.com .PHONY: helm-install helm-install: ## Install the chart into the current kube context ##@destructive $(HELM) upgrade --install go-bananas deploy/helm/go-bananas \ --namespace go-bananas --create-namespace \ --values deploy/helm/go-bananas/values.yaml \ --wait --timeout 5m .PHONY: helm-uninstall helm-uninstall: ## Remove the release ##@destructive $(HELM) uninstall go-bananas --namespace go-bananas ##@ Local (lcl) # The lcl environment is the developer's local k3s cluster with a registry at # localhost:5000 and a TLS edge serving *.intern-urpi.be. go-* apps deploy into the # shared `go-insane-lcl` namespace. These targets use values-lcl.yaml. # # KUBECONFIG — the lcl kubeconfig. On this box the `local` kubecontext goes through a # Rancher proxy that can 502; the direct k3s config is /etc/rancher/k3s/k3s.yaml. # Set it to deploy without depending on the proxy: KUBECONFIG=/etc/rancher/k3s/k3s.yaml make deploy-lcl # LCL_TAG — image tag. Defaults to a timestamp so a rebuild is always re-pulled; # `IfNotPresent` would otherwise serve a stale `0.0.1` from the node cache. LCL_REGISTRY ?= localhost:5000 LCL_REPO ?= go-bananas LCL_TAG ?= $(shell date -u +%Y%m%d%H%M%S) LCL_NAMESPACE ?= go-insane-lcl LCL_VALUES = deploy/helm/go-bananas/values-lcl.yaml .PHONY: image-lcl image-lcl: ## Build and push the image to the local registry ##@destructive $(DOCKER) buildx build \ --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg BUILD_DATE=$(BUILD_DATE) \ --tag $(LCL_REGISTRY)/$(LCL_REPO):$(LCL_TAG) \ --push \ . .PHONY: helm-template-lcl helm-template-lcl: ## Render the lcl Helm manifest $(HELM) template go-bananas deploy/helm/go-bananas \ --namespace $(LCL_NAMESPACE) \ --values $(LCL_VALUES) \ --set-string image.tag=$(LCL_TAG) .PHONY: helm-install-lcl helm-install-lcl: ## Deploy go-bananas to the local (lcl) environment ##@destructive $(HELM) upgrade --install go-bananas deploy/helm/go-bananas \ --namespace $(LCL_NAMESPACE) --create-namespace \ --values $(LCL_VALUES) \ --set-string image.tag=$(LCL_TAG) \ --wait --timeout 5m .PHONY: helm-uninstall-lcl helm-uninstall-lcl: ## Remove the lcl release ##@destructive $(HELM) uninstall go-bananas --namespace $(LCL_NAMESPACE) .PHONY: deploy-lcl deploy-lcl: image-lcl helm-install-lcl ## Build, push, deploy to lcl, then verify ##@destructive @echo "waiting for rollout ($(LCL_NAMESPACE)/go-bananas)..." @kubectl -n $(LCL_NAMESPACE) rollout status deploy/go-bananas --timeout=120s @echo "=== runtime manifest through the TLS edge ===" @curl -sk https://bananas-lcl.intern-urpi.be/rt/v1/manifest.json -o /dev/null -w 'manifest.json -> %{http_code}\n' || true @echo "deployed image: $(LCL_REGISTRY)/$(LCL_REPO):$(LCL_TAG)" ##@ Docs .PHONY: docs-serve docs-serve: ## Serve the documentation locally @command -v mkdocs >/dev/null 2>&1 || { echo "mkdocs is not installed"; exit 1; } mkdocs serve .PHONY: docs-build docs-build: ## Build the documentation ##@destructive mkdocs build --strict .PHONY: api-spec api-spec: ## Print the OpenAPI document summary @grep -E '^\s{2}/' openapi.yaml | sed 's/:$//' | tr -d ' ' | sed 's|^| |' ##@ Contract .PHONY: check-contract check-contract: ## Drive the shared styling contract and the shipped controls in a real browser ##@destructive ./hack/check-contract.sh .PHONY: check-pilot check-pilot: ## Prove both adopting applications follow the contract without editing their CSS ##@destructive ./hack/check-pilot-go-loose.sh ./hack/check-pilot-go-tell.sh ##@ CI helpers .PHONY: ci ci: check ## Everything CI runs, in one target .PHONY: verify verify: ci helm-lint helm-matrix check-contract check-pilot ## The full pre-merge gate .PHONY: deps-outdated deps-outdated: ## Report outdated Go and Node dependencies $(GO) list -u -m all @echo cd $(WEB_DIR) && $(NPM) outdated || true .PHONY: deps-update deps-update: ## Update Go and Node dependencies ##@destructive $(GO) get -u ./... && $(GO) mod tidy cd $(WEB_DIR) && $(NPM) update cd $(SDK_DIR) && $(NPM) update