Skip to content

Commit

Permalink
chore: add Go build workflow and Makefile
Browse files Browse the repository at this point in the history
  • Loading branch information
edmarfelipe committed Aug 24, 2024
1 parent 78cc846 commit 30a1915
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 0 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
47 changes: 47 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 ./...

33 changes: 33 additions & 0 deletions cmd/api.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/edmarfelipe/go-ci

go 1.23.0
24 changes: 24 additions & 0 deletions internal/env/env.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions internal/httpserver/handler_health.go
Original file line number Diff line number Diff line change
@@ -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"})
}
33 changes: 33 additions & 0 deletions internal/httpserver/handler_health_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
29 changes: 29 additions & 0 deletions internal/httpserver/server.go
Original file line number Diff line number Diff line change
@@ -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()
}
52 changes: 52 additions & 0 deletions internal/httpserver/server_util.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 30a1915

Please sign in to comment.