Skip to content

Development Guide

This guide covers setting up your development environment, understanding the codebase, and making contributions to KSail.

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

Optional: Node.js 22 (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.)
ksail/
├── .github/ # CI/CD workflows and agentic workflows
│ ├── workflows/ # GitHub Actions workflows (.yaml + .md)
│ └── copilot-instructions.md # Copilot coding instructions
├── docs/ # Documentation (Astro/Starlight)
│ ├── src/content/docs/ # Markdown documentation files
│ └── public/ # Static assets
├── internal/ # Internal (private) packages
│ └── buildmeta/ # Build metadata (version, commit, date)
├── pkg/ # Public packages
│ ├── apis/ # API types and schemas
│ ├── cli/ # CLI commands and UI
│ ├── client/ # Embedded tool clients
│ ├── svc/ # Core services
│ │ ├── provider/ # Infrastructure providers
│ │ ├── provisioner/ # Distribution provisioners
│ │ └── installer/ # Component installers
│ └── [utilities] # di, envvar, fsutil, k8s, notify, etc.
├── schemas/ # JSON schemas for validation
├── vsce/ # VSCode extension
├── main.go # Application entry point
├── go.mod # Go module definition
└── .golangci.yml # Linter configuration

internal/ packages can only be imported within the ksail module; pkg/ packages are public. Circular dependencies are not allowed.

Terminal window
git checkout -b feature/my-feature

Keep changes focused and atomic. Write tests, update documentation, and follow existing code patterns:

Terminal window
go test ./... # Run unit tests
golangci-lint run # Run linters
go build -o ksail # Build
./ksail cluster init # Smoke test
git add . && git commit -m "feat: add new feature"
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.

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

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

Export Pattern for Testing Unexported Functions

Section titled “Export Pattern for Testing Unexported Functions”

Use export_test.go to expose unexported symbols for testing without leaking into production binaries:

pkg/svc/provisioner/export_test.go
package provisioner
var (
ExportValidateConfig = validateConfig
ExportGenerateManifests = generateManifests
)

Test these exports from the corresponding <pkg>_test package (for example, package provisioner_test). Add //nolint:gochecknoglobals on exported vars if required by golangci-lint.

KSail uses mockery for generating mocks via //go:generate go run github.com/vektra/mockery/v2. Use generated mocks in tests:

func TestWithMock(t *testing.T) {
mockProv := new(MockProvider)
mockProv.On("Create", mock.Anything).Return(nil)
// Use mockProv...
mockProv.AssertExpectations(t)
}

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

func BenchmarkProvisionerCreate(b *testing.B) {
p := setupProvisioner()
ctx := context.Background()
for b.Loop() { // Go 1.26+ style
if err := p.Create(ctx); err != nil {
b.Fatal(err)
}
}
}

PRs that modify Go code are automatically benchmarked against main and a comparison comment is posted on the PR. See BENCHMARK-REGRESSION.md for details on interpreting results.

For install duration benchmarks (per-component timing via --benchmark), see benchmarks/install/BENCHMARKS.md.

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 getting-started guide in docs/src/content/docs/getting-started/. 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.0+. 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.

KSail uses several CI workflows:

  • ci.yaml — Runs on every PR and push to main; builds KSail, runs unit and system tests, uploads coverage to Codecov
  • update-skills.yaml — Upgrades npx skills daily and opens a PR when updates are available
  • test-pages.yaml — Validates documentation builds on docs changes
  • publish-pages.yaml — Deploys documentation to GitHub Pages on main branch pushes
  • release.yaml — Builds release binaries and publishes to GitHub Releases; triggered by push to main or workflow_dispatch

KSail uses five AI-powered daily workflows for continuous improvement:

  • daily-code-quality.md — Refactoring, performance optimization, and test coverage improvements
  • daily-plan.md — Triages new issues and creates/prioritizes backlog issues from the roadmap
  • daily-builder.md — Implements roadmap features and tackles backlog issues
  • daily-workflow-maintenance.md — Updates and optimizes CI workflows
  • daily-docs.md — Keeps documentation in sync and reduces bloat

These run on schedules and can be triggered manually via gh aw commands.

Releases are automated via .goreleaser.yaml and .github/workflows/release.yaml:

  1. Prepare: Bump the version, update changelog, and open a PR targeting main.
  2. Merge triggers release: Push to main builds binaries for all platforms, generates checksums, creates a GitHub Release, and publishes the VSCode extension to the marketplace. Can also be triggered via workflow_dispatch.
  3. Optional tagging: Create an annotated tag (git tag -a v5.x.x -m "v5.x.x" && git push origin v5.x.x) for explicit versioning.

PRs should be focused on a single concern with clear commit messages, tests for new functionality, and updated documentation. Squash commits before merge (or allow maintainer to).

Review checklist: code style ✓ | tests added and passing ✓ | docs updated ✓ | commit messages clear ✓ | CI passes ✓ | no security issues ✓