diff --git a/Makefile b/Makefile index d13e217..bccd935 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +# Use bash for all recipes (GitHub runners + many systems default /bin/sh = dash, no pipefail) +SHELL := /usr/bin/env bash +.SHELLFLAGS := -euo pipefail -c + MODULE = $(shell go list -m) VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || echo "1.0.0") PACKAGES := $(shell go list ./... | grep -v /vendor/) @@ -6,36 +10,47 @@ LDFLAGS := -ldflags "-X main.Version=${VERSION}" # DSN is required only for DB-related targets (migrate/testdata), not for build/lint/etc. APP_DSN ?= -# Fail only when DB targets are invoked (not on every make command) +PID_FILE := './.pid' +FSWATCH_FILE := './fswatch.cfg' + .PHONY: require-app-dsn require-app-dsn: @test -n "$(APP_DSN)" || (echo "APP_DSN is required. Set APP_DSN env var."; exit 1) # IMPORTANT: use '=' (recursive expansion) so APP_DSN is evaluated at runtime -MIGRATE = docker run -v $(shell pwd)/migrations:/migrations --network host migrate/migrate:v4.10.0 \ +MIGRATE = docker run --rm \ + -v $(shell pwd)/migrations:/migrations \ + --network host \ + migrate/migrate:v4.19.1 \ -path=/migrations/ -database "$(APP_DSN)" -PID_FILE := './.pid' -FSWATCH_FILE := './fswatch.cfg' - .PHONY: default default: help # generate help info from comments: thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html .PHONY: help help: ## help information about make commands - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: test -test: ## run unit tests +test: ## run unit tests (fails properly on test failure) and aggregate coverage @echo "mode: count" > coverage-all.out - @$(foreach pkg,$(PACKAGES), \ - go test -p=1 -cover -covermode=count -coverprofile=coverage.out ${pkg}; \ - tail -n +2 coverage.out >> coverage-all.out;) + @rm -f coverage.out + @for pkg in $(PACKAGES); do \ + echo "==> testing $$pkg"; \ + go test -p=1 -cover -covermode=count -coverprofile=coverage.out "$$pkg"; \ + tail -n +2 coverage.out >> coverage-all.out; \ + done + @rm -f coverage.out .PHONY: test-cover -test-cover: test ## run unit tests and show test coverage information - go tool cover -html=coverage-all.out +test-cover: test ## run unit tests and print coverage summary (CI-friendly) + @go tool cover -func=coverage-all.out | tail -n 1 + +.PHONY: cover-html +cover-html: test ## open HTML coverage report locally + @go tool cover -html=coverage-all.out .PHONY: run run: ## run the API server @@ -43,7 +58,7 @@ run: ## run the API server .PHONY: run-restart run-restart: ## restart the API server - @pkill -P `cat $(PID_FILE)` || true + @pkill -P "$$(cat $(PID_FILE) 2>/dev/null)" || true @printf '%*s\n' "80" '' | tr ' ' - @echo "Source file changed. Restarting server..." @go run ${LDFLAGS} cmd/server/main.go & echo $$! > $(PID_FILE) @@ -52,7 +67,8 @@ run-restart: ## restart the API server .PHONY: run-live run-live: ## run the API server with live reload support (requires fswatch) @go run ${LDFLAGS} cmd/server/main.go & echo $$! > $(PID_FILE) - @fswatch -x -o --event Created --event Updated --event Renamed -r internal pkg cmd config | xargs -n1 -I {} make run-restart + @fswatch -x -o --event Created --event Updated --event Renamed -r internal pkg cmd config | \ + xargs -n1 -I {} make run-restart .PHONY: build build: ## build the API server binary @@ -83,12 +99,12 @@ db-stop: ## stop the database server .PHONY: testdata testdata: require-app-dsn ## populate the database with test data - make migrate-reset + @$(MAKE) migrate-reset @echo "Populating test data..." @docker exec -it postgres psql "$(APP_DSN)" -f /testdata/testdata.sql .PHONY: lint -lint: ## run golint on all Go package +lint: ## run golint on all Go packages @golint $(PACKAGES) .PHONY: fmt diff --git a/internal/config/config.go b/internal/config/config.go index 9f43558..a086759 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "fmt" "github.com/go-ozzo/ozzo-validation/v4" "github.com/ico12319/devops-project/pkg/log" @@ -69,24 +70,32 @@ func Load(file string, logger log.Logger) (*Config, error) { JWTExpiration: defaultJWTExpirationHours, } - // load from YAML config file - bytes, err := readFileScoped(file) - if err != nil { - return nil, err - } - if err = yaml.Unmarshal(bytes, &c); err != nil { - return nil, err + // load from YAML config file (optional) + if file != "" { + bytes, err := readFileScoped(file) + if err != nil { + // If the file doesn't exist, ignore and rely on env. + if errors.Is(err, os.ErrNotExist) { + // continue + } else { + return nil, err + } + } else { + if err := yaml.Unmarshal(bytes, &c); err != nil { + return nil, err + } + } } // load from environment variables prefixed with "APP_" - if err = env.New("APP_", logger.Infof).Load(&c); err != nil { + if err := env.New("APP_", logger.Infof).Load(&c); err != nil { return nil, err } // validation - if err = c.Validate(); err != nil { + if err := c.Validate(); err != nil { return nil, err } - return &c, err + return &c, nil } diff --git a/internal/test/db.go b/internal/test/db.go index f6fd277..94355c4 100644 --- a/internal/test/db.go +++ b/internal/test/db.go @@ -6,30 +6,31 @@ import ( "github.com/ico12319/devops-project/pkg/dbcontext" "github.com/ico12319/devops-project/pkg/log" _ "github.com/lib/pq" // initialize posgresql for test - "path" - "runtime" "testing" ) var db *dbcontext.DB -// DB returns the database connection for testing purpose. func DB(t *testing.T) *dbcontext.DB { + t.Helper() + if db != nil { return db } + logger, _ := log.NewForTest() - dir := getSourcePath() - cfg, err := config.Load(dir+"/../../config/local.yml", logger) + + // Load config from env only (file optional / empty) + cfg, err := config.Load("", logger) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } + dbc, err := dbx.MustOpen("postgres", cfg.DSN) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } + dbc.LogFunc = logger.Infof db = dbcontext.New(dbc) return db @@ -45,9 +46,3 @@ func ResetTables(t *testing.T, db *dbcontext.DB, tables ...string) { } } } - -// getSourcePath returns the directory containing the source code that is calling this function. -func getSourcePath() string { - _, filename, _, _ := runtime.Caller(1) - return path.Dir(filename) -}