Compare commits

..

No commits in common. "main" and "v0.1.1" have entirely different histories.
main ... v0.1.1

9 changed files with 24 additions and 79 deletions

View File

@ -12,7 +12,7 @@
"remoteUser": "vscode",
// Run once after the container is created.
"postCreateCommand": "make init && make tools",
"postCreateCommand": "make init",
// Fix ownership of the mounted ~/.claude so the vscode user can read host auth.
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude 2>/dev/null || true",
@ -35,7 +35,6 @@
"github.copilot.inlineSuggest.enable": false,
"go.useLanguageServer": true,
"go.toolsManagement.autoUpdate": false,
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
"go.lintOnSave": "workspace",
@ -55,12 +54,11 @@
"mounts": [
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached"
],
// Forward the default HTTP port so `make run` is reachable from the host.
"forwardPorts": [8080],
"remoteEnv": {
"CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml",
"GOPRIVATE": "gitea.djmil.dev"
"CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml"
}
}

View File

@ -1,10 +1,12 @@
{
// Go
"go.useLanguageServer": true,
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
"go.lintOnSave": "workspace",
"go.testFlags": ["-race"],
"go.coverOnSave": false,
"go.generateOnSave": false,
// Editor
"[go]": {
@ -14,6 +16,9 @@
"source.organizeImports": "explicit"
}
},
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml"
},
// Files
"files.exclude": {
@ -22,16 +27,16 @@
},
"search.exclude": {
"**/bin": true,
"**/mocks": true,
"**/vendor": true
},
// Test explorer
"go.testExplorer.enable": true,
"makefile.configureOnOpen": false,
"cSpell.words": [
"djmil",
"Expectf",
"gitea",
"golangci",
"testutil"
]
}

7
.vscode/tasks.json vendored
View File

@ -41,6 +41,13 @@
"presentation": { "reveal": "always", "panel": "shared" },
"problemMatcher": "$go"
},
{
"label": "mocks",
"type": "shell",
"command": "make mocks",
"group": "none",
"presentation": { "reveal": "always", "panel": "shared" }
},
{
"label": "security scan",
"type": "shell",

View File

@ -39,13 +39,7 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...`
- **Packages** — keep `cmd/` thin (wiring only); business logic belongs in `internal/`
- **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
- **Errors**`pkg/result` is the default error-handling mechanism for all code in this repo, including public APIs:
- functions return `result.Expect[T]` instead of `(T, error)`
- callers unwrap with `.Expect("context")` (panics with annotated error + stack trace) or `.Must()` (panics with raw error)
- top-level entry points (e.g. `cmd/` functions, HTTP handlers) defer `result.Catch(&err)` to convert any result panic into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
- bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")`
- use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error
- still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Fail`
- **Errors** — wrap with `fmt.Errorf("context: %w", err)`; never swallow errors silently
- **Logging** — use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages; `log/slog` is the backend
- **Config** — all configuration through `internal/config` (flag-parsed); no hard-coded values in logic packages

View File

@ -1,4 +1,4 @@
.PHONY: help init setup build run test test-race lint lint-fix security docs clean
.PHONY: help init setup build run test test-race lint lint-fix security clean
include tools.versions
@ -14,7 +14,6 @@ help: ## Show this help message
# ── First-time setup ───────────────────────────────────────────────────────────
init: ## First-time project init: fetch deps, configure git hooks
go env -w GOPRIVATE=gitea.djmil.dev
go mod tidy
$(MAKE) setup
@echo "Done! Run 'make build' to verify."
@ -31,7 +30,6 @@ tools: ## Install tool binaries to GOPATH/bin (versions from tools.versions)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
go install github.com/securego/gosec/v2/cmd/gosec@$(GOSEC_VERSION)
go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)
go install golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION)
# ── Build ──────────────────────────────────────────────────────────────────────
build: ## Compile the binary to ./bin/
@ -66,10 +64,6 @@ security: ## Run gosec + govulncheck
@echo "--- govulncheck ---"
go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./...
# ── Docs ───────────────────────────────────────────────────────────────────────
docs: ## Serve package documentation locally via pkgsite (http://localhost:8080)
go run golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION) -open .
# ── Release ────────────────────────────────────────────────────────────────────
release: ## List releases, or tag+push a new one (usage: make release VERSION=v0.1.0)
ifdef VERSION

View File

@ -100,31 +100,6 @@ func Example_nonErrorPanic() {
// non-error panic: unexpected runtime problem
}
// Example_expectf shows Expectf for context messages that include runtime
// values — equivalent to Expect(fmt.Sprintf(...)) but more concise.
func Example_expectf() {
port := parsePort("3000").Expectf("read port from arg %d", 1)
fmt.Println(port)
// Output:
// 3000
}
// Example_expectfError shows that Expectf annotates the error message with the
// formatted context, just like Expect does.
func Example_expectfError() {
run := func() (err error) {
defer result.Catch(&err)
_ = parsePort("99999").Expectf("arg %d port value", 2)
return nil
}
if err := run(); err != nil {
fmt.Println("caught:", err)
}
// Output:
// caught: arg 2 port value: parsePort: 99999 out of range
}
// Example_fail shows constructing a failed Expect explicitly, e.g. when a
// function detects an error condition before calling any fallible op.
func Example_fail() {

View File

@ -59,20 +59,6 @@ func (r Expect[T]) Expect(msg string) T {
return r.value
}
// Expectf is like [Expect.Expect] but accepts a fmt.Sprintf-style format string
// for the context message. The wrapped error is always appended as ": <err>".
//
// data := Parse(raw).Expectf("parse user input id=%d", id)
func (r Expect[T]) Expectf(format string, args ...any) T {
if r.err != nil {
panic(&stackError{
err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err),
stack: debug.Stack(),
})
}
return r.value
}
// Unwrap returns the value and error in the standard Go (value, error) form.
// Useful at the boundary where you want to re-join normal error-return code.
func (r Expect[T]) Unwrap() (T, error) {

View File

@ -126,24 +126,11 @@ sedi() {
fi
}
# ── Helper: rename module path in a file, preserving pkg/result imports ───────
# pkg/result is a standalone publishable package; its import path must not
# change when the consuming project is renamed.
RESULT_PKG="${OLD_MODULE}/pkg/result"
PLACEHOLDER="__RESULT_PKG_PLACEHOLDER__"
rename_module_in() {
local file="$1"
sedi "s|${RESULT_PKG}|${PLACEHOLDER}|g" "$file"
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$file"
sedi "s|${PLACEHOLDER}|${RESULT_PKG}|g" "$file"
}
# ── Apply substitutions ───────────────────────────────────────────────────────
heading "Applying changes"
# 1. go.mod — module declaration
rename_module_in go.mod
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" go.mod
info "go.mod"
# 2. All Go source files — import paths
@ -156,7 +143,7 @@ GO_FILES=$(find . \
CHANGED_GO=0
for f in $GO_FILES; do
if grep -q "$OLD_MODULE" "$f" 2>/dev/null; then
rename_module_in "$f"
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$f"
CHANGED_GO=$((CHANGED_GO + 1))
fi
done
@ -172,19 +159,19 @@ fi
# 4. README.md — heading + all module path occurrences
if [[ -f README.md ]]; then
sedi "s|^# ${OLD_PROJECT}$|# ${NEW_PROJECT}|g" README.md
rename_module_in README.md
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" README.md
info "README.md"
fi
# 5. CLAUDE.md — Module line
if [[ -f CLAUDE.md ]]; then
rename_module_in CLAUDE.md
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" CLAUDE.md
info "CLAUDE.md"
fi
# 6. .golangci.yml — goimports local-prefixes
if [[ -f .golangci.yml ]]; then
rename_module_in .golangci.yml
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" .golangci.yml
info ".golangci.yml"
fi

View File

@ -2,4 +2,3 @@ DELVE_VERSION=v1.26.1
GOLANGCI_LINT_VERSION=v1.64.8
GOSEC_VERSION=v2.24.7
GOVULNCHECK_VERSION=v1.1.4
PKGSITE_VERSION=latest