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
63 changes: 63 additions & 0 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
name: verify
description: Use when the user runs /verify or asks to run make verify. Runs the full verification suite (tests, lint, coverage, benchmarks, license) and fixes every issue found.
---

# verify

Run `make verify` and fix all issues until it passes clean.

`make verify` runs in order: `test` → `ui-test` → `license-check` → `lint` → `benchmark` → `coverage`

## How to run

```bash
make verify
```

Read ALL output carefully. Don't stop at the first failure — run through to the end to collect all issues, then fix them together.

## Fixing issues — The Iron Law

**Fix the code. Never silence the tool.**

| Forbidden | Why |
|-----------|-----|
| Adding `//nolint:...` directives | Hides the problem, ships broken code |
| Removing or skipping tests | Destroys the safety net |
| Lowering the coverage threshold | Treats the symptom |
| Commenting out failing assertions | Same as deleting the test |
| `//nolint` without a real reason | `nolintlint` requires specific linter + explanation anyway |

The only valid `//nolint` is when the linter is provably wrong for that exact line and you include a clear explanation. This should be rare.

## Linter quick reference

Config: `.golangci.yaml` — standard linters + `nolintlint`, `gocyclo` (≥20), `nestif` (≥5), `gosec`, `dupl`

| Linter | Common fix |
|--------|-----------|
| `errcheck` | Handle or explicitly discard the error: `_ = f()` only if truly safe |
| `staticcheck` | Follow the message — usually dead code, deprecated API, or impossible condition |
| `unused` | Delete the unused symbol, don't keep it for "future use" |
| `govet` | Fix the suspicious construct (printf verbs, mutex copies, etc.) |
| `ineffassign` | Remove the assignment or actually use the value |
| `gocyclo` / `nestif` | Refactor: extract helper functions, invert conditions, reduce nesting |
| `gosec` | Fix the security issue (weak random, unhandled error on Close, etc.) |
| `dupl` | Extract the duplicated block into a shared function |
| `nolintlint` | Remove invalid nolint or add specific linter name + explanation |

## Coverage

Threshold: **70%** for `./internal/...` and `./libs/...`

If coverage drops below 70%: write the missing tests. Do not lower the threshold.

## Step-by-step

1. Run `make verify`, capture full output
2. Group failures by type (test failures, lint issues, coverage gaps)
3. Fix all test failures first (they may affect coverage numbers)
4. Fix all lint issues by refactoring code
5. Add missing tests if coverage is below threshold
6. Run `make verify` again — repeat until it passes with zero errors
30 changes: 30 additions & 0 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: golangci-lint

on:
push:
branches: [main, master]
pull_request:

permissions:
contents: read

jobs:
golangci:
name: lint
runs-on: ubuntu-latest
env:
CGO_ENABLED: 1
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.5
29 changes: 29 additions & 0 deletions .github/workflows/license-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: License check
on:
push:
branches:
- main
- master
pull_request:

jobs:
license-check:
runs-on: ubuntu-latest
env:
CGO_ENABLED: 1
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'

- name: Install go-licence-detector
run: |
go install go.elastic.co/go-licence-detector@v0.10.0

- name: License check
run: make license-check
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: golang
on: [push]
jobs:
test:
runs-on: ubuntu-latest
env:
CGO_ENABLED: 1
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'

- name: run tests
run: make test

- name: ensure test coverage
run: make coverage
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Build artifacts
*.o
*.a
*.so
*.dylib

# IDE
.idea/
.vscode/
*.swp

# Coverage
cover.html
cover.out
coverage.out

# Example output
examples/rich/output.png
40 changes: 40 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version: "2"
linters:
default: standard # errcheck,govet,ineffassign,staticcheck,unused
enable:
- nolintlint
- gocyclo
- nestif
- gosec
- dupl

# exclude test files
exclusions:
rules:
- path: '(.+)_test\.go'
linters:
- noop
- nestif
- dupl

settings:
nolintlint:
# Exclude following linters from requiring an explanation.
allow-no-explanation: []
# Enable to require an explanation of nonzero length after each nolint directive.
require-explanation: true
# Enable to require nolint directives to mention the specific linter being suppressed.
require-specific: true
gocyclo:
min-complexity: 20 # Default: 30 (but we recommend 10-20)
gocognit:
min-complexity: 20 # Default: 30 (but we recommend 10-20)
nestif:
min-complexity: 5 # Default: 5


output:
sort-order:
- file
- severity
- linter
84 changes: 84 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
COMMIT_SHA_SHORT ?= $(shell git rev-parse --short=12 HEAD)
PWD_DIR := ${CURDIR}

default: help

#==========================================================================================
##@ Testing
#==========================================================================================
test: ## run go tests
@go test ./...

lint: ## run go linter
@# depends on https://github.com/golangci/golangci-lint
@golangci-lint run

license-check: ## check for invalid licenses
@# depends on : https://github.com/elastic/go-licence-detector
@go list -m -mod=readonly -json all | go-licence-detector -includeIndirect -validate -rules allowedLicenses.json

benchmark: ## run go benchmarks
@go test -run=^$$ -bench=. ./...

.PHONY: verify
verify: lint test license-check benchmark coverage ## run all checks

# Default coverage threshold is 70
COVERAGE_THRESHOLD ?= 70

.PHONY: coverage
coverage: ## check code coverage numbers
@go test -coverprofile=coverage.out -covermode=atomic ./ > /dev/null; \
if [ -f coverage.out ]; then \
coverage=$$(go tool cover -func=coverage.out | grep total: | awk '{print $$3}' | sed 's/%//'); \
if [ $$(echo "$$coverage < $(COVERAGE_THRESHOLD)" | bc -l) -eq 1 ]; then \
echo "❌ Test coverage is below $(COVERAGE_THRESHOLD)%! Actual: $$coverage%"; \
exit 1; \
else \
echo "✅ Test coverage is $$coverage%"; \
fi; \
rm -f coverage.out; \
else \
echo "⚠️ No test coverage data found"; \
exit 1; \
fi

cover-report: ## generate a coverage report
go test -covermode=count -coverpkg=./... -coverprofile cover.out ./...
go tool cover -html cover.out -o cover.html
open cover.html

#==========================================================================================
##@ Release
#==========================================================================================

.PHONY: check-git-clean
check-git-clean: # check if git repo is clean
@git diff --quiet

.PHONY: check-branch
check-branch:
@current_branch=$$(git symbolic-ref --short HEAD) && \
if [ "$$current_branch" != "main" ]; then \
echo "Error: You are on branch '$$current_branch'. Please switch to 'main'."; \
exit 1; \
fi

check_env: # check for needed envs
@[ "${version}" ] || ( echo ">> version is not set, usage: make tag version=\"v1.2.3\" "; exit 1 )


tag: check_env check-branch check-git-clean verify ## create a tag and push to git
@git diff --quiet || ( echo 'git is in dirty state' ; exit 1 )
@[ "${version}" ] || ( echo ">> version is not set, usage: make tag version=\"v1.2.3\" "; exit 1 )
@git tag -d $(version) || true
@git tag -a $(version) -m "Release version: $(version)"
@git push --delete origin $(version) || true
@git push origin $(version) || true

#==========================================================================================
# Help
#==========================================================================================
.PHONY: help
help: # Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
Loading
Loading