-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
461 lines (406 loc) · 21.1 KB
/
Copy pathMakefile
File metadata and controls
461 lines (406 loc) · 21.1 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
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# OpenWatch (Go rebuild) — Makefile
#
# Build, test, lint, generate, package. Most targets gain real
# implementations as Stage 0 days land:
#
# Day 1 (now): build, test, tidy, lint, clean, version
# Day 3: migrate
# Day 5: generate (oapi-codegen)
# Day 11: rpm, deb
# Day 12: build-fips
#
# See app/docs/stage_0_walking_skeleton.md for the day-by-day plan.
# -----------------------------------------------------------------------------
# Variables
# -----------------------------------------------------------------------------
BINARY := openwatch
DIST_DIR := dist
CMD_DIR := ./cmd/openwatch
SPA_DIR := internal/server/spa
# golangci-lint version. MUST match the version pinned in
# .github/workflows/go-ci.yml (`go install ...golangci-lint@vX.Y.Z`) so a
# local `make lint` reproduces CI exactly — a newer local binary surfaces
# lints CI won't enforce yet (and vice-versa). Bump both together.
GOLANGCI_VERSION := 1.64.8
# Version metadata injected at build time. The Go rebuild has its own
# version track (packaging/version.env) so its milestones decouple from
# the legacy Python project at ../VERSION. Fallback order: local
# version.env → repo-root VERSION → hardcoded dev default.
VERSION := $(shell . packaging/version.env 2>/dev/null && echo "$$VERSION" || cat ../VERSION 2>/dev/null || echo "0.1.0-dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILDTIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
# ldflags inject build metadata into internal/version.
LDFLAGS := -ldflags "\
-X github.com/Hanalyx/openwatch/internal/version.Version=$(VERSION) \
-X github.com/Hanalyx/openwatch/internal/version.Commit=$(COMMIT) \
-X github.com/Hanalyx/openwatch/internal/version.BuildTime=$(BUILDTIME)"
LDFLAGS_FIPS := -ldflags "\
-X github.com/Hanalyx/openwatch/internal/version.Version=$(VERSION) \
-X github.com/Hanalyx/openwatch/internal/version.Commit=$(COMMIT) \
-X github.com/Hanalyx/openwatch/internal/version.BuildTime=$(BUILDTIME) \
-X github.com/Hanalyx/openwatch/internal/version.FIPS=true"
# -----------------------------------------------------------------------------
# Build (Day 1)
# -----------------------------------------------------------------------------
.PHONY: build
build: $(DIST_DIR) internal/server/openapi_embed.yaml spa
go build $(LDFLAGS) -o $(DIST_DIR)/$(BINARY) $(CMD_DIR)
@echo "built $(DIST_DIR)/$(BINARY) ($(VERSION) / $(COMMIT))"
$(DIST_DIR):
@mkdir -p $(DIST_DIR)
# -----------------------------------------------------------------------------
# Test & lint (Day 1)
# -----------------------------------------------------------------------------
.PHONY: test
test: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
go test ./...
# test-integration: runs the full suite (including DB-backed integration
# tests) against a dedicated TEST database. The DSN is constructed here
# so the operator can never accidentally point it at the dev or prod
# DB. These tests TRUNCATE tables between cases — pointing at a non-test
# DB would destroy real data. Run `make test-db-create` first if the
# database does not exist yet.
.PHONY: test-integration
test-integration: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
@DB="$${OPENWATCH_TEST_DB:-openwatch_go_test}"; \
case "$$DB" in *_test) ;; \
*) echo "ERROR: OPENWATCH_TEST_DB ($$DB) must end with _test"; exit 1 ;; esac; \
HOST="$${OPENWATCH_TEST_DB_HOST:-127.0.0.1}"; \
PORT="$${OPENWATCH_TEST_DB_PORT:-5432}"; \
USER="$${OPENWATCH_TEST_DB_USER:-openwatch}"; \
PASS="$${OPENWATCH_TEST_DB_PASS:-openwatch_secure_db_2025}"; \
DSN="postgres://$$USER:$$PASS@$$HOST:$$PORT/$$DB?sslmode=disable"; \
echo "Using test DB: $$DB on $$HOST:$$PORT"; \
OPENWATCH_TEST_DSN="$$DSN" go test -race -p 1 ./...
# test-db-create: provisions the test database (idempotent — exits 0
# if it already exists) and applies all migrations.
.PHONY: test-db-create
test-db-create: $(DIST_DIR)/$(BINARY)
@DB="$${OPENWATCH_TEST_DB:-openwatch_go_test}"; \
case "$$DB" in *_test) ;; \
*) echo "ERROR: OPENWATCH_TEST_DB ($$DB) must end with _test"; exit 1 ;; esac; \
HOST="$${OPENWATCH_TEST_DB_HOST:-127.0.0.1}"; \
PORT="$${OPENWATCH_TEST_DB_PORT:-5432}"; \
USER="$${OPENWATCH_TEST_DB_USER:-openwatch}"; \
PASS="$${OPENWATCH_TEST_DB_PASS:-openwatch_secure_db_2025}"; \
PGPASSWORD="$$PASS" psql -h $$HOST -p $$PORT -U $$USER -d postgres \
-tc "SELECT 1 FROM pg_database WHERE datname='$$DB'" | grep -q 1 \
|| PGPASSWORD="$$PASS" psql -h $$HOST -p $$PORT -U $$USER -d postgres \
-c "CREATE DATABASE $$DB OWNER $$USER;"; \
DSN="postgres://$$USER:$$PASS@$$HOST:$$PORT/$$DB?sslmode=disable"; \
echo "Migrating test DB to current schema..."; \
OPENWATCH_DATABASE_DSN="$$DSN" ./$(DIST_DIR)/$(BINARY) migrate
# test-db-drop: tears down the test database. Refuses any DB name that
# does not end in _test.
.PHONY: test-db-drop
test-db-drop:
@DB="$${OPENWATCH_TEST_DB:-openwatch_go_test}"; \
case "$$DB" in *_test) ;; \
*) echo "ERROR: $$DB must end with _test"; exit 1 ;; esac; \
HOST="$${OPENWATCH_TEST_DB_HOST:-127.0.0.1}"; \
PORT="$${OPENWATCH_TEST_DB_PORT:-5432}"; \
USER="$${OPENWATCH_TEST_DB_USER:-openwatch}"; \
PASS="$${OPENWATCH_TEST_DB_PASS:-openwatch_secure_db_2025}"; \
PGPASSWORD="$$PASS" psql -h $$HOST -p $$PORT -U $$USER -d postgres \
-c "DROP DATABASE IF EXISTS $$DB;"
.PHONY: tidy
tidy:
go mod tidy
# -----------------------------------------------------------------------------
# Quality + security gates (release-ci-gates.spec.yaml)
# -----------------------------------------------------------------------------
# vet: built-in suspicious-construct check. Always available.
# Depends on internal/server/openapi_embed.yaml because go:embed
# resolves at vet time too — the source tree must contain the embedded
# file for vet to type-check the embedding declaration.
.PHONY: vet
vet: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
go vet ./...
# lint: golangci-lint runs staticcheck + gosec + govet + others per .golangci.yml.
.PHONY: lint
lint: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
@if command -v golangci-lint >/dev/null 2>&1; then \
have=$$(golangci-lint version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \
if [ "$$have" != "$(GOLANGCI_VERSION)" ]; then \
echo "ERROR: golangci-lint $$have installed, but this repo pins $(GOLANGCI_VERSION)."; \
echo " Local lint must match CI (the config is v1 format; a v2 binary cannot"; \
echo " load it). Install the pinned version, then re-run:"; \
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLANGCI_VERSION)"; \
exit 1; \
fi; \
golangci-lint run; \
else \
echo "ERROR: golangci-lint not installed (this repo pins v$(GOLANGCI_VERSION)):"; \
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLANGCI_VERSION)"; \
exit 1; \
fi
# vuln: known-CVE scan against deps + stdlib (call-graph aware).
# Auto-installs govulncheck if absent.
.PHONY: vuln
vuln: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
@if ! command -v govulncheck >/dev/null 2>&1; then \
echo "installing govulncheck..."; \
go install golang.org/x/vuln/cmd/govulncheck@latest; \
fi
govulncheck ./...
# test-race: full suite with the race detector. Slower than `make test`.
# Integration tests still need OPENWATCH_TEST_DSN; without it they skip.
# -p 1 serializes packages so they don't trample each other's DB state.
.PHONY: test-race
test-race: internal/server/openapi_embed.yaml $(SPA_DIR)/index.html
go test -race -p 1 ./...
# check: the single pre-push gate. Chains vet → lint → vuln → test-race.
# First failure aborts the chain (make's default target dependency semantics).
.PHONY: check
check: vet lint vuln test-race
@echo "make check: all gates passed"
# generate-api-types: regenerate the frontend OpenAPI client types
# (frontend/src/api/schema.d.ts) from api/openapi.yaml. Kept separate from
# generate-api (which emits the Go server stubs) so the drift check can
# regenerate both halves of the contract.
.PHONY: generate-api-types
generate-api-types:
cd frontend && { [ -d node_modules ] || npm ci --no-audit --no-fund; } && npm run api:types
# check-generated: fail if committed generated code has drifted from the
# api/openapi.yaml contract. Regenerates the Go server stubs and the frontend
# client types, then asserts git sees no change. This is the guard that stops an
# OpenAPI edit from landing without a matching `make generate-api` — CI verified
# only go.mod/go.sum before, so an un-regenerated contract used to pass silently.
.PHONY: check-generated
check-generated: generate-api generate-api-types
@git diff --exit-code -- internal/server/api/server.gen.go frontend/src/api/schema.d.ts \
|| { echo "ERROR: generated code is out of sync with api/openapi.yaml."; \
echo "Run 'make generate-api generate-api-types' and commit the result."; \
exit 1; }
@echo "check-generated: server.gen.go and schema.d.ts are in sync"
# spec-check: run the Specter gate CI enforces (pinned version + annotation
# hygiene + structural coverage) so spec drift is caught before pushing. The
# policy lives in scripts/specter-gate.py and NOT here: this target and the
# workflow used to carry their own greps over specter's text output, which is
# two policies that drift apart in silence. Skips cleanly if specter is
# not on PATH; CI runs the authoritative gate regardless.
.PHONY: spec-check
spec-check:
@command -v specter >/dev/null 2>&1 || { echo "spec-check: specter not on PATH; skipping (CI enforces it)"; exit 0; }
python3 -S scripts/specter-gate.py
# release-status: render release/gates.toml for a candidate and return a
# go/no-go verdict. Needs `gh` authenticated to read check runs and assets.
#
# The script exits 0 for GO, 1 for NO-GO and 2 for a lookup error. make
# collapses any recipe failure to its own status, so anything automated should
# call scripts/release-status.py directly to tell a NO-GO from a broken lookup.
#
# make release-status # most recent tag
# make release-status TAG=v0.7.0-rc.3
.PHONY: release-status
release-status:
python3 scripts/release-status.py $(if $(TAG),--tag $(TAG),)
# release-status-test: the checker's own guards. Stdlib only, no network,
# runs in milliseconds. CI runs this in the Quality gates job.
#
# -S disables site packages, which makes "stdlib only" a fact the target
# enforces rather than a comment. A third-party import fails here instead of
# on the one CI runner that has not got the package.
.PHONY: release-status-test
release-status-test:
python3 -S scripts/test_release_status.py
python3 -S scripts/test_specter_gate.py
python3 -S scripts/test_doc_style_gate.py
# docs-style: the Hanalyx documentation style gate (em dashes, emojis, AI
# speak, US English, reading level). The ONE shared invocation: CI's "Doc Style"
# job and the pre-commit hook both run this target rather than rebuilding the
# command, so the three cannot drift into different policies.
# Canonical rules: Context Plane dev/DEVELOPER_DOCUMENTATION_STYLE_GUIDE.
#
# The gate verifies the checker before it runs it. The checker is a SHARED tool
# refetched wholesale on an upgrade, and nothing used to record which version
# this repo ran, so a superseded copy stayed authoritative for a month while
# four defects we had reported ourselves were already fixed upstream.
# `.doc-style-version` is now the single source and the gate fails closed if it
# is missing or disagrees. The version number lives ONLY in that file. This
# comment deliberately does not repeat it: the comment that did went stale and
# named a version two releases behind the one actually running.
#
# The scan is --all, not --changed. `--changed` resolves a commit RANGE and
# falls back to the index only when that range is empty, so it never reads the
# working tree: a hook keyed on it is blind to the files being committed. The
# full sweep is ~4s over ~1000 tracked files, cheap enough to be the default.
# Checked types are the checker's fifteen extensions, source comments included.
# Files git ignores are outside this gate; see specs/release/ci-gates C-08.
# Single-file python3 scripts, no dependencies; -S enforces that.
.PHONY: docs-style
docs-style:
python3 -S scripts/doc-style-gate.py
# ci-local: run locally what CI's "Quality + security gates" job runs, so a
# failure is caught before the ~9-minute push round-trip. `make check` alone
# omits the generated-code, spec, and frontend gates — this target is the
# full mirror.
.PHONY: ci-local
ci-local: check-generated vet lint vuln spec-check test-race docs-style
cd frontend && { [ -d node_modules ] || npm ci --no-audit --no-fund; } && npx vitest run
@if [ -z "$$OPENWATCH_TEST_DSN" ]; then \
echo ""; \
echo "ci-local WARNING: OPENWATCH_TEST_DSN is unset, so every DB-gated suite was SKIPPED."; \
echo " CI runs them. A change can pass here and fail there; that happened three times"; \
echo " on the v0.7 branches. Start the local test database and re-run:"; \
echo " make test-db && eval \"\$$(scripts/test-db.sh dsn)\" && make ci-local"; \
echo ""; \
fi
@echo "ci-local: all gates passed — safe to push"
## test-db: start the local test database (mirrors the CI service container)
.PHONY: test-db test-db-down
test-db:
@scripts/test-db.sh up
@echo "eval \"\$$(scripts/test-db.sh dsn)\" # to point tests at it"
test-db-down:
@scripts/test-db.sh down
.PHONY: clean
clean:
rm -rf $(DIST_DIR) $(SPA_DIR)
@echo "cleaned $(DIST_DIR)/"
.PHONY: version
version:
@echo "VERSION=$(VERSION)"
@echo "COMMIT=$(COMMIT)"
@echo "BUILDTIME=$(BUILDTIME)"
# -----------------------------------------------------------------------------
# Codegen (Day 5: oapi-codegen, sqlc; Day 5+: registry codegen)
# -----------------------------------------------------------------------------
.PHONY: generate
generate: generate-audit generate-rbac generate-license generate-api
@echo "generate: audit events + RBAC + license features + OpenAPI server stubs regenerated (sqlc lands later)"
.PHONY: generate-audit
generate-audit:
go run scripts/gen-audit-events.go
.PHONY: generate-rbac
generate-rbac:
go run scripts/gen-rbac.go
.PHONY: generate-license
generate-license:
go run scripts/gen-license-features.go
# Pinned so `make generate-api` is byte-reproducible. An unpinned @latest let the
# generated stubs drift by generator version, which made the generated-drift gate
# fail on correct code. Bump deliberately (and regenerate) when upgrading.
OAPI_CODEGEN_VERSION := v2.7.0
.PHONY: generate-api
generate-api: internal/server/openapi_embed.yaml
@if ! $(HOME)/go/bin/oapi-codegen --version 2>/dev/null | grep -q '$(OAPI_CODEGEN_VERSION)'; then \
echo "installing oapi-codegen $(OAPI_CODEGEN_VERSION)..."; \
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@$(OAPI_CODEGEN_VERSION); \
fi
$(HOME)/go/bin/oapi-codegen --config api/oapi-codegen.yaml api/openapi.yaml
@echo "generated internal/server/api/server.gen.go"
@echo "refreshed internal/server/openapi_embed.yaml (kept in sync with api/openapi.yaml)"
# The OpenAPI spec is also embedded into the binary so the /api/v1/openapi.yaml
# and /docs routes can serve it air-gap-clean. go:embed cannot follow paths
# outside the package directory, so we keep a build-time copy under
# internal/server/. The copy is gitignored; rebuilt by `make build`.
internal/server/openapi_embed.yaml: api/openapi.yaml
cp $< $@
# Embedded SPA. The Go binary serves the React app via go:embed (see
# internal/server/spa.go). go:embed cannot follow paths outside the package
# directory, so the build stages the UI under internal/server/spa/. The
# directory is gitignored.
#
# Two ways to populate it:
# - $(SPA_DIR)/index.html — a lightweight stub so go vet/lint/test compile
# the embed without a Node toolchain (created on demand, fast).
# - make spa — the real `vite build` output, for release binaries.
#
# The stub also stages assets/app-abc123.js — a content-hashed asset fixture the
# static-delivery tests (system-http-server AC-15/AC-16) serve to assert gzip +
# immutable caching. It must stay >=256 bytes and compressible so the handler's
# gzip path engages, and contain `console.log` (the gzip test decodes and checks
# for it). The directory is gitignored, so this stub is what CI tests against.
$(SPA_DIR)/index.html:
@mkdir -p $(SPA_DIR)/assets
@printf '%s\n' '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>OpenWatch</title></head><body>OpenWatch SPA placeholder. Run `make spa` (or `make build`) to embed the real UI.</body></html>' > $@
@printf '%s\n' \
'// OpenWatch SPA placeholder asset (test stub for static-delivery tests).' \
'// Run `make spa` (or `make build`) to embed the real Vite output instead.' \
'console.log("OpenWatch SPA placeholder");' \
'/* padding so the stub exceeds the 256-byte gzip threshold and stays */' \
'/* compressible (repeated lines shrink well under gzip). ------------- */' \
'/* filler ------------------------------------------------------------ */' \
'/* filler ------------------------------------------------------------ */' \
'/* filler ------------------------------------------------------------ */' \
'/* filler ------------------------------------------------------------ */' \
> $(SPA_DIR)/assets/app-abc123.js
# Build the real frontend and stage it into the embed directory. Uses
# `vite build` directly rather than the frontend `build` script's `tsc -b`,
# which is gated on the frontend's (deferred) type-error cleanup. npm ci runs
# only when node_modules is absent.
.PHONY: spa
spa:
cd frontend && { [ -d node_modules ] || npm ci --no-audit --no-fund; } && npx vite build
@rm -rf $(SPA_DIR) && mkdir -p $(SPA_DIR) && cp -r frontend/dist/. $(SPA_DIR)/
@echo "embedded SPA: frontend/dist -> $(SPA_DIR)/"
# -----------------------------------------------------------------------------
# Database migrations (Day 3)
# -----------------------------------------------------------------------------
.PHONY: migrate
migrate:
go run ./cmd/openwatch migrate
# -----------------------------------------------------------------------------
# Packaging (Day 11: RPM + DEB)
# -----------------------------------------------------------------------------
.PHONY: rpm
rpm:
@bash packaging/rpm/build-rpm.sh
.PHONY: deb
deb:
@bash packaging/deb/build-deb.sh
# Cross-compiled arm64 variants (CGO disabled; no C cross-toolchain needed).
.PHONY: rpm-arm64
rpm-arm64:
@ARCH=arm64 bash packaging/rpm/build-rpm.sh
.PHONY: deb-arm64
deb-arm64:
@ARCH=arm64 bash packaging/deb/build-deb.sh
# Kensa rule corpus, packaged separately (noarch RPM + arch:all DEB). The
# openwatch packages declare a hard dependency on this; an install needs
# both. Version tracks the vendored kensa module, not the platform.
.PHONY: kensa-rules
kensa-rules:
@bash packaging/kensa-rules/build-kensa-rules.sh
# All release artifacts: openwatch RPM + DEB for amd64 and arm64, plus the
# arch-independent kensa-rules corpus package (one RPM + one DEB).
.PHONY: packages
packages: rpm rpm-arm64 deb deb-arm64 kensa-rules
@echo "built openwatch RPM + DEB (amd64 + arm64) and kensa-rules in $(DIST_DIR)/"
# -----------------------------------------------------------------------------
# FIPS build (Day 12: microsoft/go toolchain)
# -----------------------------------------------------------------------------
.PHONY: build-fips
build-fips: $(DIST_DIR) internal/server/openapi_embed.yaml spa
GOFIPS140=v1.0.0 go build $(LDFLAGS_FIPS) -o $(DIST_DIR)/$(BINARY)-fips $(CMD_DIR)
@echo "built $(DIST_DIR)/$(BINARY)-fips ($(VERSION) / $(COMMIT)) [FIPS 140-3]"
# -----------------------------------------------------------------------------
# Help
# -----------------------------------------------------------------------------
.PHONY: help
help:
@echo "OpenWatch (Go rebuild) — Makefile targets"
@echo ""
@echo " build Build the openwatch binary into dist/ [Day 1]"
@echo " test Run all Go tests [Day 1]"
@echo " tidy Run go mod tidy [Day 1]"
@echo " clean Remove dist/ [Day 1]"
@echo " version Print version metadata that will be injected [Day 1]"
@echo ""
@echo "Pre-merge gates (release-ci-gates spec):"
@echo " vet go vet ./... [gate]"
@echo " lint golangci-lint (staticcheck + gosec + ...) [gate]"
@echo " vuln govulncheck ./... (known CVEs in deps + stdlib) [gate]"
@echo " test-race go test -race ./... (data race detection) [gate]"
@echo " check vet + lint + vuln + test-race (run before pushing) [gate]"
@echo ""
@echo "Codegen + DB:"
@echo " generate Run codegen (oapi-codegen, sqlc, registries) [Day 5]"
@echo " migrate Run goose database migrations (openwatch migrate)"
@echo ""
@echo "Packaging:"
@echo " rpm Build RPM package [Day 11]"
@echo " deb Build DEB package [Day 11]"
@echo " build-fips Build with FIPS 140-3 (GOFIPS140 native) [Day 12]"
.DEFAULT_GOAL := help