REGISTRY  := git.ryuvia.com
# The personal namespace, not ryuvia — deliberately, and for one reason: Gitea
# scopes package visibility to the owner with no per-package override, so
# ryuvia/* is private because the org is. Publishing here keeps the image and
# chart anonymously pullable, so no pull secret is needed in the cluster and
# Flux needs no registry credentials. Same choice riksdata and rd-web made.
OWNER     := niklas

IMAGE      := $(REGISTRY)/$(OWNER)/terdut-server
HELM_CHART := charts/terdut-server
HELM_REPO  := oci://$(REGISTRY)/$(OWNER)

# go.mod pins an exact patch release so nobody builds the shipped binary with a
# toolchain carrying known stdlib CVEs. Fedora's Go package overrides the
# upstream GOTOOLCHAIN default to `local`, which turns that pin into a hard
# failure on a dev box one patch behind, so restore the upstream default here.
export GOTOOLCHAIN ?= auto

.PHONY: help
help: ## Show this help
	@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-22s\033[0m %s\n", $$1, $$2}'

## --- checks ---
#
# These three mirror .gitea/workflows/ci.yaml step for step, so a green `make fmt
# lint test` here means the same thing CI means. The one deliberate difference is
# -race below.

.PHONY: test
test: ## Run the test suite
	go test -race ./...

# CI runs a bare `go test ./...`. This is stricter on purpose: the sweeper, the
# notifier goroutine and the deadman sweep all touch the same single-connection
# database, and a race there would surface as a flaky production incident rather
# than a failed build. It passes today; if it ever costs more than it catches,
# the honest fix is to teach CI -race too, not to quietly drop it here.
.PHONY: lint
lint: ## go vet
	go vet ./...

# Copied from ci.yaml rather than simplified, because both of gofmt's failure
# modes need handling and they are not alike. A file that is merely misformatted
# is listed on stdout with exit 0 — so the failure has to be raised by hand. A
# file that does not parse is the opposite: nothing on stdout and exit 2, which a
# naive `[ -n "$$out" ]` reads as success. See 9046f6e.
.PHONY: fmt
fmt: ## Report unformatted files
	@if ! unformatted=$$(gofmt -l .); then \
		echo "gofmt could not parse the tree:"; gofmt -l .; exit 1; \
	fi; \
	if [ -n "$$unformatted" ]; then \
		echo "gofmt needed:"; echo "$$unformatted"; gofmt -d .; exit 1; \
	fi

.PHONY: helm-lint
helm-lint: ## Lint and render the chart
	helm lint $(HELM_CHART) --set image.tag=v0.0.0
	helm template terdut-server $(HELM_CHART) --namespace terdut-server \
		--set image.tag=v0.0.0 >/dev/null
	@# networking.listener defaults to "", which attaches the route to every
	@# matching listener including plaintext HTTP. Production sets it, so the
	@# default render proves nothing about the path that actually ships.
	helm template terdut-server $(HELM_CHART) --namespace terdut-server \
		--set image.tag=v0.0.0 --set networking.listener=https-terdut >/dev/null

## --- release ---

# The release process (~/.claude/skills/release) reads these rather than restating them.
# One definition, so the version that gets tagged, the image that gets pushed and the chart
# the wrapper pins cannot drift apart in a second copy.
.PHONY: release-vars
release-vars: ## Print the variables the release process reads
	@printf 'IMAGE=%s\nHELM_CHART=%s\nHELM_REPO=%s\n' '$(IMAGE)' '$(HELM_CHART)' '$(HELM_REPO)'

# There is deliberately no build/push/helm-package/helm-push/release here, unlike
# riksdata and rd-web. .gitea/workflows/release.yaml owns publishing for this repo,
# and it does two things a local make cannot: it builds linux/amd64 and linux/arm64
# through buildx, and it stamps the chart's version and appVersion from the tag. A
# `docker build && docker push` target would push a single-architecture image over
# the multi-arch tag, which is both easy to do by accident and invisible afterwards
# — the tag would still resolve, just not on arm64. Publishing happens by pushing a
# tag; nothing else.

## --- publishing ---
#
# These exist so .gitea/workflows/release.yaml can call `make release` instead of
# restating the build in YAML, the way riksdata and rd-web already do. One definition
# of how this is built and published, runnable locally, reviewable in a diff.
#
# VERSION is the git tag, passed in by the workflow. The guard below is why a stray
# local `make release` cannot publish: dev is not a version anyone releases.

VERSION ?= dev

# Helm requires strict SemVer — strip a leading 'v' if present.
CHART_VERSION := $(shell echo "$(VERSION)" | sed 's/^v//')

PLATFORMS      ?= linux/amd64,linux/arm64
BUILDX_BUILDER ?= terdut

TRIVY_VERSION  := 0.73.0

# --pull, not --no-cache: refresh the base image without discarding the layer cache.
DOCKER_BUILD_FLAGS ?= --pull

# An isolated repo list. The machine-wide one is not this build's business, and one
# unreachable entry in it aborts otherwise-fine helm commands — there is a dead
# TrueCharts repo on this host that does exactly that. HELM_REPOSITORY_CACHE is
# deliberately NOT overridden alongside it: helm writes a refreshed index to the default
# cache and then looks for it in the overridden one.
HELM_ISOLATED = HELM_REPOSITORY_CONFIG=$(CURDIR)/.helm-repos.yaml

.PHONY: require-version
require-version:
	@test "$(VERSION)" != "dev" || \
		(echo "VERSION=dev names no release — pass VERSION=vX.Y.Z (the workflow passes the tag)" && exit 1)

.PHONY: build
build: ## Build the image for this host only, without pushing (local check / CI smoke)
	docker build $(DOCKER_BUILD_FLAGS) \
		--build-arg VERSION=$(VERSION) \
		-t $(IMAGE):$(VERSION) .

# Multi-arch, so unlike riksdata and rd-web this cannot be a separate build then push:
# buildx cannot load a multi-platform result into the local image store, it can only
# push it. `build` above stays single-platform and local-only for that reason.
#
# No QEMU: the Dockerfile's builder stage runs on $$BUILDPLATFORM and cross-compiles from
# TARGETARCH, so both platforms build natively. The default "docker" driver cannot build
# more than one platform at a time; the docker-container driver can.
.PHONY: push
push: require-version ## Build and publish the multi-arch image
	docker buildx create --name $(BUILDX_BUILDER) --use 2>/dev/null || docker buildx use $(BUILDX_BUILDER)
	docker buildx build \
		--platform $(PLATFORMS) \
		--build-arg "VERSION=$(VERSION)" \
		--tag "$(IMAGE):latest" \
		--tag "$(IMAGE):$(VERSION)" \
		--push .

# --version and --app-version come from the tag, so Chart.yaml's own fields decide nothing
# about what is published. They used to be rewritten in place with sed before packaging;
# the flags do the same job without mutating the tree mid-build.
.PHONY: helm-package
helm-package: require-version ## Package the chart, versioned from the tag
	$(HELM_ISOLATED) helm package $(HELM_CHART) \
		--version $(CHART_VERSION) \
		--app-version $(VERSION) \
		--destination dist

.PHONY: helm-push
helm-push: require-version ## Push the packaged chart to the OCI registry
	$(HELM_ISOLATED) helm push dist/terdut-server-$(CHART_VERSION).tgz $(HELM_REPO)

.PHONY: binaries
binaries: require-version ## Cross-compile the release binaries into dist/
	@mkdir -p dist
	@set -eu; for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do \
		GOOS="$${target%/*}"; GOARCH="$${target#*/}"; \
		out="dist/terdut-$(VERSION)-$${GOOS}-$${GOARCH}"; \
		echo "building $$out"; \
		GOOS="$$GOOS" GOARCH="$$GOARCH" go build \
			-ldflags "-w -s -X main.version=$(VERSION)" \
			-o "$$out" ./cmd/terdut; \
	done

.PHONY: release
release: push helm-package helm-push ## Publish image + chart (the workflow's one call)

## --- security ---

# Scans the pushed image, not a local one: trivy cannot read a locally built image on the
# runner -- Talos has no docker socket, and the dind sidecar shares no filesystem with the
# job -- so it pulls from the registry. Same reason riksdata and rd-web scan after pushing.
#
# It cannot gate a deploy, because this pipeline does not deploy. A red scan means: do not
# bump the wrapper chart in Ryuvia/charts to this version.
#
# The image is FROM scratch, so there are no OS packages to scan and trivy sees exactly one
# target -- the Go binary and its module graph. That also makes scanning a single platform
# sufficient here: linux/amd64 and linux/arm64 are the same modules built for a different
# GOARCH, so a CVE in one is a CVE in both. On an image with a base layer that would not
# hold and both platforms would need scanning.
#
# riksdata and rd-web also run govulncheck and gitleaks in a `security` CI job. This repo
# does not, so this target is the only security scanning it has.
.PHONY: security-image
security-image: require-version ## Scan the pushed image for CVEs (needs VERSION)
	@# The named volume persists trivy's vulnerability DB between runs; without it every
	@# scan re-downloads the whole database.
	docker run --rm -e TRIVY_USERNAME -e TRIVY_PASSWORD \
		-v trivy-cache:/root/.cache/trivy \
		docker.io/aquasec/trivy:$(TRIVY_VERSION) image --severity HIGH,CRITICAL \
		--ignore-unfixed --exit-code 1 $(IMAGE):$(VERSION)
