Files
coder/Makefile
T
Nick Vigilante ccffab23a0 feat: fail CI when the style guide claims enforcement it doesn't have (#29299)
## Summary

PR 1 corrected 9 annotations that claimed enforcement the config doesn't
provide. This adds the check that keeps them correct, because the same
drift will recur: annotations are prose, `.vale.ini` is config, and
nothing tied them together.

`scripts/styleclaims` reconciles three sources and fails `make lint`. It
also runs in `lint-light`, so the fast pre-commit hook covers docs-only
changes: the checker's inputs all live under `docs/.style/`, and leaving
it out would let this drift land through the hook that docs changes
actually use. It is the only `lint-light` entry that compiles Go, which
costs one small build on first run and nothing after.

What it reports:

1. `.vale.ini` plus `docs/.style/styles/Coder/`, which is the truth.
2. The per-rule annotations in the style guide subpages.
3. The two coverage tables on the style guide landing page.

### What it reports

| Finding | Example it would have caught |
|---|---|
| A `Coder.*` rule cited as active with no rule file |
`Coder.FirstPersonSingular`, `Coder.FirstPersonPlural` |
| A third-party rule cited as active while `BasedOnStyles = Coder` | the
8 `Google.*` citations |
| A rule file that no section cites as active | a rule landing without
the section the per-rule PR pattern requires |
| Checks-table severity, scope, or membership drift | a rule re-scoped
in `.vale.ini` without updating the table |
| Coverage-count drift | the per-section and total rows |

### Also in this PR

- One annotation reworded so each planned citation is marked
individually (`Google.Gender` and `Google.GenderBias`). A trailing
`(both planned)` covering several citations isn't machine-readable, and
the checker depends on that precision.
- The annotation format documented in `scripts/styleclaims/README.md`,
with the per-rule PR pattern in `docs/.style/README.md` pointing at it.
- `make lint/prose` prints a coverage pointer after running, so the
advisory result doesn't read as a pass.

## Verification

- `go test ./scripts/styleclaims/` covers the parser and each finding
class, including the mixed `scripts/check_emdash.sh` plus `Coder.EmDash`
(planned) annotation and the "rule mentioned but not claimed" case that
`Google.Passive` represents.
- `go run ./scripts/styleclaims` is clean on the branch.
- Regression check: reverting `Coder.FirstPersonSingular` to its
pre-PR-1 wording reproduces 3 findings (the false claim plus the 2 count
rows). Restoring it returns to clean.
- `golangci-lint run scripts/styleclaims/...` reports 0 issues; `gofmt`
clean.
- `make lint/markdown`, `make lint/emdash` clean.

<details>
<summary>Design notes</summary>

### Why Go, not bash

`scripts/` is mostly bash, but this parses Markdown tables and
multi-line annotations, which bash does badly. `scripts/docshtmlcheck`
sets the precedent for a Go checker invoked from a lint target, so this
follows its layout: `main.go`, `main_test.go`, `README.md`, and a `go
run ./scripts/...` target.

### Why not generate the tables

Considered and rejected. Generation and checking need the same parse, so
once the checker exists the table's correctness is guaranteed either
way. Generation would only save a one-row edit in a PR that already
edits the guide by doctrine, at the cost of a generated block inside
prose contributors are invited to edit. The checker also catches a case
generation can't: a rule enabled in `.vale.ini` with no style guide
section at all.

### Parsing tradeoff

The checker reads prose annotations, so it depends on their format
staying regular: start with a recognized keyword, and mark each planned
citation individually. That regularity is enforced by the checker
itself, which is the point, but it does mean an author who invents a new
annotation phrasing gets a failure rather than silent acceptance. If
that proves annoying in practice, the fallback is a small YAML manifest
of rule status that both the tables and the checker read. I'd rather not
add that until the prose form actually hurts.

### Scope validation

Scope cells are checked against `.vale.ini` section globs: every glob
that sets a rule to `YES` must appear in the cell, and a rule with no
`NO` toggle must claim `docs/**`. That's how the `OneSentencePerLine`
row stays honest about running on contributor docs only.

</details>

---

Linear:
[DOCS-887](https://linear.app/codercom/issue/DOCS-887/make-clear-that-vale-passing-is-not-style-guide-conformance)

🤖 Generated with Coder Agents on behalf of @nickvigilante.
2026-09-22 19:18:59 +00:00

1784 lines
66 KiB
Makefile

# 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
ifdef MAKE_TIMED
SHELL := $(CURDIR)/scripts/lib/timed-shell.sh
.SHELLFLAGS = $@ -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/rbacresourcesGenerated.ts \
site/src/api/countriesGenerated.ts \
site/src/theme/icons.json \
examples/examples.gen.json \
docs/manifest.json \
docs/admin/integrations/prometheus.md \
docs/admin/security/audit-logs.md \
docs/admin/setup/configuration-reference.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
# 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)
# 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
# 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
_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
tar cf "../../../build/$(@F)" coder-*
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
./scripts/pnpm_install.sh
touch "$@"
offlinedocs/node_modules/.installed: offlinedocs/package.json offlinedocs/pnpm-lock.yaml
(cd offlinedocs/ && ../scripts/pnpm_install.sh)
touch "$@"
site/node_modules/.installed: site/package.json site/pnpm-lock.yaml
(cd site/ && ../scripts/pnpm_install.sh)
touch "$@"
scripts/apidocgen/node_modules/.installed: scripts/apidocgen/package.json scripts/apidocgen/pnpm-lock.yaml
(cd scripts/apidocgen && ../../scripts/pnpm_install.sh)
touch "$@"
SITE_GEN_FILES := \
site/src/api/typesGenerated.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
.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
# 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/ts lint/examples lint/helm lint/site-icons lint/markdown lint/docs-html lint/style-claims lint/check-scopes lint/migrations lint/bootstrap lint/architecture lint/emdash lint/agents lint/mise-versions $(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/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
lint/go:
GOMEMLIMIT="$${GOMEMLIMIT:-$(GO_LINT_MEMLIMIT)}" golangci-lint run --concurrency="$(GO_LINT_CONCURRENCY)"
paralleltestctx -custom-funcs="testutil.Context,chatdTestContext" ./...
go run ./scripts/intxcheck ./...
.PHONY: lint/go
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.
lint/docs-html:
echo "--- check for invalid inline HTML in docs"
go run ./scripts/docshtmlcheck
.PHONY: lint/docs-html
# 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
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
# 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 \
docs/admin/integrations/prometheus.md \
docs/reference/cli/index.md \
docs/admin/security/audit-logs.md \
docs/admin/setup/configuration-reference.md \
coderd/apidoc/swagger.json \
docs/manifest.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
# 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: _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/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 \
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 \
coderd/apidoc/swagger.json \
docs/manifest.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 "$@"
# 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 "$@"
# 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: 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 docs/manifest.json "$$tmpdir/docs/manifest.json" && \
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" && \
for f in "$$tmpdir/docs/reference/cli/"*.md; do mv "$$f" "docs/reference/cli/$$(basename "$$f")"; done && \
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"
coderd/apidoc/.gen: \
node_modules/.installed \
scripts/apidocgen/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 docs/manifest.json "$$tmpdir/manifest.json" && \
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" && \
for f in "$$tmpdir/reference/api/"*.md; do mv "$$f" "docs/reference/api/$$(basename "$$f")"; done && \
mv "$$tmpdir/manifest.json" _gen/manifest-staging.json && \
mv "$$swagtmp/docs.go" coderd/apidoc/docs.go && \
mv "$$swagtmp/swagger.json" coderd/apidoc/swagger.json && \
rm -rf "$$tmpdir" "$$swagtmp"
touch "$@"
docs/manifest.json: site/node_modules/.installed coderd/apidoc/.gen docs/reference/cli/index.md | _gen
tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && \
cp _gen/manifest-staging.json "$$tmpfile" && \
./scripts/biome_format.sh "$$tmpfile" && \
mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"
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/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 5m shorter so tests produce goroutine dumps
# instead of the CI runner killing the process with no output.
GOTEST_FLAGS = -tags=testsmallbatch -v -timeout 25m -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