Skip to content

Development Guide

Required: Go 1.26.1+ (go version ≥ 1.26.1), Docker (docker ps without errors), Git.

Optional: Node.js 24 (docs), golangci-lint, VSCode with Go extension.

Terminal window
git clone https://github.com/devantler-tech/ksail.git
cd ksail
Terminal window
go mod download
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest # optional
cd docs/ && npm ci && cd .. # docs only
Terminal window
go build -o ksail && ./ksail --version
Terminal window
go test ./... # all tests
go test ./pkg/svc/provisioner/cluster/kind # specific package
go test -cover ./... # with coverage
go test -bench=. -benchmem ./pkg/client/helm # benchmarks
Terminal window
golangci-lint run # all linters
golangci-lint run --fix # auto-fix (import order, etc.)

See CONTRIBUTING.md for the full package structure and descriptions of each package. internal/ packages can only be imported within the ksail module; pkg/ packages are public. Circular dependencies are not allowed.

Create a feature branch, make focused atomic changes, write tests, and update docs:

Terminal window
go test ./... && golangci-lint run && go build -o ksail
./ksail cluster init # Smoke test
git add . && git commit -m "feat: add widget to cluster create" && git push origin feature/my-feature

Commit types: feat, fix, docs, refactor, test, chore, perf. Open a PR with a clear description and ensure CI passes.

Follow Effective Go. Use gofmt for formatting, goimports for import order, and keep lines under 100 characters (enforced by golines).

EntityConventionExample
PackagesLowercase, singularprovider, installer
Types, InterfacesPascalCase; interfaces often end in -erKindClusterProvisioner, Provider
Functions/MethodsPascalCase (exported), camelCase (unexported)Create, validateConfig
Variables, ConstantscamelCase (unexported), PascalCase (exported)clusterName, DefaultTimeout

Return errors explicitly (no panic in library code). Wrap errors with fmt.Errorf("context: %w", err) and use custom error types where appropriate. golangci-lint enforces that all errors are checked.

All user-supplied file path arguments in CLI commands must be canonicalized with fsutil.EvalCanonicalPath before use. This resolves symlinks and produces an absolute path, preventing symlink-escape attacks in CI pipelines that process external manifests.

canonPath, err := fsutil.EvalCanonicalPath(inputPath)
if err != nil {
return fmt.Errorf("canonicalizing path %q: %w", inputPath, err)
}
// use canonPath for all subsequent file operations

When canonicalizing an output path that may not yet exist (e.g., --output), first create the parent directory of the output path with os.MkdirAll(filepath.Dir(outputPath), <mode>), then call EvalCanonicalPath on the output path. For safe path-constrained reads, use fsutil.ReadFileSafe instead of reimplementing containment checks.

Use table-driven tests with t.Parallel():

func TestMyFunction(t *testing.T) {
t.Parallel()
tests := []struct{ name string; wantErr bool }{
{"valid", false}, {"invalid", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Parallel(); /* test body */ })
}
}

Cluster command tests that depend on installer factories use an override pattern: each Set*ForTests helper applies a factory override against shared global state and returns a restore function. Register the restore function with t.Cleanup(restore) so overrides are always reset:

restore := cluster.Set<Component>InstallerFactoryForTests(
func(_ *v1alpha1.Cluster) (installer.Installer, error) { return mockInstaller, nil },
)
t.Cleanup(restore)
// test logic...

Note: These helpers mutate shared global state, so tests that use them must not run in parallel (annotate with //nolint:paralleltest and do not call t.Parallel()). Search for Set*ForTests in pkg/cli/cmd/cluster/ for all available helpers.

KSail uses mockery v3.5+ for generating mocks via //go:generate mockery. Call mockProv.AssertExpectations(t) at the end of each test.

Use for b.Loop() style (Go 1.26+). PRs are auto-benchmarked against main; see Benchmarks. Use ksail cluster create --benchmark for per-component timings.

Note: Never use .Times(b.N) with for b.Loop() — b.N is 1 when evaluated before the loop. Omit .Times() for unlimited calls.

Document all exported types, functions, and constants with complete sentences starting with the name being documented (e.g., // Provider defines the interface for infrastructure providers.). When adding features, update the relevant .mdx files in docs/src/content/docs/, README.md if needed, and CLI help text if adding commands.

All new components follow the same pattern: create a package under the relevant pkg/svc/ subdirectory, implement the corresponding interface, register in the factory, add tests, and update documentation.

TaskPackage pathInterfaceFactory
New Providerpkg/svc/provider/newprovider/Providerpkg/svc/provider/factory.go
New Provisionerpkg/svc/provisioner/cluster/newdist/Provisionerpkg/svc/provisioner/cluster/factory.go
New Installerpkg/svc/installer/newcomponent/Installerpkg/cli/setup/

For a new Provisioner, also add a distribution guide in docs/src/content/docs/distributions/. For a new Installer, also add configuration to pkg/apis/cluster/v1alpha1/.

Terminal window
go get -u ./... # update all
go get -u github.com/some/package@latest # update specific
go mod tidy && go mod verify # tidy and verify
go test ./... # verify
Terminal window
cd docs/
npm ci # install dependencies
npm run dev # serve at http://localhost:4321
npm run build # build to docs/dist/
npm run preview # preview production build

KSail doesn’t have built-in debug logging yet. Use temporary fmt.Printf("DEBUG: value = %+v\n", value) statements or the Delve debugger:

Terminal window
go install github.com/go-delve/delve/cmd/dlv@latest
dlv test -- -test.run TestName # debug a test
dlv exec ./ksail -- cluster create # debug the binary

VSCode users can set breakpoints and press F5 with a .vscode/launch.json configuration.

ErrorFix
”Docker not available”Verify Docker is running: docker ps. Check Docker Desktop is started (macOS/Windows) or socket permissions (Linux).
”Go version mismatch”Run go version — must be 1.26.1+. Update from go.dev/dl.
”Import cycle not allowed”Move shared types to pkg/apis/ or a utility package. Avoid circular imports.
”Linter failures”Run golangci-lint run --fix to auto-fix. Check .golangci.yml for enabled linters; some issues require manual fixes.
  • ci.yaml — Builds and caches the KSail binary, generates schemas/reference docs, validates documentation builds, audits docs/vsce dependencies, packages the VS Code extension, runs system tests, and runs benchmark regression tests (for example via merge queue and workflow_dispatch)
  • update-skills.yaml — Upgrades GitHub Copilot skills daily; opens a PR when updates are available
  • release.yaml — Creates the next semantic version tag on pushes to main
  • cd.yaml — Triggered on version tag pushes; runs GoReleaser, publishes the MCP registry entry, deploys documentation to GitHub Pages, publishes the VSCode extension, and opens a Homebrew tap PR

Five AI-powered workflows run on schedules or triggers (manageable via gh aw): repo-assist (issue triage, code quality, and repository maintenance — every 12h), daily-workflow-maintenance (CI/CD updates and optimization — daily), daily-docs (documentation sync and bloat reduction — daily/on push), weekly-strategy (roadmap planning and project promotion — weekly), and ci-doctor (CI failure investigation — on CI failure).

Releases are automated via .goreleaser.yaml, .github/workflows/release.yaml (tagging), and .github/workflows/cd.yaml (publishing):

  1. Prepare: Bump the version, update changelog, and open a PR targeting main.
  2. Merge triggers tagging: Push to main runs .github/workflows/release.yaml, which validates the release configuration and creates/pushes the version tag (for example, v5.x.x). The same tagging workflow can also be run manually via workflow_dispatch on release.yaml to create and push a release tag without a new main commit.
  3. Tag-triggered CD: Any push of a version tag like v5.x.x (whether created by release.yaml or manually via git tag -a v5.x.x -m "v5.x.x" && git push origin v5.x.x) triggers .github/workflows/cd.yaml, which builds binaries for all platforms, generates checksums, creates the GitHub Release, publishes the MCP registry entry (after GoReleaser confirms the OCI image is available), deploys documentation to GitHub Pages, publishes the VSCode extension to the marketplace, and opens a PR to update the Homebrew cask in devantler-tech/homebrew-tap.

Discussions ¡ Issues ¡ Documentation ¡ CONTRIBUTING.md

PRs should be focused on a single concern with clear commit messages, tests for new functionality, and updated documentation. Squash commits before merge. Checklist: code style ¡ tests added and passing ¡ docs updated ¡ commit messages clear ¡ CI passes ¡ no security issues