diff --git a/cli/command/cmds/build.go b/cli/command/cmds/build.go index 7f77728..02dfd15 100644 --- a/cli/command/cmds/build.go +++ b/cli/command/cmds/build.go @@ -85,7 +85,7 @@ func (c *buildCommand) invoke() *cobra.Command { if err != nil { return &errorhandling.CommandError{ Err: fmt.Errorf("failed to load template resources for category %q: %w", category, err), - ExitCode: errorhandling.ExitGenericError, + ExitCode: errorhandling.ExitTemplateError, HelpText: "Failed to load template resources. Please check the category name and try again.", } } @@ -107,10 +107,11 @@ func (c *buildCommand) invoke() *cobra.Command { if err := templater.Render(ctx, c.outputPath); err != nil { return &errorhandling.CommandError{ Err: fmt.Errorf("failed to render template %q: %w", category, err), - ExitCode: errorhandling.ExitGenericError, + ExitCode: errorhandling.ExitTemplateError, HelpText: "Please check the template and try again.", } } + logger.WithFields(logrus.Fields{ "category": category, "output": c.outputPath, diff --git a/cli/command/cmds/build_test.go b/cli/command/cmds/build_test.go index 70ce01c..5b19a2c 100644 --- a/cli/command/cmds/build_test.go +++ b/cli/command/cmds/build_test.go @@ -89,7 +89,7 @@ func TestBuildCommand(t *testing.T) { return newTestBuildCommand(errorLoader, nil) }, wantErr: true, - wantExit: errorhandling.ExitGenericError, + wantExit: errorhandling.ExitTemplateError, }, { name: "renderer failure returns generic error", @@ -100,7 +100,7 @@ func TestBuildCommand(t *testing.T) { return newTestBuildCommand(successLoader, errors.New("render failure")) }, wantErr: true, - wantExit: errorhandling.ExitGenericError, + wantExit: errorhandling.ExitTemplateError, }, { name: "successful render on empty dir prints success message", diff --git a/cli/entrypoint/entrypoint.go b/cli/entrypoint/entrypoint.go index 0891c81..acb0631 100644 --- a/cli/entrypoint/entrypoint.go +++ b/cli/entrypoint/entrypoint.go @@ -51,7 +51,7 @@ func Run(metadata []byte) { }) if err != nil { printError("Error creating command: %v", err) - os.Exit(errorhandling.ExitGenericError.Int()) + os.Exit(errorhandling.ExitRuntimeError.Int()) } var exitCode int @@ -71,7 +71,7 @@ func handlerExecError(err error) int { return cmdErr.ExitCode.Int() } fmt.Printf("An unexpected error occurred: %v\n", err) - return errorhandling.ExitGenericError.Int() + return errorhandling.ExitRuntimeError.Int() } func printError(format string, args ...any) { diff --git a/cli/internal/errorhandling/errors.go b/cli/internal/errorhandling/errors.go index 6055bea..f3eb690 100644 --- a/cli/internal/errorhandling/errors.go +++ b/cli/internal/errorhandling/errors.go @@ -15,7 +15,7 @@ func (e ExitCode) Int() int { const ( ExitSuccess ExitCode = 0 - ExitGenericError ExitCode = 1 + ExitRuntimeError ExitCode = 1 ExitPanicError ExitCode = 2 ExitTemplateError ExitCode = 3 ExitInputError ExitCode = 4 diff --git a/cli/internal/errorhandling/errors_test.go b/cli/internal/errorhandling/errors_test.go index 9bf9381..d8838bf 100644 --- a/cli/internal/errorhandling/errors_test.go +++ b/cli/internal/errorhandling/errors_test.go @@ -11,7 +11,7 @@ import ( func TestCommandError(t *testing.T) { mockErr := CommandError{ Err: errors.New("mock error"), - ExitCode: ExitGenericError, + ExitCode: ExitRuntimeError, HelpText: "This is some help text.", } diff --git a/cli/internal/templating/embed.go b/cli/internal/templating/embed.go index f4e09eb..0cd8727 100644 --- a/cli/internal/templating/embed.go +++ b/cli/internal/templating/embed.go @@ -11,7 +11,7 @@ import ( "github.com/jgfranco17/hackstack/cli/internal/logging" ) -//go:embed resources +//go:embed all:resources var embeddedResources embed.FS // CLIProject holds the variables required to render the CLI project templates. @@ -41,10 +41,22 @@ func (d *CLIProject) Validate() error { return nil } +// Load retrieves the embedded template files for the specified category and returns +// them as an fs.FS instance for use in rendering. The category must be one of the +// currently-supported categories. func Load(ctx context.Context, category string) (fs.FS, error) { logger := logging.FromContext(ctx).WithField("module", "templating") + allowedCategories := map[string]bool{ + "backend": true, + "cli": true, + } + category = strings.ToLower(category) + if _, ok := allowedCategories[category]; !ok { + return nil, fmt.Errorf("invalid templating category %q", category) + } + subDirPath := filepath.Join("resources", category) sub, err := fs.Sub(embeddedResources, subDirPath) if err != nil { diff --git a/cli/internal/templating/embed_test.go b/cli/internal/templating/embed_test.go index 53a7b67..9290f94 100644 --- a/cli/internal/templating/embed_test.go +++ b/cli/internal/templating/embed_test.go @@ -67,16 +67,43 @@ func TestCLIProject_Validate(t *testing.T) { } func TestLoad_ValidCategory(t *testing.T) { + testCases := []struct { + name string + category string + invalid bool + }{ + {name: "backend category", category: "backend"}, + {name: "cli category", category: "cli"}, + {name: "category case insensitivity", category: "CLI"}, + {name: "invalid category", category: "unknown", invalid: true}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := testContext(t) + sub, err := Load(ctx, tc.category) + + if tc.invalid { + require.Error(t, err) + assert.ErrorContains(t, err, "invalid templating category") + } else { + require.NoError(t, err) + require.NotNil(t, sub) + + entries, err := fs.ReadDir(sub, ".") + require.NoError(t, err) + assert.NotEmpty(t, entries) + } + }) + } +} + +func TestLoad_InvalidCategory(t *testing.T) { ctx := testContext(t) - sub, err := Load(ctx, "cli") - require.NoError(t, err) - require.NotNil(t, sub) + _, err := Load(ctx, "unknown") - // Confirm the returned FS is non-empty. - entries, err := fs.ReadDir(sub, ".") - require.NoError(t, err) - assert.NotEmpty(t, entries, "loaded FS should contain template files") + require.Error(t, err) + assert.Contains(t, err.Error(), `invalid templating category "unknown"`) } func TestLoad_CategoryIsCaseInsensitive(t *testing.T) { diff --git a/cli/internal/templating/engine.go b/cli/internal/templating/engine.go index 64d32a4..4558798 100644 --- a/cli/internal/templating/engine.go +++ b/cli/internal/templating/engine.go @@ -7,9 +7,11 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "text/template" - "github.com/jgfranco17/hackstack/cli/internal/errorhandling" + "golang.org/x/sync/errgroup" + "github.com/jgfranco17/hackstack/cli/internal/fileutils" "github.com/jgfranco17/hackstack/cli/internal/logging" ) @@ -29,7 +31,9 @@ func NewEngine(files fs.FS, data CLIProject) *Engine { func (e *Engine) Render(ctx context.Context, outputPath string) error { logger := logging.FromContext(ctx).WithField("module", "templating") - count := 0 + var count atomic.Int64 + g, ctx := errgroup.WithContext(ctx) + walker := func(path string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("walk error at %q: %w", path, err) @@ -40,30 +44,38 @@ func (e *Engine) Render(ctx context.Context, outputPath string) error { destPath := filepath.Join(outputPath, filepath.FromSlash(path)) + var work func() error switch { case strings.HasSuffix(path, ".j2"): destPath = strings.TrimSuffix(destPath, ".j2") - logger.WithField("file", path).Trace("Rendering from template") - count++ - return renderTemplate(e.Files, path, destPath, e.Data) + work = func() error { + logger.WithField("file", path).Trace("Rendering from template") + return renderTemplate(e.Files, path, destPath, e.Data) + } case strings.HasSuffix(path, ".copy"): destPath = strings.TrimSuffix(destPath, ".copy") - logger.WithField("file", path).Trace("Copying file") - count++ - return fileutils.CopyFile(e.Files, path, destPath) + work = func() error { + logger.WithField("file", path).Trace("Copying file") + return fileutils.CopyFile(e.Files, path, destPath) + } default: - return fmt.Errorf("unrecognized resource extension for %q: expected .j2 or .copy", path) + return fmt.Errorf("unrecognized resource extension for %q", path) } + + count.Add(1) + g.Go(work) + return nil } if err := fs.WalkDir(e.Files, ".", walker); err != nil { - return &errorhandling.CommandError{ - Err: fmt.Errorf("failed to render templates: %w", err), - ExitCode: errorhandling.ExitTemplateError, - HelpText: "Check template resources and verify the contents.", - } + return fmt.Errorf("failed to render templates: %w", err) } - logger.WithField("fileCount", count).Debug("Completed render") + + if err := g.Wait(); err != nil { + return fmt.Errorf("failed to render templates: %w", err) + } + + logger.WithField("fileCount", count.Load()).Debug("Completed render") return nil } diff --git a/cli/internal/templating/resources/backend/.github/actions/setup-workspace/action.yaml.copy b/cli/internal/templating/resources/backend/.github/actions/setup-workspace/action.yaml.copy new file mode 100644 index 0000000..a3aafee --- /dev/null +++ b/cli/internal/templating/resources/backend/.github/actions/setup-workspace/action.yaml.copy @@ -0,0 +1,19 @@ +--- +name: "Setup Go Workspace" +description: "Configure Go and prepare the workspace" + +runs: + using: composite + steps: + - name: Set up Golang + uses: actions/setup-go@v5 + with: + cache: false + + - name: Install Just + uses: extractions/setup-just@v2 + + - name: Install Go modules + shell: bash + run: | + go mod tidy diff --git a/cli/internal/templating/resources/backend/.github/workflows/compliance.yaml.copy b/cli/internal/templating/resources/backend/.github/workflows/compliance.yaml.copy new file mode 100644 index 0000000..dfa70c4 --- /dev/null +++ b/cli/internal/templating/resources/backend/.github/workflows/compliance.yaml.copy @@ -0,0 +1,36 @@ +--- +name: Compliance Checks + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup workspace + uses: ./.github/actions/setup-workspace + + - name: Install linters + run: | + pip install --upgrade pip + pip install pre-commit==3.5.0 + + - name: Install dependencies + run: | + just tidy + go install golang.org/x/tools/cmd/goimports@latest + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + + - name: Lint + run: pre-commit run --all-files diff --git a/cli/internal/templating/resources/backend/.github/workflows/main.yaml.copy b/cli/internal/templating/resources/backend/.github/workflows/main.yaml.copy new file mode 100644 index 0000000..db6a98e --- /dev/null +++ b/cli/internal/templating/resources/backend/.github/workflows/main.yaml.copy @@ -0,0 +1,41 @@ +--- +name: CICD + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: Run unit tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Run setup + uses: ./.github/actions/setup-workspace + + - name: Run tests + run: | + go clean -testcache + go test -v -cover -race ./... + + build: + name: Build + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Run setup + uses: ./.github/actions/setup-workspace + + - name: Build binary + run: | + go build ./cmd/api diff --git a/cli/internal/templating/resources/backend/.gitignore.copy b/cli/internal/templating/resources/backend/.gitignore.copy new file mode 100644 index 0000000..df9dea0 --- /dev/null +++ b/cli/internal/templating/resources/backend/.gitignore.copy @@ -0,0 +1,18 @@ +# Local development +.vscode +.idea +.DS_Store +node_modules + +# Testing +coverage/ +*.test +*.log +*.out + +# Documentation +site/ + +# Go workspace +go.work +go.work.sum diff --git a/cli/internal/templating/resources/backend/.pre-commit-config.yaml.copy b/cli/internal/templating/resources/backend/.pre-commit-config.yaml.copy new file mode 100644 index 0000000..47114b2 --- /dev/null +++ b/cli/internal/templating/resources/backend/.pre-commit-config.yaml.copy @@ -0,0 +1,42 @@ +--- +fail_fast: false + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: detect-private-key + + # Go standards and linting + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-fmt + - id: go-vet + - id: go-imports + - id: go-cyclo + - id: go-unit-tests + + - repo: https://github.com/codespell-project/codespell.git + rev: v2.4.1 + hooks: + - id: codespell + args: [-w] + files: ^.*\.(md|py|jinja|yaml|yml|sh|feature)$ + + # Security + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + + # Ensure version sync + - repo: https://github.com/zricethezav/gitleaks + rev: v8.30.0 + hooks: + - id: gitleaks diff --git a/cli/internal/templating/resources/backend/README.md.j2 b/cli/internal/templating/resources/backend/README.md.j2 new file mode 100644 index 0000000..06c5d71 --- /dev/null +++ b/cli/internal/templating/resources/backend/README.md.j2 @@ -0,0 +1,35 @@ +# {{ .Name }} + +A Go HTTP service scaffolded with [Hackstack](https://github.com/jgfranco17/hackstack). + +**Author:** {{ .Author }} + +--- + +## Getting started + +```bash +# Install dependencies +go mod tidy + +# Run the service +just start + +# Run tests +just test +``` + +## Configuration + +The service is configured via environment variables (prefix: `APP_`): + +| Variable | Default | Description | +|------------|-------------|--------------| +| `APP_HOST` | `0.0.0.0` | Bind address | +| `APP_PORT` | `8080` | Listen port | + +## Endpoints + +| Method | Path | Description | +|--------|-----------|---------------| +| `GET` | `/health` | Health check | diff --git a/cli/internal/templating/resources/backend/cmd/api/main.go.j2 b/cli/internal/templating/resources/backend/cmd/api/main.go.j2 new file mode 100644 index 0000000..8b29415 --- /dev/null +++ b/cli/internal/templating/resources/backend/cmd/api/main.go.j2 @@ -0,0 +1,24 @@ +package main + +import ( + "os" + + "github.com/sirupsen/logrus" + "github.com/{{ .Username }}/{{ .Name }}/internal/config" + "github.com/{{ .Username }}/{{ .Name }}/internal/logging" + "github.com/{{ .Username }}/{{ .Name }}/internal/service" +) + +func main() { + logger := logging.New(os.Stderr, logrus.InfoLevel) + + cfg, err := config.Load() + if err != nil { + logger.Fatalf("Failed to load config: %v", err) + } + + server := service.New(cfg, logger) + if err := server.Start(); err != nil { + logger.Fatalf("Failed to start service: %v", err) + } +} diff --git a/cli/internal/templating/resources/backend/go.mod.j2 b/cli/internal/templating/resources/backend/go.mod.j2 new file mode 100644 index 0000000..0040917 --- /dev/null +++ b/cli/internal/templating/resources/backend/go.mod.j2 @@ -0,0 +1,28 @@ +module github.com/{{ .Username }}/{{ .Name }} + +go {{ .GoVersion }} + +require ( + github.com/go-chi/chi/v5 v5.2.5 + github.com/sirupsen/logrus v1.9.4 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cli/internal/templating/resources/backend/internal/config/config.go.copy b/cli/internal/templating/resources/backend/internal/config/config.go.copy new file mode 100644 index 0000000..694d7eb --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/config/config.go.copy @@ -0,0 +1,29 @@ +package config + +import ( + "fmt" + + "github.com/spf13/viper" +) + +type ServiceSettings struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` +} + +// Load reads configuration from environment variables (prefix: APP_) +// with defaults of 0.0.0.0:8080. +func Load() (ServiceSettings, error) { + v := viper.New() + v.SetDefault("host", "0.0.0.0") + v.SetDefault("port", 8080) + v.SetEnvPrefix("APP") + v.AutomaticEnv() + + var cfg ServiceSettings + if err := v.Unmarshal(&cfg); err != nil { + return ServiceSettings{}, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return cfg, nil +} diff --git a/cli/internal/templating/resources/backend/internal/config/config_test.go.copy b/cli/internal/templating/resources/backend/internal/config/config_test.go.copy new file mode 100644 index 0000000..460c0d1 --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/config/config_test.go.copy @@ -0,0 +1,37 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoad_Defaults(t *testing.T) { + cfg, err := Load() + + require.NoError(t, err) + assert.Equal(t, "0.0.0.0", cfg.Host) + assert.Equal(t, 8080, cfg.Port) +} + +func TestLoad_EnvOverride(t *testing.T) { + t.Setenv("APP_HOST", "127.0.0.1") + t.Setenv("APP_PORT", "9090") + + cfg, err := Load() + + require.NoError(t, err) + assert.Equal(t, "127.0.0.1", cfg.Host) + assert.Equal(t, 9090, cfg.Port) +} + +func TestLoad_PartialOverride(t *testing.T) { + t.Setenv("APP_PORT", "3000") + + cfg, err := Load() + + require.NoError(t, err) + assert.Equal(t, "0.0.0.0", cfg.Host) + assert.Equal(t, 3000, cfg.Port) +} diff --git a/cli/internal/templating/resources/backend/internal/logging/logging.go.copy b/cli/internal/templating/resources/backend/internal/logging/logging.go.copy new file mode 100644 index 0000000..28ed3e1 --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/logging/logging.go.copy @@ -0,0 +1,39 @@ +package logging + +import ( + "context" + "io" + "time" + + "github.com/sirupsen/logrus" +) + +type contextLogKey string + +const contextKey contextLogKey = "logger" + +func New(stream io.Writer, level logrus.Level) *logrus.Logger { + logger := logrus.New() + logger.SetOutput(stream) + logger.SetLevel(level) + + logger.SetFormatter(&logrus.TextFormatter{ + QuoteEmptyFields: true, + FullTimestamp: true, + DisableSorting: true, + DisableLevelTruncation: true, + TimestampFormat: time.DateTime, + }) + return logger +} + +func AddToContext(ctx context.Context, logger *logrus.Logger) context.Context { + return context.WithValue(ctx, contextKey, logger) +} + +func FromContext(ctx context.Context) *logrus.Logger { + if logger, ok := ctx.Value(contextKey).(*logrus.Logger); ok { + return logger + } + panic("no logger set in context") +} diff --git a/cli/internal/templating/resources/backend/internal/logging/logging_test.go.copy b/cli/internal/templating/resources/backend/internal/logging/logging_test.go.copy new file mode 100644 index 0000000..fddab10 --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/logging/logging_test.go.copy @@ -0,0 +1,24 @@ +package logging + +import ( + "bytes" + "context" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestApplyToContext(t *testing.T) { + var buf bytes.Buffer + logger := New(&buf, logrus.TraceLevel) + ctx := AddToContext(context.Background(), logger) + assert.Equal(t, logger, FromContext(ctx)) +} + +func TestFromContext(t *testing.T) { + var buf bytes.Buffer + logger := New(&buf, logrus.TraceLevel) + ctx := AddToContext(context.Background(), logger) + assert.Equal(t, logger, FromContext(ctx)) +} diff --git a/cli/internal/templating/resources/backend/internal/service/handlers/base.go.j2 b/cli/internal/templating/resources/backend/internal/service/handlers/base.go.j2 new file mode 100644 index 0000000..a60ccf7 --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/service/handlers/base.go.j2 @@ -0,0 +1,25 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/{{ .Username }}/{{ .Name }}/internal/logging" +) + +// GetHealth is a simple health check endpoint that returns a JSON +// response with the status and timestamp. +func GetHealth(w http.ResponseWriter, r *http.Request) { + type healthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + } + + logger := logging.FromContext(r.Context()) + logger.Info("Health check requested") + + writeJSON(w, http.StatusOK, healthResponse{ + Status: "ok", + Timestamp: time.Now().Format(time.DateTime), + }) +} diff --git a/cli/internal/templating/resources/backend/internal/service/handlers/utils.go.copy b/cli/internal/templating/resources/backend/internal/service/handlers/utils.go.copy new file mode 100644 index 0000000..6d5aff2 --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/service/handlers/utils.go.copy @@ -0,0 +1,14 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(v); err != nil { + http.Error(w, "failed to encode response", http.StatusInternalServerError) + } +} diff --git a/cli/internal/templating/resources/backend/internal/service/service.go.j2 b/cli/internal/templating/resources/backend/internal/service/service.go.j2 new file mode 100644 index 0000000..a09e73e --- /dev/null +++ b/cli/internal/templating/resources/backend/internal/service/service.go.j2 @@ -0,0 +1,54 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/sirupsen/logrus" + "github.com/{{ .Username }}/{{ .Name }}/internal/config" + "github.com/{{ .Username }}/{{ .Name }}/internal/logging" + "github.com/{{ .Username }}/{{ .Name }}/internal/service/handlers" +) + +type Service struct { + router *chi.Mux + logger *logrus.Logger + config config.ServiceSettings +} + +func New(cfg config.ServiceSettings, logger *logrus.Logger) *Service { + s := &Service{ + router: chi.NewRouter(), + logger: logger, + config: cfg, + } + s.router.Use(middleware.RequestID) + s.router.Use(middleware.Recoverer) + s.router.Use(s.loggerMiddleware) + s.registerRoutes() + return s +} + +func (s *Service) Start() error { + addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port) + s.logger.WithFields(logrus.Fields{ + "host": s.config.Host, + "port": s.config.Port, + }).Info("Server starting") + return http.ListenAndServe(addr, s.router) +} + +func (s *Service) loggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := logging.AddToContext(r.Context(), s.logger) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (s *Service) registerRoutes() { + s.router.Route("/v1", func(r chi.Router) { + r.Get("/health", handlers.GetHealth) + }) +} diff --git a/cli/internal/templating/resources/backend/justfile.copy b/cli/internal/templating/resources/backend/justfile.copy new file mode 100644 index 0000000..4692b55 --- /dev/null +++ b/cli/internal/templating/resources/backend/justfile.copy @@ -0,0 +1,16 @@ +# Project scripts + +# Default recipe +_default: + @just --list --unsorted + +# Run the service +start: + @go run ./cmd/api + +# Execute tests +test: + #!/usr/bin/env bash + echo "Running tests..." + go clean -testcache + go test -cover ./... diff --git a/go.mod b/go.mod index 27cbd12..81bb705 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/jgfranco17/hackstack go 1.24.2 -toolchain go1.24.3 - require ( github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 @@ -11,6 +9,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 + golang.org/x/sync v0.12.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index d07983b..126c5af 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/justfile b/justfile index 22f46bb..601abce 100644 --- a/justfile +++ b/justfile @@ -18,8 +18,8 @@ hackstack *args: # Run all BDD tests test: @echo "Running unit tests!" - go clean -testcache - go test -cover ./... + @go clean -testcache + go test -cover -race ./... # Build the binary build: