Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cli/command/cmds/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
}
}
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions cli/command/cmds/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions cli/entrypoint/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/errorhandling/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/errorhandling/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
}

Expand Down
14 changes: 13 additions & 1 deletion cli/internal/templating/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
41 changes: 34 additions & 7 deletions cli/internal/templating/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
42 changes: 27 additions & 15 deletions cli/internal/templating/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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)
Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions cli/internal/templating/resources/backend/.gitignore.copy
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading