Skip to content

Commit 59351d7

Browse files
authored
refactor: improve error handling, and context propagation (#100)
* refactor(errors): rename errors package to apperrors * refactor: standardize error wrapping with apperrors * refactor(generator): add context propagation to action handlers * refactor(git): improve error handling and function naming * style: fix lint warnings and naming conventions * build: update release config, deps, and tooling * test: allow revive lint exception for package name in tests
1 parent b7212fc commit 59351d7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+584
-404
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ jobs:
5454
version: latest
5555
args: release --clean
5656
env:
57-
GITHUB_TOKEN: ${{ secrets.TEMPO_TOKEN }}
57+
GITHUB_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}

.golangci.yaml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
version: '2'
2+
run:
3+
tests: true
4+
timeout: 5m
5+
linters:
6+
enable:
7+
- asasalint
8+
- bodyclose
9+
- errcheck
10+
- errorlint
11+
- gocritic
12+
- gosec
13+
- govet
14+
- ineffassign
15+
- noctx
16+
- prealloc
17+
- revive
18+
- staticcheck
19+
- unconvert
20+
- unused
21+
settings:
22+
gocritic:
23+
enabled-tags:
24+
- diagnostic
25+
- performance
26+
disabled-checks:
27+
- hugeParam
28+
- filepathJoin
29+
- commentedOutCode
30+
gosec:
31+
excludes:
32+
- G304
33+
revive:
34+
severity: warning
35+
rules:
36+
- name: dot-imports
37+
severity: error
38+
- name: var-naming
39+
- name: indent-error-flow
40+
- name: error-return
41+
- name: unexported-return
42+
exclusions:
43+
generated: lax
44+
presets:
45+
- comments
46+
- common-false-positives
47+
- legacy
48+
- std-error-handling
49+
rules:
50+
- path: _test\.go
51+
text: should have comment
52+
# Test files: allow exec.Command without context
53+
- path: _test\.go
54+
linters:
55+
- noctx
56+
# Test files: allow less strict file permissions
57+
- path: _test\.go
58+
linters:
59+
- gosec
60+
text: "G306"
61+
# Test files: allow subprocess with variables (for mocking)
62+
- path: _test\.go
63+
linters:
64+
- gosec
65+
text: "G204"
66+
# Test files: allow error pointer comparison for Unwrap tests
67+
- path: _test\.go
68+
linters:
69+
- errorlint
70+
text: "comparing with != will fail"
71+
# Test files: string to byte comparison is fine
72+
- path: _test\.go
73+
linters:
74+
- gocritic
75+
text: "stringXbytes"
76+
# Prealloc is often too noisy for small slices
77+
- path: \.go$
78+
linters:
79+
- prealloc
80+
paths:
81+
- third_party$
82+
- builtin$
83+
- examples$
84+
formatters:
85+
enable:
86+
- gofmt
87+
- goimports
88+
exclusions:
89+
generated: lax
90+
paths:
91+
- third_party$
92+
- builtin$
93+
- examples$
94+
output:
95+
formats:
96+
text:
97+
path: stdout
98+
colors: true

.goreleaser.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ builds:
1010
binary: tempo
1111
env:
1212
- CGO_ENABLED=0
13+
flags:
14+
- -trimpath
15+
ldflags:
16+
- -s -w
1317
goos:
1418
- linux
1519
- windows
@@ -40,6 +44,19 @@ changelog:
4044
- '^docs:'
4145
- '^test:'
4246

47+
brews:
48+
- repository:
49+
owner: indaco
50+
name: homebrew-tap
51+
directory: Formula
52+
homepage: https://github.com/indaco/tempo
53+
description: A CLI tool for managing assets and scaffolding components in [templ](https://templ.guide)-based projects
54+
license: MIT
55+
install: |
56+
bin.install "tempo"
57+
test: |
58+
system "#{bin}/tempo", "--version"
59+
4360
release:
4461
footer: >-
4562

.sley.yaml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,8 @@ plugins:
1717
# Generates changelogs from git commits
1818
changelog-generator:
1919
enabled: true
20-
mode: "versioned"
21-
changes-dir: ".changes"
22-
use-default-icons: true
2320
repository:
2421
auto-detect: true
25-
exclude-patterns:
26-
- "^Merge"
27-
- "^WIP"
28-
- "^release"
22+
use-default-icons: true
2923
contributors:
3024
enabled: true
31-

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ just help
7474
| `just test` | Run all tests and generate coverage report |
7575
| `just test-force` | Clean go tests cache and run all tests |
7676
| `just modernize` | Run go-modernize with auto-fix |
77+
| `reportcard` | Run goreportcard-cli |
78+
| `security-scan` | Run govulncheck |
7779
| `just check` | Run modernize, lint, and test |
7880
| `just lint` | Run golangci-lint |
7981
| `just build` | Build the binary to build/tempo |

cmd/tempo/componentcmd/define.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"path/filepath"
66

77
"github.com/indaco/tempo/internal/app"
8+
"github.com/indaco/tempo/internal/apperrors"
89
"github.com/indaco/tempo/internal/config"
9-
"github.com/indaco/tempo/internal/errors"
1010
"github.com/indaco/tempo/internal/generator"
1111
"github.com/indaco/tempo/internal/helpers"
1212
"github.com/indaco/tempo/internal/resolver"
@@ -64,7 +64,7 @@ func runComponentDefineSubCommand(cmdCtx *app.AppContext) func(ctx context.Conte
6464
// Step 1: Create template data
6565
data, err := createTemplateData(cmd, cmdCtx.Config)
6666
if err != nil {
67-
return errors.Wrap("Failed to create template data for component", err)
67+
return apperrors.Wrap("Failed to create template data for component", err)
6868
}
6969

7070
if data.DryRun {
@@ -86,14 +86,14 @@ func runComponentDefineSubCommand(cmdCtx *app.AppContext) func(ctx context.Conte
8686
}
8787

8888
// Step 3: Retrieve component actions
89-
builtInActions, err := generator.BuildComponentActions(generator.CopyActionId, data.Force, data.WithJs)
89+
builtInActions, err := generator.BuildComponentActions(generator.CopyActionID, data.Force, data.WithJs)
9090
if err != nil {
91-
return errors.Wrap("Failed to build component actions", err)
91+
return apperrors.Wrap("Failed to build component actions", err)
9292
}
9393

9494
// Step 4: Process actions
95-
if err := generator.ProcessActions(cmdCtx.Logger, builtInActions, data); err != nil {
96-
return errors.Wrap("Failed to process actions for component", err)
95+
if err := generator.ProcessActions(ctx, cmdCtx.Logger, builtInActions, data); err != nil {
96+
return apperrors.Wrap("Failed to process actions for component", err)
9797
}
9898

9999
if !data.DryRun {

cmd/tempo/componentcmd/new.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"path/filepath"
66

77
"github.com/indaco/tempo/internal/app"
8+
"github.com/indaco/tempo/internal/apperrors"
89
"github.com/indaco/tempo/internal/config"
9-
"github.com/indaco/tempo/internal/errors"
1010
"github.com/indaco/tempo/internal/generator"
1111
"github.com/indaco/tempo/internal/helpers"
1212
"github.com/indaco/tempo/internal/resolver"
@@ -81,7 +81,7 @@ func runComponentNewSubCommand(cmdCtx *app.AppContext) func(ctx context.Context,
8181
// Step 1: Create template data
8282
data, err := createComponentData(cmd, cmdCtx.Config)
8383
if err != nil {
84-
return errors.Wrap("Failed to create template data for component", err)
84+
return apperrors.Wrap("Failed to create template data for component", err)
8585
}
8686

8787
if data.DryRun {
@@ -97,7 +97,7 @@ func runComponentNewSubCommand(cmdCtx *app.AppContext) func(ctx context.Context,
9797
return err
9898
}
9999
if !exists {
100-
return errors.Wrap("Cannot find actions folder. Did you run 'tempo component define' before?")
100+
return apperrors.Wrap("Cannot find actions folder. Did you run 'tempo component define' before?")
101101
}
102102

103103
// Step 3: Check if the component already exists
@@ -114,8 +114,8 @@ func runComponentNewSubCommand(cmdCtx *app.AppContext) func(ctx context.Context,
114114
}
115115

116116
// Step 4: Retrieve and process actions
117-
if err := generator.ProcessEntityActions(cmdCtx.Logger, pathToComponentActionsFile, data, cmdCtx.Config); err != nil {
118-
return errors.Wrap("failed to process actions for component", err, data.ComponentName)
117+
if err := generator.ProcessEntityActions(ctx, cmdCtx.Logger, pathToComponentActionsFile, data, cmdCtx.Config); err != nil {
118+
return apperrors.Wrap("failed to process actions for component", err, data.ComponentName)
119119
}
120120

121121
// Step 5: Log success and asset information

cmd/tempo/initcmd/init.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
"strings"
1111

1212
"github.com/indaco/tempo/internal/app"
13+
"github.com/indaco/tempo/internal/apperrors"
1314
"github.com/indaco/tempo/internal/config"
14-
"github.com/indaco/tempo/internal/errors"
1515
"github.com/indaco/tempo/internal/helpers"
1616
"github.com/indaco/tempo/internal/utils"
1717
"github.com/urfave/cli/v3"
@@ -75,10 +75,10 @@ func runInitCommand(cmdCtx *app.AppContext) func(ctx context.Context, cmd *cli.C
7575
cmdCtx.Logger.Info("Generating", tempoConfigPath)
7676
cfg, err := prepareConfig(cmdCtx.CWD, tempoRoot, templatesDir, actionsDir)
7777
if err != nil {
78-
return errors.Wrap("Failed to prepare the configuration file", err)
78+
return apperrors.Wrap("Failed to prepare the configuration file", err)
7979
}
8080
if err := writeConfigFile(tempoConfigPath, cfg); err != nil {
81-
return errors.Wrap("Failed to write the configuration file", err, tempoConfigPath)
81+
return apperrors.Wrap("Failed to write the configuration file", err, tempoConfigPath)
8282
}
8383

8484
// Step 5: Log the successful initialization
@@ -100,25 +100,25 @@ func runInitCommand(cmdCtx *app.AppContext) func(ctx context.Context, cmd *cli.C
100100
func validateInitPrerequisites(workingDir, configFilePath string) error {
101101
goModPath := filepath.Join(workingDir, "go.mod")
102102
if _, err := os.Stat(goModPath); os.IsNotExist(err) {
103-
return errors.Wrap("missing go.mod file. Run 'go mod init' to create one")
103+
return apperrors.Wrap("missing go.mod file. Run 'go mod init' to create one")
104104
} else if err != nil {
105-
return errors.Wrap("error checking go.mod file", err)
105+
return apperrors.Wrap("error checking go.mod file", err)
106106
}
107107

108108
exists, err := utils.FileExistsFunc(configFilePath)
109109
if err != nil {
110-
return errors.Wrap("Error checking configuration file", err)
110+
return apperrors.Wrap("Error checking configuration file", err)
111111
}
112112
if exists {
113113
// Check if the file is writable
114114
file, err := os.OpenFile(configFilePath, os.O_WRONLY, 0644)
115115
if err != nil {
116-
return errors.Wrap("Failed to write the configuration file", err)
116+
return apperrors.Wrap("Failed to write the configuration file", err)
117117
}
118118
if err := file.Close(); err != nil {
119-
return errors.Wrap("Failed to close the configuration file", err)
119+
return apperrors.Wrap("Failed to close the configuration file", err)
120120
}
121-
return errors.Wrap("Configuration file already exists", configFilePath)
121+
return apperrors.Wrap("Configuration file already exists", configFilePath)
122122
}
123123
return nil
124124
}
@@ -163,32 +163,32 @@ func writeConfigFile(filePath string, cfg *config.Config) error {
163163
sb.WriteString("# Tempo CLI configuration file\n")
164164
sb.WriteString("# Documentation & source code: https://github.com/indaco/tempo\n\n")
165165
sb.WriteString("# The root folder for tempo files\n")
166-
sb.WriteString(fmt.Sprintf("tempo_root: %s\n\n", cfg.TempoRoot))
166+
fmt.Fprintf(&sb, "tempo_root: %s\n\n", cfg.TempoRoot)
167167

168168
// Write app-specific configuration
169169
sb.WriteString("app:\n")
170170
sb.WriteString(" # The name of the Go module being worked on.\n")
171-
sb.WriteString(fmt.Sprintf(" go_module: %s\n\n", cfg.App.GoModule))
171+
fmt.Fprintf(&sb, " go_module: %s\n\n", cfg.App.GoModule)
172172
sb.WriteString(" # The Go package name where components will be organized and generated.\n")
173-
sb.WriteString(fmt.Sprintf(" go_package: %s\n\n", cfg.App.GoPackage))
173+
fmt.Fprintf(&sb, " go_package: %s\n\n", cfg.App.GoPackage)
174174
sb.WriteString(" # The directory where asset files (CSS, JS) will be generated.\n")
175-
sb.WriteString(fmt.Sprintf(" assets_dir: %s\n\n", cfg.App.AssetsDir))
175+
fmt.Fprintf(&sb, " assets_dir: %s\n\n", cfg.App.AssetsDir)
176176
sb.WriteString(" # Indicates whether JavaScript is required for the component.\n")
177-
sb.WriteString(fmt.Sprintf(" # with_js: %s\n\n", strconv.FormatBool(cfg.App.WithJs)))
177+
fmt.Fprintf(&sb, " # with_js: %s\n\n", strconv.FormatBool(cfg.App.WithJs))
178178
sb.WriteString(" # The name of the CSS layer to associate with component styles.\n")
179-
sb.WriteString(fmt.Sprintf(" # css_layer: %s\n\n", cfg.App.CssLayer))
179+
fmt.Fprintf(&sb, " # css_layer: %s\n\n", cfg.App.CssLayer)
180180

181181
// Write processor configuration
182182
sb.WriteString("# processor:\n")
183183
sb.WriteString(" # Number of concurrent workers (numCPUs * 2).\n")
184-
sb.WriteString(fmt.Sprintf(" # workers: %d\n\n", cfg.Processor.Workers))
184+
fmt.Fprintf(&sb, " # workers: %d\n\n", cfg.Processor.Workers)
185185
sb.WriteString(" # Summary format: compact, long, json, none.\n")
186-
sb.WriteString(fmt.Sprintf(" # summary_format: %s\n\n", cfg.Processor.SummaryFormat))
186+
fmt.Fprintf(&sb, " # summary_format: %s\n\n", cfg.Processor.SummaryFormat)
187187

188188
// Write templates configuration
189189
sb.WriteString("# templates:\n")
190190
sb.WriteString(" # A placeholder in template files indicating auto-generated sections.\n")
191-
sb.WriteString(fmt.Sprintf(" # guard_marker: %s\n\n", cfg.Templates.GuardMarker))
191+
fmt.Fprintf(&sb, " # guard_marker: %s\n\n", cfg.Templates.GuardMarker)
192192
sb.WriteString(" # File extensions used for template files.\n")
193193
sb.WriteString(" # extensions:\n")
194194

@@ -198,7 +198,7 @@ func writeConfigFile(filePath string, cfg *config.Config) error {
198198
}
199199

200200
for _, ext := range extensions {
201-
sb.WriteString(fmt.Sprintf(" # - %s\n", ext))
201+
fmt.Fprintf(&sb, " # - %s\n", ext)
202202
}
203203

204204
// Add user data section

cmd/tempo/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"github.com/indaco/tempo/cmd/tempo/synccmd"
1313
"github.com/indaco/tempo/cmd/tempo/variantcmd"
1414
"github.com/indaco/tempo/internal/app"
15+
apperrors "github.com/indaco/tempo/internal/apperrors"
1516
"github.com/indaco/tempo/internal/config"
16-
"github.com/indaco/tempo/internal/errors"
1717
"github.com/indaco/tempo/internal/logger"
1818
"github.com/indaco/tempo/internal/utils"
1919
"github.com/indaco/tempo/internal/version"
@@ -31,7 +31,7 @@ const (
3131
// main is the CLI application's entry point.
3232
func main() {
3333
if err := runCLI(os.Args); err != nil {
34-
errors.LogErrorChain(err)
34+
apperrors.LogErrorChain(err)
3535
log.Fatal(err)
3636
}
3737
}
@@ -44,7 +44,7 @@ func runCLI(args []string) error {
4444
// Load configuration.
4545
cfg, err := config.LoadConfig()
4646
if err != nil {
47-
return fmt.Errorf("error loading config: %w", err)
47+
return apperrors.Wrap("error loading config", err)
4848
}
4949

5050
// Initialize CLI context.

cmd/tempo/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func TestRunCLI_LoadConfigError(t *testing.T) {
212212
if err == nil {
213213
t.Fatal("expected error from LoadConfig, got nil")
214214
}
215-
if !utils.ContainsSubstring(err.Error(), "failed to read config file:") {
215+
if !utils.ErrorContains(err, "failed to read config file:") {
216216
t.Errorf("unexpected error: %v", err)
217217
}
218218
}

0 commit comments

Comments
 (0)