From 30a19156306c8b4206dba74a87fb77f665298d7a Mon Sep 17 00:00:00 2001 From: Edmar Felipe Date: Sat, 24 Aug 2024 14:25:48 -0300 Subject: [PATCH] chore: add Go build workflow and Makefile --- .github/workflows/build.yml | 50 +++++++++++++++++++++ .gitignore | 1 + .golangci.yml | 47 +++++++++++++++++++ Makefile | 12 +++++ cmd/api.go | 33 ++++++++++++++ go.mod | 3 ++ internal/env/env.go | 24 ++++++++++ internal/httpserver/handler_health.go | 13 ++++++ internal/httpserver/handler_health_test.go | 33 ++++++++++++++ internal/httpserver/server.go | 29 ++++++++++++ internal/httpserver/server_util.go | 52 ++++++++++++++++++++++ 11 files changed, 297 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .golangci.yml create mode 100644 Makefile create mode 100644 cmd/api.go create mode 100644 go.mod create mode 100644 internal/env/env.go create mode 100644 internal/httpserver/handler_health.go create mode 100644 internal/httpserver/handler_health_test.go create mode 100644 internal/httpserver/server.go create mode 100644 internal/httpserver/server_util.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..49116a3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,50 @@ +name: Go + +env: + GO_VERSION: '1.23' + GO_LINT_VERSION: 'v1.60.3' + +on: + push: + branches: + - develop + - main + + pull_request: +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{env.GO_VERSION}} + - name: Build + run: make build + + lint: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{env.GO_VERSION}} + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: ${{env.GO_LINT_VERSION}} + + test: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{env.GO_VERSION}} + - name: Test + run: make test diff --git a/.gitignore b/.gitignore index 6f72f89..7fdd46b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +coverage.txt # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..102c9fc --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,47 @@ +linters: + disable-all: true + enable: + - bodyclose + - errcheck + - dogsled + - errcheck + - exhaustive + - funlen + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - goprintffuncname + - gosimple + - govet + - ineffassign + - lll + - maintidx + - misspell + - nakedret + - nakedret + - nestif + - nilerr + - noctx + - nolintlint + - revive + - rowserrcheck + - staticcheck + - unused + - typecheck + - unconvert + - unparam + - unused + - whitespace + +issues: + exclude-rules: + - path: _test\.go + linters: + - errcheck + - noctx + - govet + - staticcheck + - bodyclose + - funlen diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3f93bb5 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +run: + go run -race cmd/api.go + +build: + go build -race -o bin/main cmd/api.go + +lint: + golangci-lint run ./... + +test: + go test -race -coverprofile=coverage.txt ./... + diff --git a/cmd/api.go b/cmd/api.go new file mode 100644 index 0000000..0a3812f --- /dev/null +++ b/cmd/api.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + + "github.com/edmarfelipe/go-ci/internal/env" + "github.com/edmarfelipe/go-ci/internal/httpserver" +) + +func main() { + if err := run(); err != nil { + slog.Error("failed to start", "err", err) + os.Exit(1) + } +} + +func run() error { + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) + + cfg, err := env.Load() + if err != nil { + return fmt.Errorf("error loading configs: %w", err) + } + + err = httpserver.New(cfg).Start() + if err != nil { + return fmt.Errorf("error starting the http server: %w", err) + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bbb46bb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/edmarfelipe/go-ci + +go 1.23.0 diff --git a/internal/env/env.go b/internal/env/env.go new file mode 100644 index 0000000..83b1d87 --- /dev/null +++ b/internal/env/env.go @@ -0,0 +1,24 @@ +package env + +import ( + "os" +) + +type Env struct { + ServiceAddr string +} + +// Load loads the environment variables from the system +func Load() (*Env, error) { + return &Env{ + ServiceAddr: getEnv("SERVICE_PORT", ":8080"), + }, nil +} + +// getEnv returns the value of an environment variable, or a fallback value if it's not set. +func getEnv(key string, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/internal/httpserver/handler_health.go b/internal/httpserver/handler_health.go new file mode 100644 index 0000000..99345e6 --- /dev/null +++ b/internal/httpserver/handler_health.go @@ -0,0 +1,13 @@ +package httpserver + +import ( + "net/http" +) + +type HealthResponse struct { + Status string `json:"status"` +} + +func HealthHandler(w http.ResponseWriter, _ *http.Request) { + WriteJSON(w, http.StatusOK, HealthResponse{Status: "ok"}) +} diff --git a/internal/httpserver/handler_health_test.go b/internal/httpserver/handler_health_test.go new file mode 100644 index 0000000..5138c59 --- /dev/null +++ b/internal/httpserver/handler_health_test.go @@ -0,0 +1,33 @@ +package httpserver_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/edmarfelipe/go-ci/internal/httpserver" +) + +func TestHealthHandler(t *testing.T) { + t.Run("Should return a health response", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(httpserver.HealthHandler)) + + resp, err := http.Get(srv.URL) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected status code %d, got %d", http.StatusOK, resp.StatusCode) + } + defer resp.Body.Close() + + var health httpserver.HealthResponse + json.NewDecoder(resp.Body).Decode(&health) + + if health.Status != "ok" { + t.Fatalf("expected status %s, got %s", "ok", health.Status) + } + }) +} diff --git a/internal/httpserver/server.go b/internal/httpserver/server.go new file mode 100644 index 0000000..39bcf0a --- /dev/null +++ b/internal/httpserver/server.go @@ -0,0 +1,29 @@ +package httpserver + +import ( + "log/slog" + "net/http" + + "github.com/edmarfelipe/go-ci/internal/env" +) + +type HTTPServer struct { + srv *http.Server +} + +func New(cfg *env.Env) *HTTPServer { + router := http.NewServeMux() + router.HandleFunc("GET /health", HealthHandler) + + return &HTTPServer{ + srv: &http.Server{ + Addr: cfg.ServiceAddr, + Handler: router, + }, + } +} + +func (s *HTTPServer) Start() error { + slog.Info("starting http server", "addr", s.srv.Addr) + return s.srv.ListenAndServe() +} diff --git a/internal/httpserver/server_util.go b/internal/httpserver/server_util.go new file mode 100644 index 0000000..47de8c9 --- /dev/null +++ b/internal/httpserver/server_util.go @@ -0,0 +1,52 @@ +package httpserver + +import ( + "encoding/json" + "log/slog" + "net/http" +) + +// ErrorResponse is a generic error response. +type ErrorResponse struct { + Error string `json:"error"` +} + +// WriteBadRequest writes a bad request response. +func WriteBadRequest(w http.ResponseWriter, err string) { + WriteJSON(w, http.StatusBadRequest, ErrorResponse{Error: err}) +} + +// WriteNotFound writes a not found response. +func WriteNotFound(w http.ResponseWriter, err string) { + WriteJSON(w, http.StatusNotFound, ErrorResponse{Error: err}) +} + +// WriteUnauthorized writes an unauthorized response. +func WriteUnauthorized(w http.ResponseWriter, err string) { + WriteJSON(w, http.StatusUnauthorized, ErrorResponse{Error: err}) +} + +// WriteInternalError logs the error and writes a generic error response. +func WriteInternalError(w http.ResponseWriter, err error) { + slog.Error("internal error", "err", err.Error()) + WriteJSON(w, http.StatusInternalServerError, ErrorResponse{Error: err.Error()}) +} + +// WriteJSON writes a JSON response. +func WriteJSON(w http.ResponseWriter, code int, data any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + err := json.NewEncoder(w).Encode(data) + if err != nil { + http.Error(w, "failed to write response", http.StatusInternalServerError) + } +} + +// DecodeJSON decodes a JSON request body. +func DecodeJSON[T any](r *http.Request) (T, error) { + var v T + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + return v, err + } + return v, nil +}