Ctrl K
ComunidadGuías…

Makefile

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

Archivo original ↗Preguntar a KiBot
# This is the Coder Makefile. The build directory for most tasks is `build/`.
#
# These are the targets you're probably looking for:
# - clean
# - build-fat: builds all "fat" binaries for all architectures
# - build-slim: builds all "slim" binaries (no frontend or slim binaries
#   embedded) for all architectures
# - release: simulate a release (mostly, does not push images)
# - build/coder(-slim)?_${os}_${arch}(.exe)?: build a single fat binary
# - build/coder_${os}_${arch}.(zip|tar.gz): build a release archive
# - build/coder_linux_${arch}.(apk|deb|rpm): build a release Linux package
# - build/coder_${version}_linux_${arch}.tag: build a release Linux Docker image
# - build/coder_helm.tgz: build a release Helm chart

.DEFAULT_GOAL := build-fat

# Use a single bash shell for each job, and immediately exit on failure
SHELL := bash
.SHELLFLAGS := -ceu
.ONESHELL:

# When MAKE_TIMED=1, replace SHELL with a wrapper that prints
# elapsed wall-clock time for each recipe. pre-commit and pre-push
# set this on their sub-makes so every parallel job reports its
# duration. Ad-hoc usage: make MAKE_TIMED=1 test
# The target name is prefixed with "target:" because GNU make 4.4 with
# .ONESHELL mangles the arguments when the first word of .SHELLFLAGS is
# a shell builtin such as test, exec, or cd. SHELL then receives
# "/bin/sh -c '<target> -ceu' <recipe>", which skips the recipe for the
# test target or crashes make.
ifdef MAKE_TIMED
SHELL := $(CURDIR)/scripts/lib/timed-shell.sh
.SHELLFLAGS = target:$@ -ceu
export MAKE_TIMED
export MAKE_LOGDIR
endif

# This doesn't work on directories.
# See https://stackoverflow.com/questions/25752543/make-delete-on-error-for-directory-targets
.DELETE_ON_ERROR:

# Protect git-tracked generated files from deletion on interrupt.
# .DELETE_ON_ERROR is desirable for most targets but for files that
# are committed to git and serve as inputs to other rules, deletion
# is worse than a stale file — `git restore` is the recovery path.
.PRECIOUS: \
	coderd/database/dump.sql \
	coderd/database/querier.go \
	coderd/database/unique_constraint.go \
	coderd/database/dbmetrics/querymetrics.go \
	coderd/database/dbauthz/dbauthz.go \
	coderd/database/dbmock/dbmock.go \
	coderd/database/pubsub/psmock/psmock.go \
	agent/agentcontainers/acmock/acmock.go \
	coderd/httpmw/loggermw/loggermock/loggermock.go \
	codersdk/workspacesdk/agentconnmock/agentconnmock.go \
	tailnet/tailnettest/coordinatormock.go \
	tailnet/tailnettest/coordinateemock.go \
	tailnet/tailnettest/workspaceupdatesprovidermock.go \
	tailnet/tailnettest/subscriptionmock.go \
	coderd/aibridged/aibridgedmock/clientmock.go \
	coderd/aibridged/aibridgedmock/poolmock.go \
	tailnet/proto/tailnet.pb.go \
	agent/proto/agent.pb.go \
	agent/agentsocket/proto/agentsocket.pb.go \
	agent/boundarylogproxy/codec/boundary.pb.go \
	provisionersdk/proto/provisioner.pb.go \
	provisionerd/proto/provisionerd.pb.go \
	vpn/vpn.pb.go \
	coderd/aibridged/proto/aibridged.pb.go \
	site/src/api/typesGenerated.ts \
	site/e2e/provisionerGenerated.ts \
	site/src/api/chatModelOptionsGenerated.json \
	site/src/api/appFamiliesGenerated.ts \
	site/src/api/rbacresourcesGenerated.ts \
	site/src/api/countriesGenerated.ts \
	site/src/theme/icons.json \
	examples/examples.gen.json \
	docs/manifest.json \
	docs/manifest/generated/cli.json \
	docs/manifest/generated/rest-api.json \
	docs/admin/integrations/prometheus.md \
	docs/admin/security/audit-logs.md \
	docs/admin/setup/configuration-reference.md \
	docs/reference/api-key-scopes.md \
	docs/reference/cli/index.md \
	coderd/apidoc/swagger.json \
	coderd/rbac/object_gen.go \
	coderd/rbac/scopes_constants_gen.go \
	codersdk/rbacresources_gen.go \
	codersdk/apikey_scopes_gen.go \
	codersdk/dbenums_gen.go

# atomic_write runs a command, captures stdout into a temp file, and
# atomically replaces $@. An optional second argument is a formatting
# command that receives the temp file path as its argument.
# Usage: $(call atomic_write,GENERATE_CMD[,FORMAT_CMD])
define atomic_write
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && \
		$(1) > "$$tmpfile" && \
		$(if $(2),$(2) "$$tmpfile" &&) \
		mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"
endef

# CLI doc generation reflects over the assembled CLI tree. Track command
# definitions plus the top-level SDK types they expose in help text and flag
# values, without pulling in unrelated generated sources.
CLIDOC_SRC_FILES := \
	$(shell find ./cli ./enterprise/cli -type f -name '*.go' -not -name '*_test.go') \
	$(wildcard codersdk/*.go) \
	$(wildcard buildinfo/*.go)

CLIDOCGEN_INPUTS := \
	$(wildcard scripts/clidocgen/*.go) \
	$(filter-out %_test.go,$(wildcard scripts/docgenenv/*.go)) \
	scripts/clidocgen/command.tpl \
	$(CLIDOC_SRC_FILES)

# Hand-edited YAML sidebar sources that docsmanifestgen compiles, together
# with the generated fragments under docs/manifest/generated, into
# docs/manifest.json. Each file holds one route and one level of children, so
# the sources nest in subdirectories.
DOCS_MANIFEST_SOURCES := $(shell find docs/manifest -type f -name '*.yml' -not -path 'docs/manifest/generated/*')

DOCSMANIFESTGEN_INPUTS := \
	$(filter-out %_test.go,$(wildcard scripts/docsmanifestgen/*.go)) \
	$(filter-out %_test.go,$(wildcard scripts/docgenenv/*.go))

# Helper binaries that import repo packages need their compile-time inputs on
# the binary target. Most generated outputs keep these binaries as order-only
# prereqs, so stale binaries otherwise survive source changes.
RBAC_GO_FILES := \
	$(wildcard coderd/rbac/*.go) \
	$(wildcard coderd/rbac/policy/*.go)

DBDUMP_INPUTS := \
	$(wildcard coderd/database/migrations/*.go) \
	$(wildcard coderd/database/migrations/*.sql)

# Exclude generated RBAC files to avoid cycles with typegen outputs. The
# output rules still order generated RBAC prerequisites where needed.
TYPEGEN_RBAC_GO_FILES := \
	$(filter-out coderd/rbac/%_gen.go,$(wildcard coderd/rbac/*.go)) \
	$(wildcard coderd/rbac/policy/*.go)

TYPEGEN_INPUTS := \
	$(wildcard scripts/typegen/*.go) \
	$(wildcard scripts/typegen/*.gotmpl) \
	$(wildcard scripts/typegen/*.tstmpl) \
	$(TYPEGEN_RBAC_GO_FILES) \
	$(wildcard coderd/util/strings/*.go) \
	codersdk/countries.go

# Helper binary targets. Built with go build -o to avoid caching
# link-stage executables in GOCACHE. Each binary is a real Make
# target so parallel -j builds serialize correctly instead of
# racing on the same output path.

_gen/bin/apitypings: $(wildcard scripts/apitypings/*.go) $(wildcard codersdk/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/apitypings

_gen/bin/auditdocgen: $(wildcard scripts/auditdocgen/*.go) $(wildcard enterprise/audit/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/auditdocgen

_gen/bin/check-scopes: $(wildcard scripts/check-scopes/*.go) $(RBAC_GO_FILES) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/check-scopes

# clidocgen reflects over the full CLI tree, so it must rebuild when its
# command definitions, flag types, or embedded template change.
_gen/bin/clidocgen: $(CLIDOCGEN_INPUTS) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/clidocgen

_gen/bin/docsmanifestgen: $(DOCSMANIFESTGEN_INPUTS) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/docsmanifestgen

# configdocgen reflects over codersdk.DeploymentValues to produce the
# configuration reference page.
_gen/bin/configdocgen: $(wildcard scripts/configdocgen/*.go) $(wildcard codersdk/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/configdocgen

_gen/bin/dbdump: $(wildcard coderd/database/gen/dump/*.go) $(DBDUMP_INPUTS) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./coderd/database/gen/dump

_gen/bin/examplegen: $(wildcard scripts/examplegen/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/examplegen

_gen/bin/gensite: $(wildcard scripts/gensite/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/gensite

_gen/bin/apikeyscopesgen: $(wildcard scripts/apikeyscopesgen/*.go) $(RBAC_GO_FILES) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/apikeyscopesgen

# scopesdocgen reads the RBAC scope catalog to produce the API key scopes
# reference page.
_gen/bin/scopesdocgen: $(wildcard scripts/scopesdocgen/*.go) $(wildcard scripts/docgenenv/*.go) $(RBAC_GO_FILES) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/scopesdocgen

_gen/bin/aibridgepricesgen: $(wildcard scripts/aibridgepricesgen/*.go) scripts/aibridgepricesgen/curation.json | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/aibridgepricesgen

_gen/bin/metricsdocgen: $(wildcard scripts/metricsdocgen/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/metricsdocgen

_gen/bin/metricsdocgen-scanner: $(wildcard scripts/metricsdocgen/scanner/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/metricsdocgen/scanner

_gen/bin/modeloptionsgen: $(wildcard scripts/modeloptionsgen/*.go) $(wildcard codersdk/*.go) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/modeloptionsgen

_gen/bin/typegen: $(TYPEGEN_INPUTS) | _gen
	@mkdir -p _gen/bin
	go build -o $@ ./scripts/typegen

# Shared temp directory for atomic writes. Lives at the project root
# so all targets share the same filesystem, and is gitignored.
# Order-only prerequisite: recipes that need it depend on | _gen
_gen:
	mkdir -p _gen

# Don't print the commands in the file unless you specify VERBOSE. This is
# essentially the same as putting "@" at the start of each line.
ifndef VERBOSE
.SILENT:
endif

# Create the output directories if they do not exist.
$(shell mkdir -p build site/out/bin)

GOOS         := $(shell go env GOOS)
GOARCH       := $(shell go env GOARCH)
GOOS_BIN_EXT := $(if $(filter windows, $(GOOS)),.exe,)
VERSION      := $(shell ./scripts/version.sh)

POSTGRES_VERSION ?= 17
POSTGRES_IMAGE   ?= us-docker.pkg.dev/coder-v2-images-public/public/postgres:$(POSTGRES_VERSION)
# CI test jobs set this to "none": nothing reads the statement log there.
TEST_POSTGRES_LOG_STATEMENT ?= all

# Limit parallel Make jobs in pre-commit/pre-push. Defaults to
# nproc/4 (min 2) since test, lint, and build targets have internal
# parallelism. Override: make pre-push PARALLEL_JOBS=8
PARALLEL_JOBS ?= $(shell n=$$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 8); echo $$(( n / 4 > 2 ? n / 4 : 2 )))

# Use the highest ZSTD compression level in release builds to
# minimize artifact size. For non-release CI builds (e.g. main
# branch preview), use multithreaded level 6 which is ~99% faster
# at the cost of ~30% larger archives.
ifeq ($(CODER_RELEASE),true)
ZSTDFLAGS := -22 --ultra
else
ZSTDFLAGS := -6 -T0
endif

# Common paths to exclude from find commands, this rule is written so
# that it can be it can be used in a chain of AND statements (meaning
# you can simply write `find . $(FIND_EXCLUSIONS) -name thing-i-want`).
# Note, all find statements should be written with `.` or `./path` as
# the search path so that these exclusions match.
FIND_EXCLUSIONS= \
	-not \( \( -path '*/.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path '*/node_modules/*' -o -path '*/out/*' -o -path './coderd/apidoc/*' -o -path '*/.next/*' -o -path '*/.terraform/*' -o -path './_gen/*' \) -prune \)

# Source files used for make targets, evaluated on use.
GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -name '*_test.go')

# All the shell files in the repo, excluding ignored files.
SHELL_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.sh')

MIGRATION_FILES := $(shell find ./coderd/database/migrations/ -maxdepth 1 $(FIND_EXCLUSIONS) -type f -name '*.sql')
FIXTURE_FILES := $(shell find ./coderd/database/migrations/testdata/fixtures/ $(FIND_EXCLUSIONS) -type f -name '*.sql')

# Ensure we don't use the user's git configs which might cause side-effects
GIT_FLAGS = GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null

# All ${OS}_${ARCH} combos we build for. Windows binaries have the .exe suffix.
OS_ARCHES := \
	linux_amd64 linux_arm64 linux_armv7 \
	darwin_amd64 darwin_arm64 \
	windows_amd64.exe windows_arm64.exe

# Archive formats and their corresponding ${OS}_${ARCH} combos.
ARCHIVE_TAR_GZ := linux_amd64 linux_arm64 linux_armv7
ARCHIVE_ZIP    := \
	darwin_amd64 darwin_arm64 \
	windows_amd64 windows_arm64

# All package formats we build and the ${OS}_${ARCH} combos we build them for.
PACKAGE_FORMATS   := apk deb rpm
PACKAGE_OS_ARCHES := linux_amd64 linux_armv7 linux_arm64

# All architectures we build Docker images for (Linux only).
DOCKER_ARCHES := amd64 arm64 armv7

# Computed variables based on the above.
CODER_SLIM_BINARIES      := $(addprefix build/coder-slim_$(VERSION)_,$(OS_ARCHES))
CODER_FAT_BINARIES       := $(addprefix build/coder_$(VERSION)_,$(OS_ARCHES))
CODER_ALL_BINARIES       := $(CODER_SLIM_BINARIES) $(CODER_FAT_BINARIES)
CODER_TAR_GZ_ARCHIVES    := $(foreach os_arch, $(ARCHIVE_TAR_GZ), build/coder_$(VERSION)_$(os_arch).tar.gz)
CODER_ZIP_ARCHIVES       := $(foreach os_arch, $(ARCHIVE_ZIP), build/coder_$(VERSION)_$(os_arch).zip)
CODER_ALL_ARCHIVES       := $(CODER_TAR_GZ_ARCHIVES) $(CODER_ZIP_ARCHIVES)
CODER_ALL_PACKAGES       := $(foreach os_arch, $(PACKAGE_OS_ARCHES), $(addprefix build/coder_$(VERSION)_$(os_arch).,$(PACKAGE_FORMATS)))
CODER_ARCH_IMAGES        := $(foreach arch, $(DOCKER_ARCHES), build/coder_$(VERSION)_linux_$(arch).tag)
CODER_ARCH_IMAGES_PUSHED := $(addprefix push/, $(CODER_ARCH_IMAGES))
CODER_MAIN_IMAGE         := build/coder_$(VERSION)_linux.tag

CODER_SLIM_NOVERSION_BINARIES     := $(addprefix build/coder-slim_,$(OS_ARCHES))
CODER_FAT_NOVERSION_BINARIES      := $(addprefix build/coder_,$(OS_ARCHES))
CODER_ALL_NOVERSION_IMAGES        := $(foreach arch, $(DOCKER_ARCHES), build/coder_linux_$(arch).tag) build/coder_linux.tag
CODER_ALL_NOVERSION_IMAGES_PUSHED := $(addprefix push/, $(CODER_ALL_NOVERSION_IMAGES))

# If callers are only building Docker images and not the packages and archives,
# we can skip those prerequisites as they are not actually required and only
# specified to avoid concurrent write failures.
ifdef DOCKER_IMAGE_NO_PREREQUISITES
CODER_ARCH_IMAGE_PREREQUISITES :=
else
CODER_ARCH_IMAGE_PREREQUISITES := \
	build/coder_$(VERSION)_%.apk \
	build/coder_$(VERSION)_%.deb \
	build/coder_$(VERSION)_%.rpm \
	build/coder_$(VERSION)_%.tar.gz
endif


clean:
	rm -rf build/ site/build/ site/out/
	rm -rf _gen/bin
	mkdir -p build/
	git restore site/out/
.PHONY: clean

build-slim: $(CODER_SLIM_BINARIES)
.PHONY: build-slim

build-fat build-full build: $(CODER_FAT_BINARIES)
.PHONY: build-fat build-full build

release: $(CODER_FAT_BINARIES) $(CODER_ALL_ARCHIVES) $(CODER_ALL_PACKAGES) $(CODER_ARCH_IMAGES) build/coder_helm_$(VERSION).tgz
.PHONY: release

build/coder-slim_$(VERSION)_checksums.sha1: site/out/bin/coder.sha1
	cp "$<" "$@"

site/out/bin/coder.sha1: $(CODER_SLIM_BINARIES)
	pushd ./site/out/bin
		openssl dgst -r -sha1 coder-* | tee coder.sha1
	popd

build/coder-slim_$(VERSION).tar: build/coder-slim_$(VERSION)_checksums.sha1 $(CODER_SLIM_BINARIES)
	pushd ./site/out/bin
		# Group binaries by architecture so that zstd's long-distance matching
		# window can deduplicate code shared across operating systems.
		tar cf "../../../build/$(@F)" $$(ls coder-* | sort -t- -k3)
	popd

	# delete the uncompressed binaries from the embedded dir
	rm -f site/out/bin/coder-*

site/out/bin/coder.tar.zst: build/coder-slim_$(VERSION).tar.zst
	cp "$<" "$@"

build/coder-slim_$(VERSION).tar.zst: build/coder-slim_$(VERSION).tar
	zstd $(ZSTDFLAGS) \
		--force \
		--long \
		--no-progress \
		-o "build/coder-slim_$(VERSION).tar.zst" \
		"build/coder-slim_$(VERSION).tar"

# Redirect from version-less targets to the versioned ones. There is a similar
# target for slim binaries below.
#
# Called like this:
#   make build/coder_linux_amd64
#   make build/coder_windows_amd64.exe
$(CODER_FAT_NOVERSION_BINARIES): build/coder_%: build/coder_$(VERSION)_%
	rm -f "$@"
	ln "$<" "$@"

# Same as above, but for slim binaries.
#
# Called like this:
#   make build/coder-slim_linux_amd64
#   make build/coder-slim_windows_amd64.exe
$(CODER_SLIM_NOVERSION_BINARIES): build/coder-slim_%: build/coder-slim_$(VERSION)_%
	rm -f "$@"
	ln "$<" "$@"

# "fat" binaries always depend on the site and the compressed slim binaries.
$(CODER_FAT_BINARIES): \
	site/out/index.html \
	site/out/bin/coder.sha1 \
	site/out/bin/coder.tar.zst

# This is a handy block that parses the target to determine whether it's "slim"
# or "fat", which OS was specified and which architecture was specified.
#
# It populates the following variables: mode, os, arch_ext, arch, ext (without
# dot).
define get-mode-os-arch-ext =
	mode="$$([[ "$@" = build/coder-slim* ]] && echo "slim" || echo "fat")"
	os="$$(echo $@ | cut -d_ -f3)"
	arch_ext="$$(echo $@ | cut -d_ -f4)"
	if [[ "$$arch_ext" == *.* ]]; then
		arch="$$(echo $$arch_ext | cut -d. -f1)"
		ext="$${arch_ext#*.}"
	else
		arch="$$arch_ext"
		ext=""
	fi
endef

# This task handles all builds, for both "fat" and "slim" binaries. It parses
# the target name to get the metadata for the build, so it must be specified in
# this format:
#     build/coder(-slim)?_${version}_${os}_${arch}(.exe)?
#
# You should probably use the non-version targets above instead if you're
# calling this manually.
$(CODER_ALL_BINARIES): go.mod go.sum \
	$(GO_SRC_FILES) \
	$(shell find ./examples/templates) \
	site/static/error.html

	$(get-mode-os-arch-ext)
	if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
		echo "ERROR: Invalid build binary extension" 1>&2
		exit 1
	fi
	if [[ "$$os" == "windows" ]] && [[ "$$ext" != exe ]]; then
		echo "ERROR: Windows binaries must have an .exe extension." 1>&2
		exit 1
	fi

	build_args=( \
		--os "$$os" \
		--arch "$$arch" \
		--version "$(VERSION)" \
		--output "$@" \
	)
	if [ "$$mode" == "slim" ]; then
		build_args+=(--slim)
	fi

	./scripts/build_go.sh "$${build_args[@]}"

	if [[ "$$mode" == "slim" ]]; then
		dot_ext=""
		if [[ "$$ext" != "" ]]; then
			dot_ext=".$$ext"
		fi

		cp "$@" "./site/out/bin/coder-$$os-$$arch$$dot_ext"

		if [[ "$${CODER_SIGN_GPG:-0}" == "1" ]]; then
			cp "$@.asc" "./site/out/bin/coder-$$os-$$arch$$dot_ext.asc"
		fi
	fi

# This task builds all archives. It parses the target name to get the metadata
# for the build, so it must be specified in this format:
#     build/coder_${version}_${os}_${arch}.${format}
#
# The following OS/arch/format combinations are supported:
#     .tar.gz: linux_amd64, linux_arm64, linux_armv7
#     .zip:    darwin_amd64, darwin_arm64, windows_amd64, windows_arm64
#
# This depends on all fat binaries because it's difficult to do dynamic
# dependencies due to the .exe requirement on Windows. These targets are
# typically only used during release anyways.
$(CODER_ALL_ARCHIVES): $(CODER_FAT_BINARIES)
	$(get-mode-os-arch-ext)
	bin_ext=""
	if [[ "$$os" == "windows" ]]; then
		bin_ext=".exe"
	fi

	./scripts/archive.sh \
		--format "$$ext" \
		--os "$$os" \
		--output "$@" \
		"build/coder_$(VERSION)_$${os}_$${arch}$${bin_ext}"

# This task builds all packages. It parses the target name to get the metadata
# for the build, so it must be specified in this format:
#     build/coder_${version}_linux_${arch}.${format}
#
# Supports apk, deb, rpm for all linux targets.
#
# This depends on all Linux fat binaries and archives because it's difficult to
# do dynamic dependencies due to the extensions in the filenames. These targets
# are typically only used during release anyways.
#
# Packages need to run after the archives are built, otherwise they cause tar
# errors like "file changed as we read it".
CODER_PACKAGE_DEPS := $(foreach os_arch, $(PACKAGE_OS_ARCHES), build/coder_$(VERSION)_$(os_arch) build/coder_$(VERSION)_$(os_arch).tar.gz)
$(CODER_ALL_PACKAGES): $(CODER_PACKAGE_DEPS)
	$(get-mode-os-arch-ext)

	./scripts/package.sh \
		--arch "$$arch" \
		--format "$$ext" \
		--version "$(VERSION)" \
		--output "$@" \
		"build/coder_$(VERSION)_$${os}_$${arch}"

# This task builds a Windows amd64 installer. Depends on makensis.
build/coder_$(VERSION)_windows_amd64_installer.exe: build/coder_$(VERSION)_windows_amd64.exe
	./scripts/build_windows_installer.sh \
		--version "$(VERSION)" \
		--output "$@" \
		"$<"

# Redirect from version-less Docker image targets to the versioned ones.
#
# Called like this:
#   make build/coder_linux_amd64.tag
$(CODER_ALL_NOVERSION_IMAGES): build/coder_%: build/coder_$(VERSION)_%
.PHONY: $(CODER_ALL_NOVERSION_IMAGES)

# Redirect from version-less push Docker image targets to the versioned ones.
#
# Called like this:
#   make push/build/coder_linux_amd64.tag
$(CODER_ALL_NOVERSION_IMAGES_PUSHED): push/build/coder_%: push/build/coder_$(VERSION)_%
.PHONY: $(CODER_ALL_NOVERSION_IMAGES_PUSHED)

# This task builds all Docker images. It parses the target name to get the
# metadata for the build, so it must be specified in this format:
#     build/coder_${version}_${os}_${arch}.tag
#
# Supports linux_amd64, linux_arm64, linux_armv7.
#
# Images need to run after the archives and packages are built, otherwise they
# cause errors like "file changed as we read it".
$(CODER_ARCH_IMAGES): build/coder_$(VERSION)_%.tag: build/coder_$(VERSION)_% $(CODER_ARCH_IMAGE_PREREQUISITES)
	$(get-mode-os-arch-ext)

	image_tag="$$(./scripts/image_tag.sh --arch "$$arch" --version "$(VERSION)")"
	./scripts/build_docker.sh \
		--arch "$$arch" \
		--target "$$image_tag" \
		--version "$(VERSION)" \
		"build/coder_$(VERSION)_$${os}_$${arch}"

	echo "$$image_tag" > "$@"

# Multi-arch Docker image. This requires all architecture-specific images to be
# built AND pushed.
$(CODER_MAIN_IMAGE): $(CODER_ARCH_IMAGES_PUSHED)
	image_tag="$$(./scripts/image_tag.sh --version "$(VERSION)")"
	./scripts/build_docker_multiarch.sh \
		--target "$$image_tag" \
		--version "$(VERSION)" \
		$(foreach img, $^, "$$(cat "$(img:push/%=%)")")

	echo "$$image_tag" > "$@"

# Push a Docker image.
$(CODER_ARCH_IMAGES_PUSHED): push/%: %
	image_tag="$$(cat "$<")"
	docker push "$$image_tag"
.PHONY: $(CODER_ARCH_IMAGES_PUSHED)

# Push the multi-arch Docker manifest.
push/$(CODER_MAIN_IMAGE): $(CODER_MAIN_IMAGE)
	image_tag="$$(cat "$<")"
	docker manifest push "$$image_tag"
.PHONY: push/$(CODER_MAIN_IMAGE)

# Helm charts that are available
charts = coder provisioner ai-gateway

# Shortcut for Helm chart package.
$(foreach chart,$(charts),build/$(chart)_helm.tgz): build/%_helm.tgz: build/%_helm_$(VERSION).tgz
	rm -f "$@"
	ln "$<" "$@"

# Helm chart package.
$(foreach chart,$(charts),build/$(chart)_helm_$(VERSION).tgz): build/%_helm_$(VERSION).tgz:
	./scripts/helm.sh \
		--version "$(VERSION)" \
		--chart $* \
		--output "$@"

node_modules/.installed: package.json pnpm-lock.yaml pnpm-workspace.yaml
	./scripts/pnpm_install.sh
	touch "$@"

offlinedocs/node_modules/.installed: offlinedocs/package.json offlinedocs/pnpm-lock.yaml offlinedocs/pnpm-workspace.yaml
	(cd offlinedocs/ && ../scripts/pnpm_install.sh)
	touch "$@"

site/node_modules/.installed: site/package.json site/pnpm-lock.yaml site/pnpm-workspace.yaml
	(cd site/ && ../scripts/pnpm_install.sh)
	touch "$@"

scripts/apidocgen/node_modules/.installed: scripts/apidocgen/package.json scripts/apidocgen/pnpm-lock.yaml scripts/apidocgen/pnpm-workspace.yaml
	(cd scripts/apidocgen && ../../scripts/pnpm_install.sh)
	touch "$@"

SITE_GEN_FILES := \
	site/src/api/typesGenerated.ts \
	site/src/api/appFamiliesGenerated.ts \
	site/src/api/rbacresourcesGenerated.ts \
	site/src/api/countriesGenerated.ts \
	site/src/api/chatModelOptionsGenerated.json \
	site/src/theme/icons.json

site/out/index.html: \
	site/node_modules/.installed \
	site/static/install.sh \
	$(SITE_GEN_FILES) \
	$(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
	cd site/
	# prevents this directory from getting to big, and causing "too much data" errors
	rm -rf out/assets/
	pnpm build

offlinedocs/out/index.html: offlinedocs/node_modules/.installed $(shell find ./offlinedocs $(FIND_EXCLUSIONS) -type f) $(shell find ./docs $(FIND_EXCLUSIONS) -type f | sed 's: :\\ :g')
	cd offlinedocs/
	../scripts/pnpm_install.sh
	pnpm export

build/coder_docs_$(VERSION).tgz: offlinedocs/out/index.html
	tar -czf "$@" -C offlinedocs/out .

install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
	install_dir="$$(go env GOPATH)/bin"
	output_file="$${install_dir}/coder$(GOOS_BIN_EXT)"

	mkdir -p "$$install_dir"
	cp "$<" "$$output_file"
.PHONY: install

# Only wildcard the go files in the develop directory to avoid rebuilds
# when project files are changd. Technically changes to some imports may
# not be detected, but it's unlikely to cause any issues.
build/.bin/develop: go.mod go.sum $(wildcard scripts/develop/*.go)
	CGO_ENABLED=0 go build -o $@ ./scripts/develop

BOLD := $(shell tput bold 2>/dev/null)
GREEN := $(shell tput setaf 2 2>/dev/null)
RED := $(shell tput setaf 1 2>/dev/null)
YELLOW := $(shell tput setaf 3 2>/dev/null)
DIM := $(shell tput dim 2>/dev/null || tput setaf 8 2>/dev/null)
RESET := $(shell tput sgr0 2>/dev/null)

fmt: fmt/ts fmt/go fmt/terraform fmt/shfmt fmt/biome fmt/markdown fmt/docs-manifest
.PHONY: fmt

# Subset of fmt that does not require Go or Node toolchains.
fmt-light: fmt/shfmt fmt/terraform fmt/markdown
.PHONY: fmt-light

fmt/go:
ifdef FILE
	# Format single file
	if [[ -f "$(FILE)" ]] && [[ "$(FILE)" == *.go ]] && ! grep -q "DO NOT EDIT" "$(FILE)"; then \
		echo "$(GREEN)==>$(RESET) $(BOLD)fmt/go$(RESET) $(FILE)"; \
		./scripts/format_go_file.sh "$(FILE)"; \
	fi
else
	go mod tidy
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/go$(RESET)"
	# VS Code users should check out
	# https://github.com/mvdan/gofumpt#visual-studio-code
	find . $(FIND_EXCLUSIONS) -type f -name '*.go' -print0 | \
		xargs -0 grep -E --null -L '^// Code generated .* DO NOT EDIT\.$$' | \
		xargs -0 ./scripts/format_go_file.sh
endif
.PHONY: fmt/go

fmt/ts: site/node_modules/.installed
ifdef FILE
	# Format single TypeScript/JavaScript file
	if [[ -f "$(FILE)" ]] && [[ "$(FILE)" == *.ts ]] || [[ "$(FILE)" == *.tsx ]] || [[ "$(FILE)" == *.js ]] || [[ "$(FILE)" == *.jsx ]]; then \
		echo "$(GREEN)==>$(RESET) $(BOLD)fmt/ts$(RESET) $(FILE)"; \
		(cd site/ && pnpm exec biome format --write "../$(FILE)"); \
	fi
else
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/ts$(RESET)"
	cd site
# Avoid writing files in CI to reduce file write activity
ifdef CI
	pnpm run check --linter-enabled=false
else
	pnpm run check:fix
endif
endif
.PHONY: fmt/ts

fmt/biome: site/node_modules/.installed
ifdef FILE
	# Format single file with biome
	if [[ -f "$(FILE)" ]] && [[ "$(FILE)" == *.ts ]] || [[ "$(FILE)" == *.tsx ]] || [[ "$(FILE)" == *.js ]] || [[ "$(FILE)" == *.jsx ]]; then \
		echo "$(GREEN)==>$(RESET) $(BOLD)fmt/biome$(RESET) $(FILE)"; \
		(cd site/ && pnpm exec biome format --write "../$(FILE)"); \
	fi
else
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/biome$(RESET)"
	cd site/
# Avoid writing files in CI to reduce file write activity
ifdef CI
	pnpm run format:check
else
	pnpm run format
endif
endif
.PHONY: fmt/biome

fmt/terraform: $(wildcard *.tf)
ifdef FILE
	# Format single Terraform file
	if [[ -f "$(FILE)" ]] && [[ "$(FILE)" == *.tf ]] || [[ "$(FILE)" == *.tfvars ]]; then \
		echo "$(GREEN)==>$(RESET) $(BOLD)fmt/terraform$(RESET) $(FILE)"; \
		terraform fmt "$(FILE)"; \
	fi
else
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/terraform$(RESET)"
	terraform fmt -recursive
endif
.PHONY: fmt/terraform

fmt/shfmt: $(SHELL_SRC_FILES)
ifdef FILE
	# Format single shell script
	if [[ -f "$(FILE)" ]] && [[ "$(FILE)" == *.sh ]]; then \
		echo "$(GREEN)==>$(RESET) $(BOLD)fmt/shfmt$(RESET) $(FILE)"; \
		shfmt -w "$(FILE)"; \
	fi
else
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/shfmt$(RESET)"
# Only do diff check in CI, errors on diff.
ifdef CI
	shfmt -d $(SHELL_SRC_FILES)
else
	shfmt -w $(SHELL_SRC_FILES)
endif
endif
.PHONY: fmt/shfmt

fmt/markdown: node_modules/.installed
ifdef FILE
	# Format single markdown file
	if [[ -f "$(FILE)" ]] && [[ "$(FILE)" == *.md ]]; then \
		echo "$(GREEN)==>$(RESET) $(BOLD)fmt/markdown$(RESET) $(FILE)"; \
		pnpm exec markdown-table-formatter "$(FILE)"; \
	fi
else
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/markdown$(RESET)"
	pnpm format-docs
endif
.PHONY: fmt/markdown

# Formats the YAML sidebar sources under docs/manifest: canonical key order,
# two-space indentation, and quoting only where YAML needs it.
fmt/docs-manifest: _gen/bin/docsmanifestgen
	echo "$(GREEN)==>$(RESET) $(BOLD)fmt/docs-manifest$(RESET)"
	_gen/bin/docsmanifestgen fmt
.PHONY: fmt/docs-manifest

# Formats the sidebar sources and rebuilds docs/manifest.json from them and
# the committed generated fragments. Run this after editing the sidebar. It
# skips regenerating the CLI and REST API fragments, which make gen does.
gen/docs-manifest: fmt/docs-manifest site/node_modules/.installed | _gen
	$(build-docs-manifest)
.PHONY: gen/docs-manifest

# Note: we don't run zizmor in the lint target because it takes a while.
# GitHub Actions linters are run in a separate CI job (lint-actions) that only
# triggers when workflow files change, so we skip them here when CI=true.
LINT_ACTIONS_TARGETS := $(if $(CI),,lint/actions/actionlint)
lint: lint/shellcheck lint/go lint/go/new-code lint/ts lint/examples lint/helm lint/site-icons lint/markdown lint/docs-html lint/docs-manifest lint/docs-redirects lint/style-claims lint/check-scopes lint/check-experiment-keys lint/migrations lint/bootstrap lint/architecture lint/emdash lint/agents lint/mise-versions lint/docs-gen-filter $(LINT_ACTIONS_TARGETS)
.PHONY: lint

# Fast lint subset for lightweight hooks. Some targets use mise-managed tools.
lint-light: lint/shellcheck lint/markdown lint/helm lint/bootstrap lint/migrations lint/actions/actionlint lint/typos lint/emdash lint/docs-redirects lint/style-claims lint/mise-versions
.PHONY: lint-light

lint/site-icons:
	./scripts/check_site_icons.sh
.PHONY: lint/site-icons

lint/ts: site/node_modules/.installed
	cd site/
	pnpm lint
.PHONY: lint/ts

# Cap cold-cache golangci-lint on high-core hosts to stay below memory limits.
GO_LINT_CONCURRENCY := $(shell n=$$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1); echo $$(( n < 8 ? n : 8 )))
GO_LINT_MEMLIMIT ?= 8GiB

# --allow-serial-runners makes lint/go and lint/go/new-code wait for each
# other's golangci-lint lock when `make -j lint` starts them together.
lint/go:
	./scripts/check_golangci_exclusions.sh
	GOMEMLIMIT="$${GOMEMLIMIT:-$(GO_LINT_MEMLIMIT)}" golangci-lint run --allow-serial-runners --concurrency="$(GO_LINT_CONCURRENCY)"
	paralleltestctx -custom-funcs="testutil.Context,chatdTestContext" ./...
	go run ./scripts/intxcheck ./...
.PHONY: lint/go

# Revision whose merge base with HEAD bounds the lines lint/go/new-code checks.
GO_LINT_NEW_CODE_BASE ?= origin/main

# Rules the existing tree does not pass yet, applied only to changed lines.
# golangci-lint matches changed lines against symlink-resolved file paths
# made relative to PWD, so a checkout reached through a symlink would
# silently report nothing; `pwd -P` resolves the working directory first.
lint/go/new-code:
	cd "$$(pwd -P)"
	GOMEMLIMIT="$${GOMEMLIMIT:-$(GO_LINT_MEMLIMIT)}" golangci-lint run --allow-serial-runners --concurrency="$(GO_LINT_CONCURRENCY)" \
		--config=.golangci.new-code.yaml --new-from-merge-base="$(GO_LINT_NEW_CODE_BASE)"
.PHONY: lint/go/new-code

lint/examples: | _gen/bin/examplegen
	_gen/bin/examplegen -lint
.PHONY: lint/examples

# Use shfmt to determine the shell files, takes editorconfig into consideration.
lint/shellcheck: $(SHELL_SRC_FILES)
	echo "--- shellcheck"
	shellcheck --external-sources $(SHELL_SRC_FILES)
.PHONY: lint/shellcheck

lint/bootstrap:
	bash scripts/check_bootstrap_quotes.sh
.PHONY: lint/bootstrap

lint/emdash:
	bash scripts/check_emdash.sh
.PHONY: lint/emdash

# Fails when docs Markdown contains invalid inline HTML the docs site drops or
# mangles: swallowed angle-bracket placeholders (e.g. <region>), void-element
# end tags (</br>), capitalized or unregistered component tags (e.g. <Image>),
# and unclosed container tags. Also fails on table header cells with no text.
lint/docs-html:
	echo "--- check for invalid inline HTML and empty table headers in docs"
	go run ./scripts/docshtmlcheck
.PHONY: lint/docs-html

# Fails when the YAML sidebar sources under docs/manifest are unformatted or
# invalid: unknown or misordered keys, missing pages, overlays that match no
# generated route, source files that nothing references, or a page description
# (in the sources, a generated fragment, or page front matter) that doesn't end
# with a period.
lint/docs-manifest: _gen/bin/docsmanifestgen
	echo "--- check docs sidebar sources"
	_gen/bin/docsmanifestgen check
.PHONY: lint/docs-manifest

# Fails when docs/redirects.json, the redirects the docs site applies for pages
# that moved or were removed, is malformed, points at a page that is not in
# docs/manifest.json, hides a live page, or has duplicate, chained, or looping
# rules. Also fails when a route that worked on the base branch or on the live
# site for the target branch no longer resolves. The live page list needs
# network access; pass -offline to skip it. Passes when the file does not
# exist. See scripts/docsredirectscheck/README.md.
lint/docs-redirects:
	echo "--- check docs/redirects.json"
	go run ./scripts/docsredirectscheck
.PHONY: lint/docs-redirects

# Fails when the style guide claims a prose rule is enforced by tooling that is
# not enabled, when an enabled rule has no style guide section, or when the
# coverage tables on the style guide landing page drift from the annotations.
# Vale checks a subset of the guide and runs advisory, so the guide's own claims
# about what is enforced are the only signal an author has; this keeps them
# true. See scripts/styleclaims/README.md.
lint/style-claims:
	echo "--- check docs style guide enforcement claims"
	go run ./scripts/styleclaims
.PHONY: lint/style-claims

# Fails when documented --experiments and CODER_EXPERIMENTS values are not in
# codersdk.ExperimentsKnown.
lint/check-experiment-keys:
	echo "--- check documented experiment keys"
	go run ./scripts/checkexperimentkeys
.PHONY: lint/check-experiment-keys

lint/architecture:
	./scripts/check_architecture.sh
.PHONY: lint/architecture

lint/agents:
	./scripts/check_agents_structure.sh
.PHONY: lint/agents

lint/helm:
	cd helm/
	make lint
.PHONY: lint/helm

lint/markdown: node_modules/.installed
	pnpm lint-docs
.PHONY: lint/markdown

lint/actions: lint/actions/actionlint lint/actions/zizmor
.PHONY: lint/actions

lint/actions/actionlint:
	mise exec "go:github.com/rhysd/actionlint/cmd/actionlint" -- actionlint
.PHONY: lint/actions/actionlint

# zizmor uses GH_TOKEN to fetch imported workflows from GitHub; without it,
# external action references are skipped silently.
lint/actions/zizmor:
	@set -euo pipefail; \
	if [ -z "$${GH_TOKEN:-}" ] && command -v gh >/dev/null 2>&1; then \
		GH_TOKEN="$$(gh auth token 2>/dev/null || true)"; \
		export GH_TOKEN; \
	fi; \
	mise exec zizmor -- zizmor \
		--strict-collection \
		--persona=regular \
		.
.PHONY: lint/actions/zizmor

lint/mise-versions:
	./scripts/check_mise_versions.sh
.PHONY: lint/mise-versions

# Fails when the docs-gen filter in .github/workflows/ci.yaml misses a docs
# file make gen writes.
lint/docs-gen-filter:
	go run ./scripts/docsgenfiltercheck $(GEN_FILES)
.PHONY: lint/docs-gen-filter

# Verify api_key_scope enum contains all RBAC <resource>:<action> values.
lint/check-scopes: coderd/database/dump.sql | _gen/bin/check-scopes
	_gen/bin/check-scopes
.PHONY: lint/check-scopes

# Verify migrations do not hardcode the public schema.
lint/migrations:
	./scripts/check_pg_schema.sh "Migrations" $(MIGRATION_FILES)
	./scripts/check_pg_schema.sh "Fixtures" $(FIXTURE_FILES)
.PHONY: lint/migrations

lint/typos:
	typos --config .github/workflows/typos.toml
.PHONY: lint/typos

# Vale (prose linter).
#
# Invoked through `mise exec` like actionlint and zizmor above, so the
# version pinned in mise.toml ("aqua:errata-ai/vale") is the single source
# of truth and mise downloads the right OS/arch build. Always pass the full
# aqua key: the bare `vale` short name ignores the pin and resolves to the
# latest release.

# `vale sync` pulls the packages listed in .vale.ini's Packages directive
# into StylesPath (docs/.style/styles/). The .vale-synced sentinel makes
# sync idempotent across `make lint/prose` calls and lets warm checkouts
# skip the re-sync entirely. Make rebuilds this target when `.vale.ini`
# changes.
docs/.style/.vale-synced: .vale.ini
	@echo "$(GREEN)==>$(RESET) $(BOLD)vale sync$(RESET)"
	mise exec "aqua:errata-ai/vale" -- vale sync
	@touch $@

# Vale exits non-zero only on error-level alerts. `--no-exit` keeps the
# target green while the un-overridden Google error-level rules still
# produce a baseline error count; real failures (bad config, missing
# files) still propagate. Once the baseline error count reaches zero, drop
# `--no-exit` and surface error-level violations as real failures. See
# DOCS-40.
lint/prose: docs/.style/.vale-synced
	@echo "$(GREEN)==>$(RESET) $(BOLD)lint/prose$(RESET)"
	mise exec "aqua:errata-ai/vale" -- vale --no-exit docs/
	@echo "$(GREEN)==>$(RESET) Vale checks a subset of the style guide and never fails this target."
	@echo "    Coverage: docs/.style/style-guide/README.md#what-the-tooling-checks-and-what-it-doesnt"
.PHONY: lint/prose

# pre-commit and pre-push mirror CI checks locally.
#
# pre-commit runs checks that don't need external services (Docker,
# Playwright). This is the git pre-commit hook default since Docker
# and browser issues in the local environment would otherwise block
# all commits.
#
# pre-push adds heavier checks: Go tests, JS tests, and site build.
# Storybook tests are deliberately excluded: the vitest browser run
# has repeatedly stalled or failed for developers running pre-push,
# and PR #24703's serialization did not fix it. Storybook stays
# covered by the storybook job in CI (storybook:build plus Pixel
# snapshots) on every PR.
# The pre-push hook is allowlisted, see scripts/githooks/pre-push.
#
# pre-commit uses two phases: gen+fmt first, then lint+build. This
# avoids races where gen creates temporary .go files that lint's
# find-based checks pick up. Within each phase, targets run in
# parallel via -j. It fails if any tracked files have unstaged
# changes afterward.

define check-unstaged
	unstaged="$$(git diff --name-only)"
	if [[ -n $$unstaged ]]; then
		echo "$(RED)✗ check unstaged changes$(RESET)"
		echo "$$unstaged" | sed 's/^/  - /'
		echo ""
		echo "$(DIM)  Verify generated changes are correct before staging:$(RESET)"
		echo "$(DIM)    git diff$(RESET)"
		echo "$(DIM)    git add -u && git commit$(RESET)"
		exit 1
	fi
endef
define check-untracked
	untracked=$$(git ls-files --other --exclude-standard)
	if [[ -n $$untracked ]]; then
		echo "$(YELLOW)? check untracked files$(RESET)"
		echo "$$untracked" | sed 's/^/  - /'
		echo ""
		echo "$(DIM)  Review if these should be committed or added to .gitignore.$(RESET)"
	fi
endef

pre-commit:
	start=$$(date +%s)
	logdir=$$(mktemp -d "$${TMPDIR:-/tmp}/coder-pre-commit.XXXXXX")
	echo "$(BOLD)pre-commit$(RESET) ($$logdir)"
	echo "gen + fmt:"
	$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir gen fmt
	$(check-unstaged)
	echo "lint + build:"
	$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir \
		lint \
		lint/typos \
		build/coder-slim_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
	$(check-unstaged)
	$(check-untracked)
	rm -rf $$logdir
	echo "$(GREEN)✓ pre-commit passed$(RESET) ($$(( $$(date +%s) - $$start ))s)"
.PHONY: pre-commit

# Lightweight pre-commit for changes that don't touch Go or
# TypeScript. Skips gen, lint/go, lint/ts, fmt/go, fmt/ts, and
# the binary build. Used by the pre-commit hook when only docs,
# shell, terraform, helm, or other fast-to-check files changed.
pre-commit-light:
	start=$$(date +%s)
	logdir=$$(mktemp -d "$${TMPDIR:-/tmp}/coder-pre-commit-light.XXXXXX")
	echo "$(BOLD)pre-commit-light$(RESET) ($$logdir)"
	echo "fmt:"
	$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir fmt-light
	$(check-unstaged)
	echo "lint:"
	$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir lint-light
	$(check-unstaged)
	$(check-untracked)
	rm -rf $$logdir
	echo "$(GREEN)✓ pre-commit-light passed$(RESET) ($$(( $$(date +%s) - $$start ))s)"
.PHONY: pre-commit-light

pre-push:
	start=$$(date +%s)
	logdir=$$(mktemp -d "$${TMPDIR:-/tmp}/coder-pre-push.XXXXXX")
	echo "$(BOLD)pre-push$(RESET) ($$logdir)"
	echo "test + build site:"
	$(MAKE) --no-print-directory -j$(PARALLEL_JOBS) MAKE_TIMED=1 MAKE_LOGDIR=$$logdir \
		test \
		test-js \
		site/out/index.html
	rm -rf $$logdir
	echo "$(GREEN)✓ pre-push passed$(RESET) ($$(( $$(date +%s) - $$start ))s)"
.PHONY: pre-push

offlinedocs/check: offlinedocs/node_modules/.installed
	cd offlinedocs/
	pnpm format:check
	pnpm lint
	pnpm export
.PHONY: offlinedocs/check

# All files generated by the database should be added here, and this can be used
# as a target for jobs that need to run after the database is generated.
DB_GEN_FILES := \
	coderd/database/dump.sql \
	coderd/database/querier.go \
	coderd/database/unique_constraint.go \
	coderd/database/dbmetrics/querymetrics.go \
	coderd/database/dbauthz/dbauthz.go \
	coderd/database/dbmock/dbmock.go

TAILNETTEST_MOCKS := \
	tailnet/tailnettest/coordinatormock.go \
	tailnet/tailnettest/coordinateemock.go \
	tailnet/tailnettest/workspaceupdatesprovidermock.go \
	tailnet/tailnettest/subscriptionmock.go

AIBRIDGED_MOCKS := \
	coderd/aibridged/aibridgedmock/clientmock.go \
	coderd/aibridged/aibridgedmock/poolmock.go

GEN_FILES := \
	tailnet/proto/tailnet.pb.go \
	agent/proto/agent.pb.go \
	agent/agentsocket/proto/agentsocket.pb.go \
	agent/boundarylogproxy/codec/boundary.pb.go \
	provisionersdk/proto/provisioner.pb.go \
	provisionerd/proto/provisionerd.pb.go \
	vpn/vpn.pb.go \
	coderd/aibridged/proto/aibridged.pb.go \
	$(DB_GEN_FILES) \
	$(SITE_GEN_FILES) \
	coderd/rbac/object_gen.go \
	codersdk/rbacresources_gen.go \
	coderd/rbac/scopes_constants_gen.go \
	codersdk/apikey_scopes_gen.go \
	codersdk/dbenums_gen.go \
	docs/admin/integrations/prometheus.md \
	docs/reference/cli/index.md \
	docs/admin/security/audit-logs.md \
	docs/admin/setup/configuration-reference.md \
	docs/reference/api-key-scopes.md \
	coderd/apidoc/swagger.json \
	docs/manifest.json \
	docs/manifest/generated/cli.json \
	docs/manifest/generated/rest-api.json \
	provisioner/terraform/testdata/generation.sha1 \
	scripts/metricsdocgen/generated_metrics \
	site/e2e/provisionerGenerated.ts \
	examples/examples.gen.json \
	$(TAILNETTEST_MOCKS) \
	coderd/database/pubsub/psmock/psmock.go \
	agent/agentcontainers/acmock/acmock.go \
	agent/agentcontainers/dcspec/dcspec_gen.go \
	coderd/httpmw/loggermw/loggermock/loggermock.go \
	codersdk/workspacesdk/agentconnmock/agentconnmock.go \
	$(AIBRIDGED_MOCKS)

# all gen targets should be added here and to gen/mark-fresh. Generated files
# and inputs under any path in the docs filter in .github/workflows/ci.yaml
# must also match its docs-gen filter, or CI skips gen for docs-only PRs that
# edit them. lint/docs-gen-filter checks generated files but not inputs.
# Set GEN_SKIP_GOLDEN=1 to skip gen/golden-files (which needs Docker to
# start PostgreSQL via testcontainers).
GEN_SKIP_GOLDEN ?=
gen: gen/db $(if $(GEN_SKIP_GOLDEN),,gen/golden-files) $(GEN_FILES)
.PHONY: gen

gen/db: $(DB_GEN_FILES)
.PHONY: gen/db

# Patched snapshot of the models.dev catalog. Fetched once per
# gen/aibridge-prices run, with upstream corrections applied by
# overrides.jq; both prices.json and the frontend known-models catalog are
# generated from this single snapshot. Phony so each invocation refreshes it.
_gen/models-dev.json: | _gen
	set -o pipefail; $(call atomic_write,curl -fsSL https://models.dev/api.json | jq -f scripts/aibridgepricesgen/overrides.jq)
.PHONY: _gen/models-dev.json

# Refresh the AI Bridge pricing seed file from the patched models.dev
# snapshot. Kept out of `make gen` because the output depends on live
# upstream data. Phony so each invocation regenerates.
coderd/aibridge/prices/data/prices.json: _gen/bin/aibridgepricesgen _gen/models-dev.json | _gen
	@mkdir -p $(dir $@)
	$(call atomic_write,_gen/bin/aibridgepricesgen -upstream _gen/models-dev.json)
.PHONY: coderd/aibridge/prices/data/prices.json

# Frontend known-models catalog, generated from the same patched models.dev
# snapshot joined with the editorial curation in
# scripts/aibridgepricesgen/curation.json. Kept out of `make gen` for the
# same live-upstream-data reason as prices.json.
site/src/modules/aiModels/knownModels/knownModelsGenerated.json: site/node_modules/.installed _gen/bin/aibridgepricesgen _gen/models-dev.json | _gen
	$(call atomic_write,_gen/bin/aibridgepricesgen -format=catalog -upstream _gen/models-dev.json,./scripts/biome_format.sh)
.PHONY: site/src/modules/aiModels/knownModels/knownModelsGenerated.json

gen/aibridge-prices: \
	coderd/aibridge/prices/data/prices.json \
	site/src/modules/aiModels/knownModels/knownModelsGenerated.json
.PHONY: gen/aibridge-prices

gen/golden-files: \
	agent/unit/testdata/.gen-golden \
	cli/testdata/.gen-golden \
	coderd/.gen-golden \
	coderd/mcp/testdata/.gen-golden \
	coderd/notifications/.gen-golden \
	enterprise/cli/testdata/.gen-golden \
	enterprise/tailnet/testdata/.gen-golden \
	helm/coder/tests/testdata/.gen-golden \
	helm/provisioner/tests/testdata/.gen-golden \
	helm/ai-gateway/tests/testdata/.gen-golden \
	provisioner/terraform/testdata/.gen-golden \
	tailnet/testdata/.gen-golden
.PHONY: gen/golden-files

# Mark all generated files as fresh so make thinks they're up-to-date. This is
# used during releases so we don't run generation scripts.
gen/mark-fresh:
	files="\
		tailnet/proto/tailnet.pb.go \
		agent/proto/agent.pb.go \
		provisionersdk/proto/provisioner.pb.go \
		provisionerd/proto/provisionerd.pb.go \
		agent/agentsocket/proto/agentsocket.pb.go \
		agent/boundarylogproxy/codec/boundary.pb.go \
		vpn/vpn.pb.go \
		coderd/aibridged/proto/aibridged.pb.go \
		coderd/database/dump.sql \
		coderd/database/querier.go \
		coderd/database/unique_constraint.go \
		coderd/database/dbmetrics/querymetrics.go \
		coderd/database/dbauthz/dbauthz.go \
		coderd/database/dbmock/dbmock.go \
		coderd/database/pubsub/psmock/psmock.go \
		site/src/api/typesGenerated.ts \
		coderd/rbac/object_gen.go \
		codersdk/rbacresources_gen.go \
		coderd/rbac/scopes_constants_gen.go \
		codersdk/apikey_scopes_gen.go \
		codersdk/dbenums_gen.go \
		site/src/api/appFamiliesGenerated.ts \
		site/src/api/rbacresourcesGenerated.ts \
		site/src/api/countriesGenerated.ts \
		site/src/api/chatModelOptionsGenerated.json \
		docs/admin/integrations/prometheus.md \
		docs/reference/cli/index.md \
		docs/admin/security/audit-logs.md \
		docs/admin/setup/configuration-reference.md \
		docs/reference/api-key-scopes.md \
		coderd/apidoc/swagger.json \
		docs/manifest.json \
		docs/manifest/generated/cli.json \
		docs/manifest/generated/rest-api.json \
		site/e2e/provisionerGenerated.ts \
		site/src/theme/icons.json \
		examples/examples.gen.json \
		scripts/metricsdocgen/generated_metrics \
		$(TAILNETTEST_MOCKS) \
		agent/agentcontainers/acmock/acmock.go \
		agent/agentcontainers/dcspec/dcspec_gen.go \
		coderd/httpmw/loggermw/loggermock/loggermock.go \
		codersdk/workspacesdk/agentconnmock/agentconnmock.go \
		$(AIBRIDGED_MOCKS) \
		"

	for file in $$files; do
		echo "$$file"
		if [ ! -f "$$file" ]; then
			echo "File '$$file' does not exist"
			exit 1
		fi

		# touch sets the mtime of the file to the current time
		touch "$$file"
	done
.PHONY: gen/mark-fresh

# Runs migrations to output a dump of the database schema after migrations are
# applied.
coderd/database/dump.sql: coderd/database/gen/dump/main.go $(DBDUMP_INPUTS) | _gen/bin/dbdump
	_gen/bin/dbdump
	touch "$@"

# Generates Go code for querying the database.
# coderd/database/queries.sql.go
# coderd/database/models.go
#
# NOTE: grouped target (&:) ensures generate.sh runs only once even
# with -j and all outputs are considered produced together. These
# files are all written by generate.sh (via sqlc and scripts/dbgen).
coderd/database/querier.go \
coderd/database/unique_constraint.go \
coderd/database/dbmetrics/querymetrics.go \
coderd/database/dbauthz/dbauthz.go &: \
	coderd/database/sqlc.yaml \
	coderd/database/dump.sql \
	$(wildcard coderd/database/queries/*.sql)
	SKIP_DUMP_SQL=1 ./coderd/database/generate.sh
	touch coderd/database/querier.go coderd/database/unique_constraint.go coderd/database/dbmetrics/querymetrics.go coderd/database/dbauthz/dbauthz.go

coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier.go
	go generate ./coderd/database/dbmock/
	touch "$@"

coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go
	go generate ./coderd/database/pubsub/psmock
	touch "$@"

agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go
	go generate ./agent/agentcontainers/acmock/
	touch "$@"

coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.go
	go generate ./coderd/httpmw/loggermw/loggermock/
	touch "$@"

codersdk/workspacesdk/agentconnmock/agentconnmock.go: codersdk/workspacesdk/agentconn.go
	go generate ./codersdk/workspacesdk/agentconnmock/
	./scripts/format_go_file.sh "$@"
	touch "$@"

$(AIBRIDGED_MOCKS): coderd/aibridged/client.go coderd/aibridged/pool.go
	go generate ./coderd/aibridged/aibridgedmock/
	touch "$@"

agent/agentcontainers/dcspec/dcspec_gen.go: \
	node_modules/.installed \
	agent/agentcontainers/dcspec/devContainer.base.schema.json \
	agent/agentcontainers/dcspec/gen.sh \
	agent/agentcontainers/dcspec/doc.go
	DCSPEC_QUIET=true go generate ./agent/agentcontainers/dcspec/
	touch "$@"

$(TAILNETTEST_MOCKS): tailnet/coordinator.go tailnet/service.go
	go generate ./tailnet/tailnettest/
	touch "$@"

tailnet/proto/tailnet.pb.go: tailnet/proto/tailnet.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-drpc_out=. \
		--go-drpc_opt=paths=source_relative \
		./tailnet/proto/tailnet.proto

agent/proto/agent.pb.go: agent/proto/agent.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-drpc_out=. \
		--go-drpc_opt=paths=source_relative \
		./agent/proto/agent.proto

agent/agentsocket/proto/agentsocket.pb.go: agent/agentsocket/proto/agentsocket.proto agent/proto/agent.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-drpc_out=. \
		--go-drpc_opt=paths=source_relative \
		./agent/agentsocket/proto/agentsocket.proto

provisionersdk/proto/provisioner.pb.go: provisionersdk/proto/provisioner.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-drpc_out=. \
		--go-drpc_opt=paths=source_relative \
		./provisionersdk/proto/provisioner.proto

provisionerd/proto/provisionerd.pb.go: provisionerd/proto/provisionerd.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-drpc_out=. \
		--go-drpc_opt=paths=source_relative \
		./provisionerd/proto/provisionerd.proto

vpn/vpn.pb.go: vpn/vpn.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		./vpn/vpn.proto

agent/boundarylogproxy/codec/boundary.pb.go: agent/boundarylogproxy/codec/boundary.proto agent/proto/agent.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		./agent/boundarylogproxy/codec/boundary.proto

coderd/aibridged/proto/aibridged.pb.go: coderd/aibridged/proto/aibridged.proto
	./scripts/atomic_protoc.sh \
		--go_out=. \
		--go_opt=paths=source_relative \
		--go-drpc_out=. \
		--go-drpc_opt=paths=source_relative \
		./coderd/aibridged/proto/aibridged.proto

site/src/api/typesGenerated.ts: site/node_modules/.installed $(wildcard scripts/apitypings/*) \
		$(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go') \
		$(wildcard coderd/healthcheck/health/*.go) \
		$(wildcard codersdk/healthsdk/*.go) | _gen _gen/bin/apitypings
	$(call atomic_write,_gen/bin/apitypings,./scripts/biome_format.sh)

site/e2e/provisionerGenerated.ts: site/node_modules/.installed provisionerd/proto/provisionerd.pb.go provisionersdk/proto/provisioner.pb.go
	(cd site/ && pnpm run gen:provisioner)
	touch "$@"

site/src/theme/icons.json: site/node_modules/.installed $(wildcard scripts/gensite/*) $(wildcard site/static/icon/*) | _gen _gen/bin/gensite
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && \
		_gen/bin/gensite -icons "$$tmpfile" && \
		./scripts/biome_format.sh "$$tmpfile" && \
		mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

examples/examples.gen.json: scripts/examplegen/main.go examples/examples.go $(shell find ./examples/templates) | _gen _gen/bin/examplegen
	$(call atomic_write,_gen/bin/examplegen)

coderd/rbac/object_gen.go: scripts/typegen/rbacobject.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go | _gen _gen/bin/typegen
	$(call atomic_write,_gen/bin/typegen rbac object)
	touch "$@"

# NOTE: depends on object_gen.go because the generator build
# compiles coderd/rbac which includes it.
coderd/rbac/scopes_constants_gen.go: scripts/typegen/scopenames.gotmpl scripts/typegen/main.go coderd/rbac/policy/policy.go \
	coderd/rbac/object_gen.go | _gen _gen/bin/typegen
	# Write to a temp file first to avoid truncating the package
	# during build since the generator imports the rbac package.
	$(call atomic_write,_gen/bin/typegen rbac scopenames)
	touch "$@"

# NOTE: depends on object_gen.go and scopes_constants_gen.go because
# the generator build compiles coderd/rbac which includes both.
codersdk/rbacresources_gen.go: scripts/typegen/codersdk.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go \
	coderd/rbac/object_gen.go coderd/rbac/scopes_constants_gen.go | _gen _gen/bin/typegen
	# Write to a temp file to avoid truncating the target, which
	# would break the codersdk package and any parallel build targets.
	$(call atomic_write,_gen/bin/typegen rbac codersdk)
	touch "$@"

# Reads coderd/database/models.go, which generate.sh writes with querier.go.
codersdk/dbenums_gen.go: scripts/typegen/dbenums.gotmpl scripts/typegen/main.go coderd/database/querier.go | _gen _gen/bin/typegen
	$(call atomic_write,_gen/bin/typegen dbenums)
	touch "$@"

# NOTE: depends on object_gen.go and scopes_constants_gen.go because
# the generator build compiles coderd/rbac which includes both.
codersdk/apikey_scopes_gen.go: scripts/apikeyscopesgen/main.go coderd/rbac/scopes_catalog.go coderd/rbac/scopes.go \
	coderd/rbac/object_gen.go coderd/rbac/scopes_constants_gen.go | _gen _gen/bin/apikeyscopesgen
	# Generate SDK constants for external API key scopes.
	$(call atomic_write,_gen/bin/apikeyscopesgen)
	touch "$@"

site/src/api/appFamiliesGenerated.ts: site/node_modules/.installed scripts/typegen/appfamilies.tstmpl scripts/typegen/main.go codersdk/appname.go | _gen _gen/bin/typegen
	$(call atomic_write,_gen/bin/typegen appfamilies,./scripts/biome_format.sh)

# NOTE: depends on object_gen.go and scopes_constants_gen.go because
# the generator build compiles coderd/rbac which includes both.
site/src/api/rbacresourcesGenerated.ts: site/node_modules/.installed scripts/typegen/codersdk.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go \
	coderd/rbac/object_gen.go coderd/rbac/scopes_constants_gen.go | _gen _gen/bin/typegen
	$(call atomic_write,_gen/bin/typegen rbac typescript,./scripts/biome_format.sh)

site/src/api/countriesGenerated.ts: site/node_modules/.installed scripts/typegen/countries.tstmpl scripts/typegen/main.go codersdk/countries.go | _gen _gen/bin/typegen
	$(call atomic_write,_gen/bin/typegen countries,./scripts/biome_format.sh)

site/src/api/chatModelOptionsGenerated.json: site/node_modules/.installed scripts/modeloptionsgen/main.go codersdk/chats.go | _gen _gen/bin/modeloptionsgen
	$(call atomic_write,_gen/bin/modeloptionsgen | tail -n +2,./scripts/biome_format.sh)

scripts/metricsdocgen/generated_metrics: $(GO_SRC_FILES) | _gen _gen/bin/metricsdocgen-scanner
	$(call atomic_write,_gen/bin/metricsdocgen-scanner)

docs/admin/integrations/prometheus.md: node_modules/.installed scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics scripts/metricsdocgen/generated_metrics | _gen _gen/bin/metricsdocgen
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && cp "$@" "$$tmpfile" && \
		_gen/bin/metricsdocgen --prometheus-doc-file="$$tmpfile" && \
		pnpm exec markdownlint-cli2 --fix "$$tmpfile" && \
		pnpm exec markdown-table-formatter "$$tmpfile" && \
		mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

docs/reference/cli/index.md: node_modules/.installed examples/examples.gen.json _gen/bin/clidocgen | _gen
	tmpdir=$$(mktemp -d -p _gen) && \
		tmpdir=$$(realpath "$$tmpdir") && \
		mkdir -p "$$tmpdir/docs/reference/cli" && \
		cp -R docs/manifest "$$tmpdir/docs/manifest" && \
		CI=true DOCS_DIR="$$tmpdir/docs" _gen/bin/clidocgen && \
		pnpm exec markdownlint-cli2 --fix "$$tmpdir/docs/reference/cli/**/*.md" && \
		pnpm exec markdown-table-formatter "$$tmpdir/docs/reference/cli/**/*.md" && \
		rm -rf docs/reference/cli && \
		mv "$$tmpdir/docs/reference/cli" docs/reference/cli && \
		rm -rf "$$tmpdir"

docs/admin/security/audit-logs.md: node_modules/.installed coderd/database/querier.go scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go | _gen _gen/bin/auditdocgen
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && cp "$@" "$$tmpfile" && \
		_gen/bin/auditdocgen --audit-doc-file="$$tmpfile" && \
		pnpm exec markdownlint-cli2 --fix "$$tmpfile" && \
		pnpm exec markdown-table-formatter "$$tmpfile" && \
		mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

docs/admin/setup/configuration-reference.md: node_modules/.installed $(wildcard scripts/configdocgen/*.go) $(wildcard codersdk/*.go) _gen/bin/configdocgen | _gen
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && \
		_gen/bin/configdocgen --out="$$tmpfile" && \
		pnpm exec markdownlint-cli2 --fix "$$tmpfile" && \
		pnpm exec markdown-table-formatter "$$tmpfile" && \
		mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

docs/reference/api-key-scopes.md: node_modules/.installed $(wildcard scripts/scopesdocgen/*.go) $(RBAC_GO_FILES) $(DOCS_MANIFEST_SOURCES) _gen/bin/scopesdocgen | _gen
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && \
		_gen/bin/scopesdocgen --out="$$tmpfile" && \
		pnpm exec markdownlint-cli2 --fix "$$tmpfile" && \
		pnpm exec markdown-table-formatter "$$tmpfile" && \
		mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

coderd/apidoc/.gen: \
	node_modules/.installed \
	site/node_modules/.installed \
	scripts/apidocgen/node_modules/.installed \
	site/node_modules/.installed \
	$(wildcard coderd/*.go) \
	$(wildcard enterprise/coderd/*.go) \
	$(wildcard codersdk/*.go) \
	$(wildcard enterprise/wsproxy/wsproxysdk/*.go) \
	$(wildcard coderd/workspaceconnwatcher/*.go) \
	$(DB_GEN_FILES) \
	coderd/rbac/object_gen.go \
	.swaggo \
	scripts/apidocgen/generate.sh \
	scripts/apidocgen/swaginit/main.go \
	$(wildcard scripts/apidocgen/postprocess/*) \
	$(wildcard scripts/apidocgen/markdown-template/*) | _gen
	tmpdir=$$(mktemp -d -p _gen) && swagtmp=$$(mktemp -d -p _gen) && \
		tmpdir=$$(realpath "$$tmpdir") && swagtmp=$$(realpath "$$swagtmp") && \
		mkdir -p "$$tmpdir/reference/api" && \
		cp -R docs/manifest "$$tmpdir/manifest" && \
		SWAG_OUTPUT_DIR="$$swagtmp" APIDOCGEN_DOCS_DIR="$$tmpdir" ./scripts/apidocgen/generate.sh && \
		pnpm exec markdownlint-cli2 --fix "$$tmpdir/reference/api/*.md" && \
		pnpm exec markdown-table-formatter "$$tmpdir/reference/api/*.md" && \
		./scripts/biome_format.sh "$$swagtmp/swagger.json" && \
		./scripts/biome_format.sh "$$tmpdir/manifest/generated/rest-api.json" && \
		for f in "$$tmpdir/reference/api/"*.md; do mv "$$f" "docs/reference/api/$$(basename "$$f")"; done && \
		mv "$$tmpdir/manifest/generated/rest-api.json" docs/manifest/generated/rest-api.json && \
		mv "$$swagtmp/docs.go" coderd/apidoc/docs.go && \
		mv "$$swagtmp/swagger.json" coderd/apidoc/swagger.json && \
		rm -rf "$$tmpdir" "$$swagtmp"
	touch "$@"

# apidocgen writes the REST API sidebar fragment named by children_from in
# docs/manifest; the rule above moves it into place.
docs/manifest/generated/rest-api.json: coderd/apidoc/.gen
	touch "$@"

# clidocgen writes the "Command Line" sidebar fragment named by children_from
# in docs/manifest, built from the command tree.
docs/manifest/generated/cli.json: site/node_modules/.installed _gen/bin/clidocgen | _gen
	tmpdir=$$(mktemp -d -p _gen) && tmpdir=$$(realpath "$$tmpdir") && \
		cp -R docs/manifest "$$tmpdir/manifest" && \
		CI=true DOCS_DIR="$$tmpdir" _gen/bin/clidocgen -manifest-only && \
		./scripts/biome_format.sh "$$tmpdir/manifest/generated/cli.json" && \
		mv "$$tmpdir/manifest/generated/cli.json" "$@" && rm -rf "$$tmpdir"

# Compiles the sidebar sources and generated fragments into docs/manifest.json.
define build-docs-manifest
	tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/manifest.json && \
		_gen/bin/docsmanifestgen build -out "$$tmpfile" && \
		./scripts/biome_format.sh "$$tmpfile" && \
		mv "$$tmpfile" docs/manifest.json && rm -rf "$$tmpdir"
endef

docs/manifest.json: $(DOCS_MANIFEST_SOURCES) docs/manifest/generated/cli.json docs/manifest/generated/rest-api.json site/node_modules/.installed _gen/bin/docsmanifestgen docs/reference/cli/index.md | _gen
	$(build-docs-manifest)

coderd/apidoc/swagger.json: site/node_modules/.installed coderd/apidoc/.gen
	touch "$@"

update-golden-files:
	echo 'WARNING: This target is deprecated. Use "make gen/golden-files" instead.' >&2
	echo 'Running "make gen/golden-files"' >&2
	make gen/golden-files
.PHONY: update-golden-files

clean/golden-files:
	find . -type f -name '.gen-golden' -delete
	find \
		cli/testdata \
		coderd/notifications/testdata \
		coderd/testdata \
		enterprise/cli/testdata \
		enterprise/tailnet/testdata \
		helm/coder/tests/testdata \
		helm/provisioner/tests/testdata \
		helm/ai-gateway/tests/testdata \
		provisioner/terraform/testdata \
		tailnet/testdata \
		-type f -name '*.golden' -delete
.PHONY: clean/golden-files

agent/unit/testdata/.gen-golden: $(wildcard agent/unit/testdata/*.golden) $(GO_SRC_FILES) $(wildcard agent/unit/*_test.go)
	TZ=UTC go test ./agent/unit -run="TestGraph" -update
	touch "$@"

cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
	TZ=UTC go test ./cli -run="Test(CommandHelp|ServerYAML|ErrorExamples|.*Golden)" -update
	touch "$@"

enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard enterprise/cli/*_test.go)
	TZ=UTC go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
	touch "$@"

tailnet/testdata/.gen-golden: $(wildcard tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard tailnet/*_test.go)
	TZ=UTC go test ./tailnet -run="TestDebugTemplate" -update
	touch "$@"

enterprise/tailnet/testdata/.gen-golden: $(wildcard enterprise/tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard enterprise/tailnet/*_test.go)
	TZ=UTC go test ./enterprise/tailnet -run="TestDebugTemplate" -update
	touch "$@"

helm/coder/tests/testdata/.gen-golden: $(wildcard helm/coder/tests/testdata/*.yaml) $(wildcard helm/coder/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/coder/tests/*_test.go)
	if command -v helm >/dev/null 2>&1; then
		TZ=UTC go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
	else
		echo "WARNING: helm not found; skipping helm/coder golden generation" >&2
	fi
	touch "$@"

helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/testdata/*.yaml) $(wildcard helm/provisioner/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/provisioner/tests/*_test.go)
	if command -v helm >/dev/null 2>&1; then
		TZ=UTC go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update
	else
		echo "WARNING: helm not found; skipping helm/provisioner golden generation" >&2
	fi
	touch "$@"

helm/ai-gateway/tests/testdata/.gen-golden: $(wildcard helm/ai-gateway/tests/testdata/*.yaml) $(wildcard helm/ai-gateway/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/ai-gateway/tests/*_test.go)
	TZ=UTC go test ./helm/ai-gateway/tests -run=TestUpdateGoldenFiles -update
	touch "$@"

coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/*_test.go)
	TZ=UTC go test ./coderd -run="Test.*Golden$$" -update
	touch "$@"

coderd/mcp/testdata/.gen-golden: $(wildcard coderd/mcp/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/mcp/*_test.go)
	TZ=UTC go test ./coderd/mcp -run="Test.*Golden$$" -update
	touch "$@"

coderd/notifications/.gen-golden: $(wildcard coderd/notifications/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/notifications/*_test.go)
	TZ=UTC go test ./coderd/notifications -run="Test.*Golden$$" -update
	touch "$@"

# Wait for fixture generation before reading its outputs under make -j.
provisioner/terraform/testdata/.gen-golden: provisioner/terraform/testdata/generation.sha1 $(wildcard provisioner/terraform/testdata/resources/*/*.tfplan.* provisioner/terraform/testdata/resources/*/*.tfstate.*) $(wildcard provisioner/terraform/testdata/*/*.golden) $(wildcard provisioner/terraform/testdata/*/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go)
	TZ=UTC go test ./provisioner/terraform -run="Test.*Golden$$" -update
	touch "$@"

# Terraform reads ~/.terraformrc unless TF_CLI_CONFIG_FILE selects another file.
# After rebuilding a local provider or changing its override, regenerate with:
# ./provisioner/terraform/testdata/generate.sh
provisioner/terraform/testdata/generation.sha1: FORCE
	@./provisioner/terraform/testdata/generate.sh --if-needed

FORCE:
.PHONY: FORCE

# pre-commit runs gen and fmt concurrently; formatting must finish before hashing.
ifneq ($(filter fmt,$(MAKECMDGOALS)),)
provisioner/terraform/testdata/generation.sha1: | fmt/terraform fmt/shfmt
endif

update-terraform-testdata:
	./provisioner/terraform/testdata/generate.sh --upgrade
.PHONY: update-terraform-testdata

# Set the retry flags if TEST_RETRIES is set
ifdef TEST_RETRIES
GOTESTSUM_RETRY_FLAGS := --rerun-fails=$(TEST_RETRIES)
else
GOTESTSUM_RETRY_FLAGS :=
endif

# Default to 8x8 parallelism to avoid overwhelming our workspaces.
# Race detection defaults to 4x4 because the detector adds significant
# CPU overhead. Override via TEST_NUM_PARALLEL_PACKAGES /
# TEST_NUM_PARALLEL_TESTS.
TEST_PARALLEL_PACKAGES := $(or $(TEST_NUM_PARALLEL_PACKAGES),8)
TEST_PARALLEL_TESTS := $(or $(TEST_NUM_PARALLEL_TESTS),8)
RACE_PARALLEL_PACKAGES := $(or $(TEST_NUM_PARALLEL_PACKAGES),4)
RACE_PARALLEL_TESTS := $(or $(TEST_NUM_PARALLEL_TESTS),4)

# Use testsmallbatch tag to reduce wireguard memory allocation in tests
# (from ~18GB to negligible). Recursively expanded so target-specific
# overrides of TEST_PARALLEL_* take effect (e.g. test-race lowers
# parallelism). CI job timeout is 30m (see test-go-pg in ci.yaml).
# Keep the Go timeout shorter so tests produce goroutine dumps instead
# of the CI runner stopping the process with no output. Override via
# TEST_TIMEOUT for lanes where setup takes a large share of the job.
GOTEST_TIMEOUT := $(or $(TEST_TIMEOUT),25m)
GOTEST_FLAGS = -tags=testsmallbatch -v -timeout $(GOTEST_TIMEOUT) -p $(TEST_PARALLEL_PACKAGES) -parallel=$(TEST_PARALLEL_TESTS)

# The most common use is to set TEST_COUNT=1 to avoid Go's test cache.
ifdef TEST_COUNT
GOTEST_FLAGS += -count=$(TEST_COUNT)
endif

ifdef TEST_SHORT
GOTEST_FLAGS += -short
endif

# RUN is single-quoted for the shell so regex metacharacters survive make.
# Embedded single quotes are not supported; whichtests only emits RUN values
# built from ASCII test names so generated regexes stay within this contract.
ifdef RUN
GOTEST_FLAGS += -run '$(RUN)'
endif

# TEST_SHUFFLE values must be off, on, or an integer seed.
ifdef TEST_SHUFFLE
GOTEST_FLAGS += -shuffle=$(TEST_SHUFFLE)
endif

ifdef TEST_CPUPROFILE
GOTEST_FLAGS += -cpuprofile=$(TEST_CPUPROFILE)
endif

ifdef TEST_MEMPROFILE
GOTEST_FLAGS += -memprofile=$(TEST_MEMPROFILE)
endif

TEST_PACKAGES ?= ./...

test:
	$(GIT_FLAGS) gotestsum --format standard-quiet \
		$(GOTESTSUM_RETRY_FLAGS) \
		--packages="$(TEST_PACKAGES)" \
		-- \
		$(GOTEST_FLAGS)
.PHONY: test

test-race: TEST_PARALLEL_PACKAGES := $(RACE_PARALLEL_PACKAGES)
test-race: TEST_PARALLEL_TESTS := $(RACE_PARALLEL_TESTS)
test-race:
	$(GIT_FLAGS) gotestsum --format standard-quiet \
		--junitfile="gotests.xml" \
		$(GOTESTSUM_RETRY_FLAGS) \
		--packages="$(TEST_PACKAGES)" \
		-- \
		-race \
		$(GOTEST_FLAGS)
.PHONY: test-race

test-cli:
	$(MAKE) test TEST_PACKAGES="./cli..."
.PHONY: test-cli

test-js: site/node_modules/.installed
	cd site/
	pnpm test:ci
.PHONY: test-js

test-storybook: site/node_modules/.installed
	cd site/
	pnpm playwright:install
	pnpm exec vitest run --project=storybook
.PHONY: test-storybook

# sqlc-cloud-is-setup will fail if no SQLc auth token is set. Use this as a
# dependency for any sqlc-cloud related targets.
sqlc-cloud-is-setup:
	if [[ "$(SQLC_AUTH_TOKEN)" == "" ]]; then
		echo "ERROR: 'SQLC_AUTH_TOKEN' must be set to auth with sqlc cloud before running verify." 1>&2
		exit 1
	fi
.PHONY: sqlc-cloud-is-setup

sqlc-push: sqlc-cloud-is-setup test-postgres-docker
	echo "--- sqlc push"
	SQLC_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/$$(go run scripts/migrate-ci/main.go)" \
	sqlc push -f coderd/database/sqlc.yaml && echo "Passed sqlc push"
.PHONY: sqlc-push

sqlc-verify: sqlc-cloud-is-setup test-postgres-docker
	echo "--- sqlc verify"
	SQLC_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/$$(go run scripts/migrate-ci/main.go)" \
	sqlc verify -f coderd/database/sqlc.yaml && echo "Passed sqlc verify"
.PHONY: sqlc-verify

sqlc-vet: test-postgres-docker
	echo "--- sqlc vet"
	SQLC_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/$$(go run scripts/migrate-ci/main.go)" \
	sqlc vet -f coderd/database/sqlc.yaml && echo "Passed sqlc vet"
.PHONY: sqlc-vet


test-migrations: test-postgres-docker
	echo "--- test migrations"
	set -euo pipefail
	COMMIT_FROM=$(shell git log -1 --format='%h' HEAD)
	echo "COMMIT_FROM=$${COMMIT_FROM}"
	COMMIT_TO=$(shell git log -1 --format='%h' origin/main)
	echo "COMMIT_TO=$${COMMIT_TO}"
	if [[ "$${COMMIT_FROM}" == "$${COMMIT_TO}" ]]; then echo "Nothing to do!"; exit 0; fi
	echo "DROP DATABASE IF EXISTS migrate_test_$${COMMIT_FROM}; CREATE DATABASE migrate_test_$${COMMIT_FROM};" | psql 'postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable'
	go run ./scripts/migrate-test/main.go --from="$$COMMIT_FROM" --to="$$COMMIT_TO" --postgres-url="postgresql://postgres:postgres@localhost:5432/migrate_test_$${COMMIT_FROM}?sslmode=disable"
.PHONY: test-migrations

# NOTE: we set --memory to the same size as a GitHub runner.
test-postgres-docker:
	# If our container is already running, nothing to do.
	if docker ps --filter "name=test-postgres-docker-${POSTGRES_VERSION}" --format '{{.Names}}' | grep -q .; then \
		echo "test-postgres-docker-${POSTGRES_VERSION} is already running."; \
		exit 0; \
	fi
	# If something else is on 5432, warn but don't fail.
	if pg_isready -h 127.0.0.1 -q 2>/dev/null; then \
		echo "WARNING: PostgreSQL is already running on 127.0.0.1:5432 (not our container)."; \
		echo "Tests will use this instance. To use the Makefile's container, stop it first."; \
		exit 0; \
	fi
	docker rm -f test-postgres-docker-${POSTGRES_VERSION} || true

	# Try pulling up to three times to avoid CI flakes.
	docker pull ${POSTGRES_IMAGE} || {
		retries=2
		for try in $$(seq 1 $${retries}); do
			echo "Failed to pull image, retrying ($${try}/$${retries})..."
			sleep 1
			if docker pull ${POSTGRES_IMAGE}; then
				break
			fi
		done
	}

	# Make sure to not overallocate work_mem and max_connections as each
	# connection will be allowed to use this much memory. Try adjusting
	# shared_buffers instead, if needed.
	#
	# - work_mem=8MB * max_connections=1000 = 8GB
	# - shared_buffers=2GB + effective_cache_size=1GB = 3GB
	#
	# This leaves 5GB for the rest of the system _and_ storing the
	# database in memory (--tmpfs).
	#
	# https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM
	docker run \
		--env POSTGRES_PASSWORD=postgres \
		--env POSTGRES_USER=postgres \
		--env POSTGRES_DB=postgres \
		--env PGDATA=/tmp \
		--tmpfs /tmp \
		--publish 5432:5432 \
		--name test-postgres-docker-${POSTGRES_VERSION} \
		--restart no \
		--detach \
		--memory 16GB \
		${POSTGRES_IMAGE} \
		-c shared_buffers=2GB \
		-c effective_cache_size=1GB \
		-c work_mem=8MB \
		-c max_connections=1000 \
		-c fsync=off \
		-c synchronous_commit=off \
		-c full_page_writes=off \
		-c log_statement=$(TEST_POSTGRES_LOG_STATEMENT)
	while ! pg_isready -h 127.0.0.1
	do
		echo "$$(date) - waiting for database to start"
		sleep 0.5
	done
.PHONY: test-postgres-docker

# test-postgres-docker-logs prints the PostgreSQL container's logs. The
# postgres image logs to stderr (no logging_collector), which Docker captures,
# so combined with log_statement=all in test-postgres-docker these logs include
# every executed statement. Redirect to a file to save them, e.g.
# `make test-postgres-docker-logs > postgres.log`.
test-postgres-docker-logs:
	docker logs test-postgres-docker-${POSTGRES_VERSION}
.PHONY: test-postgres-docker-logs

test-tailnet-integration:
	env \
		CODER_TAILNET_TESTS=true \
		CODER_MAGICSOCK_DEBUG_LOGGING=true \
		TS_DEBUG_NETCHECK=true \
		GOTRACEBACK=single \
		go test \
			-tags=testsmallbatch \
			-exec "sudo -E" \
			-timeout=5m \
			-count=1 \
			./tailnet/test/integration
.PHONY: test-tailnet-integration

test-timings:
	@tmp_json="$$(mktemp)"; \
	trap 'rm -f "$$tmp_json"' EXIT; \
	set +e; \
	GOTESTSUM_JSONFILE="$$tmp_json" $(GIT_FLAGS) gotestsum --format standard-quiet \
		$(GOTESTSUM_RETRY_FLAGS) \
		--packages="$(TEST_PACKAGES)" \
		-- \
		$(GOTEST_FLAGS); \
	test_status=$$?; \
	jq -r -s '
		[
			["package", "test", "status", "elapsed_ms"],
			(
				map(select(
					(.Test // "") != "" and
					(.Action == "pass" or .Action == "fail" or .Action == "skip")
				))
				| sort_by([.Package, .Test])
				| group_by([.Package, .Test])
				| map(last)
				| sort_by([(-(.Elapsed // 0)), .Package, .Test])
				| .[]
				| [.Package, .Test, .Action, ((.Elapsed // 0) * 1000)]
			)
		]
		| .[]
		| @tsv
	' "$$tmp_json" > "$(or $(TEST_TIMINGS_OUTPUT),test-timings.tsv)"; \
	report_status=$$?; \
	if [[ $$test_status -ne 0 ]]; then exit $$test_status; fi; \
	exit $$report_status
.PHONY: test-timings

# Note: we used to add this to the test target, but it's not necessary and we can
# achieve the desired result by specifying -count=1 in the go test invocation
# instead. Keeping it here for convenience.
test-clean:
	go clean -testcache
.PHONY: test-clean

site/e2e/bin/coder: go.mod go.sum $(GO_SRC_FILES) site/out/index.html
	go build -o $@ \
		-tags embed,ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube \
		./enterprise/cmd/coder

test-e2e: site/e2e/bin/coder site/node_modules/.installed site/out/index.html
	cd site/
	pnpm playwright:install
ifdef CI
	DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1
else
	pnpm playwright:test
endif
.PHONY: test-e2e

# Count the number of test databases created per test package.
count-test-databases:
	PGPASSWORD=postgres psql -h localhost -U postgres -d coder_testing -P pager=off -c 'SELECT test_package, count(*) as count from test_databases GROUP BY test_package ORDER BY count DESC'
.PHONY: count-test-databases

.PHONY: count-test-databases