Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09d7c98069 | |||
| b46a998aac | |||
| dbd513f7b4 | |||
| 7e2b50faf0 | |||
| 9ea29d3ba4 | |||
| bc637b3a77 | |||
| 4b8a092201 |
@ -12,7 +12,7 @@
|
|||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
|
|
||||||
// Run once after the container is created.
|
// Run once after the container is created.
|
||||||
"postCreateCommand": "make init",
|
"postCreateCommand": "make init && make tools",
|
||||||
|
|
||||||
// Fix ownership of the mounted ~/.claude so the vscode user can read host auth.
|
// 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",
|
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude 2>/dev/null || true",
|
||||||
@ -35,6 +35,7 @@
|
|||||||
"github.copilot.inlineSuggest.enable": false,
|
"github.copilot.inlineSuggest.enable": false,
|
||||||
|
|
||||||
"go.useLanguageServer": true,
|
"go.useLanguageServer": true,
|
||||||
|
"go.toolsManagement.autoUpdate": false,
|
||||||
"go.lintTool": "golangci-lint",
|
"go.lintTool": "golangci-lint",
|
||||||
"go.lintFlags": ["--fast"],
|
"go.lintFlags": ["--fast"],
|
||||||
"go.lintOnSave": "workspace",
|
"go.lintOnSave": "workspace",
|
||||||
@ -54,11 +55,12 @@
|
|||||||
"mounts": [
|
"mounts": [
|
||||||
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached"
|
"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.
|
// Forward the default HTTP port so `make run` is reachable from the host.
|
||||||
"forwardPorts": [8080],
|
"forwardPorts": [8080],
|
||||||
|
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
"CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml"
|
"CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml",
|
||||||
|
"GOPRIVATE": "gitea.djmil.dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
// ── Go ─────────────────────────────────────────────────────────────────────
|
// ── Go ─────────────────────────────────────────────────────────────────────
|
||||||
"go.useLanguageServer": true,
|
|
||||||
"go.lintTool": "golangci-lint",
|
"go.lintTool": "golangci-lint",
|
||||||
"go.lintFlags": ["--fast"],
|
"go.lintFlags": ["--fast"],
|
||||||
"go.lintOnSave": "workspace",
|
"go.lintOnSave": "workspace",
|
||||||
"go.testFlags": ["-race"],
|
"go.testFlags": ["-race"],
|
||||||
"go.coverOnSave": false,
|
"go.coverOnSave": false,
|
||||||
"go.generateOnSave": false,
|
|
||||||
|
|
||||||
// ── Editor ─────────────────────────────────────────────────────────────────
|
// ── Editor ─────────────────────────────────────────────────────────────────
|
||||||
"[go]": {
|
"[go]": {
|
||||||
@ -16,9 +14,6 @@
|
|||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": "explicit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"[yaml]": {
|
|
||||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── Files ──────────────────────────────────────────────────────────────────
|
// ── Files ──────────────────────────────────────────────────────────────────
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
@ -27,16 +22,16 @@
|
|||||||
},
|
},
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/bin": true,
|
"**/bin": true,
|
||||||
"**/mocks": true,
|
|
||||||
"**/vendor": true
|
"**/vendor": true
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Test explorer ──────────────────────────────────────────────────────────
|
// ── Test explorer ──────────────────────────────────────────────────────────
|
||||||
"go.testExplorer.enable": true,
|
"go.testExplorer.enable": true,
|
||||||
"makefile.configureOnOpen": false,
|
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"djmil",
|
"djmil",
|
||||||
|
"Expectf",
|
||||||
"gitea",
|
"gitea",
|
||||||
|
"golangci",
|
||||||
"testutil"
|
"testutil"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
7
.vscode/tasks.json
vendored
7
.vscode/tasks.json
vendored
@ -41,13 +41,6 @@
|
|||||||
"presentation": { "reveal": "always", "panel": "shared" },
|
"presentation": { "reveal": "always", "panel": "shared" },
|
||||||
"problemMatcher": "$go"
|
"problemMatcher": "$go"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "mocks",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "make mocks",
|
|
||||||
"group": "none",
|
|
||||||
"presentation": { "reveal": "always", "panel": "shared" }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "security scan",
|
"label": "security scan",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
@ -39,7 +39,13 @@ tools.versions Pinned tool versions (sourced by Makefile and pre-push
|
|||||||
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...`
|
- **Module imports** — always use the full module path `gitea.djmil.dev/go/template/...`
|
||||||
- **Packages** — keep `cmd/` thin (wiring only); business logic belongs in `internal/`
|
- **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)
|
- **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** — wrap with `fmt.Errorf("context: %w", err)`; never swallow errors silently
|
- **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`
|
||||||
- **Logging** — use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages; `log/slog` is the backend
|
- **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
|
- **Config** — all configuration through `internal/config` (flag-parsed); no hard-coded values in logic packages
|
||||||
|
|
||||||
|
|||||||
8
Makefile
8
Makefile
@ -1,4 +1,4 @@
|
|||||||
.PHONY: help init setup build run test test-race lint lint-fix security clean
|
.PHONY: help init setup build run test test-race lint lint-fix security docs clean
|
||||||
|
|
||||||
include tools.versions
|
include tools.versions
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ help: ## Show this help message
|
|||||||
|
|
||||||
# ── First-time setup ───────────────────────────────────────────────────────────
|
# ── First-time setup ───────────────────────────────────────────────────────────
|
||||||
init: ## First-time project init: fetch deps, configure git hooks
|
init: ## First-time project init: fetch deps, configure git hooks
|
||||||
|
go env -w GOPRIVATE=gitea.djmil.dev
|
||||||
go mod tidy
|
go mod tidy
|
||||||
$(MAKE) setup
|
$(MAKE) setup
|
||||||
@echo "Done! Run 'make build' to verify."
|
@echo "Done! Run 'make build' to verify."
|
||||||
@ -30,6 +31,7 @@ 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/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||||
go install github.com/securego/gosec/v2/cmd/gosec@$(GOSEC_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/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)
|
||||||
|
go install golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION)
|
||||||
|
|
||||||
# ── Build ──────────────────────────────────────────────────────────────────────
|
# ── Build ──────────────────────────────────────────────────────────────────────
|
||||||
build: ## Compile the binary to ./bin/
|
build: ## Compile the binary to ./bin/
|
||||||
@ -64,6 +66,10 @@ security: ## Run gosec + govulncheck
|
|||||||
@echo "--- govulncheck ---"
|
@echo "--- govulncheck ---"
|
||||||
go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./...
|
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 ────────────────────────────────────────────────────────────────────
|
||||||
release: ## List releases, or tag+push a new one (usage: make release VERSION=v0.1.0)
|
release: ## List releases, or tag+push a new one (usage: make release VERSION=v0.1.0)
|
||||||
ifdef VERSION
|
ifdef VERSION
|
||||||
|
|||||||
@ -100,6 +100,31 @@ func Example_nonErrorPanic() {
|
|||||||
// non-error panic: unexpected runtime problem
|
// 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
|
// Example_fail shows constructing a failed Expect explicitly, e.g. when a
|
||||||
// function detects an error condition before calling any fallible op.
|
// function detects an error condition before calling any fallible op.
|
||||||
func Example_fail() {
|
func Example_fail() {
|
||||||
|
|||||||
@ -59,6 +59,20 @@ func (r Expect[T]) Expect(msg string) T {
|
|||||||
return r.value
|
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.
|
// 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.
|
// Useful at the boundary where you want to re-join normal error-return code.
|
||||||
func (r Expect[T]) Unwrap() (T, error) {
|
func (r Expect[T]) Unwrap() (T, error) {
|
||||||
|
|||||||
23
rename.sh
23
rename.sh
@ -126,11 +126,24 @@ sedi() {
|
|||||||
fi
|
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 ───────────────────────────────────────────────────────
|
# ── Apply substitutions ───────────────────────────────────────────────────────
|
||||||
heading "Applying changes"
|
heading "Applying changes"
|
||||||
|
|
||||||
# 1. go.mod — module declaration
|
# 1. go.mod — module declaration
|
||||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" go.mod
|
rename_module_in go.mod
|
||||||
info "go.mod"
|
info "go.mod"
|
||||||
|
|
||||||
# 2. All Go source files — import paths
|
# 2. All Go source files — import paths
|
||||||
@ -143,7 +156,7 @@ GO_FILES=$(find . \
|
|||||||
CHANGED_GO=0
|
CHANGED_GO=0
|
||||||
for f in $GO_FILES; do
|
for f in $GO_FILES; do
|
||||||
if grep -q "$OLD_MODULE" "$f" 2>/dev/null; then
|
if grep -q "$OLD_MODULE" "$f" 2>/dev/null; then
|
||||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" "$f"
|
rename_module_in "$f"
|
||||||
CHANGED_GO=$((CHANGED_GO + 1))
|
CHANGED_GO=$((CHANGED_GO + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@ -159,19 +172,19 @@ fi
|
|||||||
# 4. README.md — heading + all module path occurrences
|
# 4. README.md — heading + all module path occurrences
|
||||||
if [[ -f README.md ]]; then
|
if [[ -f README.md ]]; then
|
||||||
sedi "s|^# ${OLD_PROJECT}$|# ${NEW_PROJECT}|g" README.md
|
sedi "s|^# ${OLD_PROJECT}$|# ${NEW_PROJECT}|g" README.md
|
||||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" README.md
|
rename_module_in README.md
|
||||||
info "README.md"
|
info "README.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. CLAUDE.md — Module line
|
# 5. CLAUDE.md — Module line
|
||||||
if [[ -f CLAUDE.md ]]; then
|
if [[ -f CLAUDE.md ]]; then
|
||||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" CLAUDE.md
|
rename_module_in CLAUDE.md
|
||||||
info "CLAUDE.md"
|
info "CLAUDE.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6. .golangci.yml — goimports local-prefixes
|
# 6. .golangci.yml — goimports local-prefixes
|
||||||
if [[ -f .golangci.yml ]]; then
|
if [[ -f .golangci.yml ]]; then
|
||||||
sedi "s|${OLD_MODULE}|${NEW_MODULE}|g" .golangci.yml
|
rename_module_in .golangci.yml
|
||||||
info ".golangci.yml"
|
info ".golangci.yml"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -2,3 +2,4 @@ DELVE_VERSION=v1.26.1
|
|||||||
GOLANGCI_LINT_VERSION=v1.64.8
|
GOLANGCI_LINT_VERSION=v1.64.8
|
||||||
GOSEC_VERSION=v2.24.7
|
GOSEC_VERSION=v2.24.7
|
||||||
GOVULNCHECK_VERSION=v1.1.4
|
GOVULNCHECK_VERSION=v1.1.4
|
||||||
|
PKGSITE_VERSION=latest
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user