Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
358 lines (317 loc) · 12.2 KB
/
Copy pathMakefile
File metadata and controls
358 lines (317 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# Check for .env file and include it
ifneq (,$(wildcard .env))
include .env
export
endif
# Every package in the workspace. The migration adapters under migrate/ are
# separate modules, so ./... alone covers only the root module.
PKGS := ./... ./migrate/goose/... ./migrate/tern/... ./migrate/golangmigrate/...
# % is a wildcard character that matches anything, so if make doesn't find a rule
# for a target, it uses this rule. The @ beneath is a no-op command. Using these
# together allows us to define a rule that matches anything, but doesn't do anything.
# This is particularly useful when you have targets that take arbitrary user input.
%:
@:
# ============================================================================= #
# HELPERS
# ============================================================================= #
## help: Print this help message
.PHONY: help
help:
@echo 'Usage:'
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
.PHONY: confirm
confirm:
@echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ]
# ============================================================================= #
# DEVELOPMENT
# ============================================================================= #
# ============================================================================= #
# DOCUMENTATION
# ============================================================================= #
## docs/serve: Serve Go documentation locally with pkgsite
.PHONY: docs/serve
docs/serve:
@echo 'Serving documentation at http://localhost:3030...'
@if ! command -v pkgsite >/dev/null 2>&1; then \
echo "Installing pkgsite..."; \
go install golang.org/x/pkgsite/cmd/pkgsite@latest; \
fi
pkgsite -http localhost:3030
# ============================================================================= #
# DATABASES
# ============================================================================= #
POSTGRES_COMPOSE_FILE := docker-compose.test.yml
POSTGRES_SERVICE := test-postgres
POSTGRES_HOST := localhost
POSTGRES_PORT := 5432
POSTGRES_USER := postgres
POSTGRES_PASSWORD := postgres
POSTGRES_DB := testdb
MYSQL_SERVICE := test-mysql
MYSQL_USER := root
MYSQL_PASSWORD := root
MARIADB_SERVICE := test-mariadb
.PHONY: check_pgcli
check_pgcli:
@if ! command -v pgcli >/dev/null 2>&1; then \
echo "Error: 'pgcli' is not installed. Please install pgcli first."; \
echo "Consider 'brew install pgcli' and visit https://www.pgcli.com for more info."; \
exit 1; \
fi
define wait_for_postgres
@echo "Waiting for Postgres to be ready..."
@attempts=0; \
while ! docker compose -f $(1) exec -T $(2) pg_isready -U $(3) -d $(4) >/dev/null 2>&1; do \
attempts=$$((attempts+1)); \
if [ $$attempts -gt 30 ]; then \
echo "❌ Postgres failed to become ready after 30 seconds"; \
echo "Try 'make $(5)' for a fresh start"; \
exit 1; \
fi; \
echo "⏳ Waiting... ($$attempts/30)"; \
sleep 1; \
done; \
echo "✅ Postgres is ready!"
endef
# mysqladmin ping answers before the server accepts SQL, so wait for a query.
define wait_for_mysql
@echo "Waiting for MySQL to be ready..."
@attempts=0; \
while ! docker compose -f $(1) exec -T $(2) mysql -u$(3) -p$(4) -e 'SELECT 1' >/dev/null 2>&1; do \
attempts=$$((attempts+1)); \
if [ $$attempts -gt 60 ]; then \
echo "❌ MySQL failed to become ready after 60 seconds"; \
echo "Try 'make $(5)' for a fresh start"; \
exit 1; \
fi; \
echo "⏳ Waiting... ($$attempts/60)"; \
sleep 1; \
done; \
echo "✅ MySQL is ready!"
endef
# The MariaDB image ships the mariadb client, not mysql.
define wait_for_mariadb
@echo "Waiting for MariaDB to be ready..."
@attempts=0; \
while ! docker compose -f $(1) exec -T $(2) mariadb -u$(3) -p$(4) -e 'SELECT 1' >/dev/null 2>&1; do \
attempts=$$((attempts+1)); \
if [ $$attempts -gt 60 ]; then \
echo "❌ MariaDB failed to become ready after 60 seconds"; \
echo "Try 'make $(5)' for a fresh start"; \
exit 1; \
fi; \
echo "⏳ Waiting... ($$attempts/60)"; \
sleep 1; \
done; \
echo "✅ MariaDB is ready (port 3307; set TEST_MYSQL_URL to use it)"
endef
## test/db/start: Start PostgreSQL test database
.PHONY: test/db/start
test/db/start:
@echo 'Starting test database...'
docker compose -f $(POSTGRES_COMPOSE_FILE) up -d
$(call wait_for_postgres,$(POSTGRES_COMPOSE_FILE),$(POSTGRES_SERVICE),$(POSTGRES_USER),$(POSTGRES_DB),test/db/restart)
$(call wait_for_mysql,$(POSTGRES_COMPOSE_FILE),$(MYSQL_SERVICE),$(MYSQL_USER),$(MYSQL_PASSWORD),test/db/restart)
$(call wait_for_mariadb,$(POSTGRES_COMPOSE_FILE),$(MARIADB_SERVICE),$(MYSQL_USER),$(MYSQL_PASSWORD),test/db/restart)
## test/db/stop: Stop PostgreSQL test database
.PHONY: test/db/stop
test/db/stop:
@echo 'Stopping test database...'
docker compose -f $(POSTGRES_COMPOSE_FILE) down
## test/db/logs: Show PostgreSQL logs
.PHONY: test/db/logs
test/db/logs:
docker compose -f $(POSTGRES_COMPOSE_FILE) logs -f $(POSTGRES_SERVICE)
## test/db/clean: Clean up Postgres containers and volumes (destructive)
.PHONY: test/db/clean
test/db/clean:
@echo "⚠️ This will remove all Postgres data. Continue? [y/N] " && read ans && [ $${ans:-N} = y ]
@echo "Cleaning up Postgres containers and volumes..."
docker compose -f $(POSTGRES_COMPOSE_FILE) down -v
@volumes=$$(docker volume ls -q -f name=testdb); \
if [ -n "$$volumes" ]; then \
docker volume rm $$volumes; \
fi
## test/db/restart: Restart Postgres with fresh container and volume (destructive)
.PHONY: test/db/restart
test/db/restart: test/db/clean
@echo "Starting fresh Postgres instance..."
docker compose -f $(POSTGRES_COMPOSE_FILE) build --force-rm --no-cache $(POSTGRES_SERVICE)
docker compose -f $(POSTGRES_COMPOSE_FILE) up -d
$(call wait_for_postgres,$(POSTGRES_COMPOSE_FILE),$(POSTGRES_SERVICE),$(POSTGRES_USER),$(POSTGRES_DB),test/db/restart)
$(call wait_for_mysql,$(POSTGRES_COMPOSE_FILE),$(MYSQL_SERVICE),$(MYSQL_USER),$(MYSQL_PASSWORD),test/db/restart)
$(call wait_for_mariadb,$(POSTGRES_COMPOSE_FILE),$(MARIADB_SERVICE),$(MYSQL_USER),$(MYSQL_PASSWORD),test/db/restart)
## test/db/psql: Connect to Postgres database
.PHONY: test/db/psql
test/db/psql:
@if ! docker compose -f $(POSTGRES_COMPOSE_FILE) exec $(POSTGRES_SERVICE) pg_isready -U $(POSTGRES_USER) > /dev/null 2>&1; then \
echo "❌ Error: Postgres is not running. Please start it first with 'make test/db/start'"; \
exit 1; \
fi
@echo "🔌 Connecting to Postgres..."
docker compose -f $(POSTGRES_COMPOSE_FILE) exec $(POSTGRES_SERVICE) psql --username=$(POSTGRES_USER) --dbname=$(POSTGRES_DB)
## test/db/pgcli: Connect to Postgres database via pgcli
.PHONY: test/db/pgcli
test/db/pgcli: check_pgcli
@if ! docker compose -f $(POSTGRES_COMPOSE_FILE) exec $(POSTGRES_SERVICE) pg_isready -U $(POSTGRES_USER) > /dev/null 2>&1; then \
echo "❌ Error: Postgres is not running. Please start it first with 'make test/db/start'"; \
exit 1; \
fi
@echo "🔌 Connecting to Postgres..."
pgcli -h $(POSTGRES_HOST) -p $(POSTGRES_PORT) -U $(POSTGRES_USER) -d $(POSTGRES_DB)
## test/db/execute: Execute a SQL script against the database
.PHONY: test/db/execute
test/db/execute:
@if [ -z "$(file)" ]; then \
echo "❌ Error: SQL file not provided. Usage: make test/db/execute file=path/to/your/script.sql"; \
exit 1; \
fi
@if [ ! -f "$(file)" ]; then \
echo "❌ Error: SQL file '$(file)' not found"; \
exit 1; \
fi
@if ! docker compose -f $(POSTGRES_COMPOSE_FILE) exec $(POSTGRES_SERVICE) pg_isready -U $(POSTGRES_USER) > /dev/null 2>&1; then \
echo "❌ Error: Postgres is not running. Please start it first with 'make test/db/start'"; \
exit 1; \
fi
@echo "Executing SQL script $(file)..."
cat $(file) | docker compose -f $(POSTGRES_COMPOSE_FILE) exec -T $(POSTGRES_SERVICE) psql --username=$(POSTGRES_USER) --dbname=$(POSTGRES_DB)
@echo "✅ SQL script executed successfully"
## test: Run all tests (requires database)
.PHONY: test
test: test/db/start
@echo 'Running tests...'
go test -v $(PKGS)
## test/postgres: Run PostgreSQL tests only
.PHONY: test/postgres
test/postgres: test/db/start
@echo 'Running PostgreSQL tests...'
cd postgres && go test -v
## test/parallel: Run tests in parallel
.PHONY: test/parallel
test/parallel: test/db/start
@echo 'Running tests in parallel...'
go test -v -parallel 10 $(PKGS)
## test/race: Run tests with race detector
.PHONY: test/race
test/race: test/db/start
@echo 'Running tests with race detector...'
go test -race -v $(PKGS)
# ============================================================================= #
# QUALITY CONTROL
# ============================================================================= #
## check-staticcheck: Check if staticcheck is installed
.PHONY: check-staticcheck
check-staticcheck:
@if command -v staticcheck > /dev/null; then \
echo "✅ staticcheck is installed"; \
else \
echo "⚠️ staticcheck not found, static analysis will be skipped"; \
echo "🔍 Install with: go install honnef.co/go/tools/cmd/staticcheck@latest"; \
exit 1; \
fi
## check-golangci-lint: Check if golangci-lint is installed
.PHONY: check-golangci-lint
check-golangci-lint:
@if command -v golangci-lint > /dev/null; then \
echo "✅ golangci-lint is installed"; \
else \
echo "⚠️ golangci-lint not found, external linting will be skipped"; \
echo "🔍 Install with: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $$(go env GOPATH)/bin v2.15.0"; \
exit 1; \
fi
## run-staticcheck: Run staticcheck if it exists
.PHONY: run-staticcheck
run-staticcheck: check-staticcheck
@echo 'Running staticcheck...'
@staticcheck $(PKGS) || echo "Note: staticcheck found issues (exit code: $$?)"
## run-golangci-lint: Run golangci-lint if it exists
.PHONY: run-golangci-lint
run-golangci-lint: check-golangci-lint
@echo 'Running golangci-lint...'
@golangci-lint run $(PKGS) || echo "Note: golangci-lint found issues (exit code: $$?)"
## security/scan: Run security scan
.PHONY: security/scan
security/scan:
@if ! command -v gosec >/dev/null 2>&1; then \
echo "Error: 'gosec' is not installed. Installing..."; \
go install github.com/securego/gosec/v2/cmd/gosec@latest; \
fi
gosec $(PKGS)
## lint: Run linters
.PHONY: lint
lint:
@echo 'Linting...'
@echo 'Running go vet...'
@go vet $(PKGS)
@$(MAKE) run-golangci-lint || echo "Skipping external linting"
@$(MAKE) run-staticcheck || echo "Skipping staticcheck"
## audit: Tidy dependencies and format, vet and test all code
.PHONY: audit
audit:
@echo 'Tidying and verifying module dependencies...'
go mod tidy
go mod verify
@echo 'Formatting code...'
go fmt $(PKGS)
@echo 'Vetting code...'
go vet $(PKGS)
@$(MAKE) run-staticcheck || echo "Skipping staticcheck"
@$(MAKE) run-golangci-lint || echo "Skipping external linting"
@echo 'Running tests...'
go test -short -vet=off $(PKGS)
@echo '✅ Audit complete'
## audit/long: Run all tests including long-running tests (no -short flag)
.PHONY: audit/long
audit/long:
@echo 'Running ALL tests including long-running tests...'
@echo 'Tidying and verifying module dependencies...'
@go mod tidy
@go mod verify
@echo 'Formatting code...'
@go fmt $(PKGS)
@echo 'Vetting code...'
@go vet $(PKGS)
@$(MAKE) run-staticcheck || echo "Skipping staticcheck"
@$(MAKE) run-golangci-lint || echo "Skipping external linting"
@echo 'Starting test database...'
@$(MAKE) test/db/start
@echo 'Running ALL tests (including long tests, 10 minute timeout, no cache, with race detection)...'
@go test -race -count=1 -timeout=10m -vet=off $(PKGS)
@echo '✅ Full audit complete (including long tests)'
## audit/security: Run security audit
.PHONY: audit/security
audit/security:
@echo 'Checking for security vulnerabilities...'
$(MAKE) security/scan
@echo '✅ Security audit complete'
## vendor: Tidy and vendor dependencies
.PHONY: vendor
vendor:
@echo 'Tidying and verifying module dependencies...'
go mod tidy
go mod verify
@echo 'Vendoring dependencies...'
go mod vendor
## coverage: Run test suite with coverage
.PHONY: coverage
coverage:
go test -coverprofile=coverage.txt $(PKGS)
go tool cover -html=coverage.txt -o coverage.html
## fmt: Format all code
.PHONY: fmt
fmt:
@echo 'Formatting code...'
go fmt $(PKGS)
## vet: Vet all code
.PHONY: vet
vet:
@echo 'Vetting code...'
go vet $(PKGS)
## tidy: Tidy module dependencies
.PHONY: tidy
tidy:
@echo 'Tidying module dependencies...'
go mod tidy
go mod verify