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
69 changes: 69 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: ci

on:
push:
branches: [dev]
pull_request:
branches: [dev]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

env:
GO_VERSION: "1.25.x"
GOLANGCI_VERSION: "v2.12.2"

jobs:
build-test-lint:
name: build · vet · gofmt · lint · test · e2e
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3
with:
persist-credentials: false

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ${{ env.GO_VERSION }}
check-latest: true
cache: true
cache-dependency-path: go.sum

- name: Download and verify modules
run: |
go mod download
go mod verify

- name: gofmt check
run: |
diff=$(gofmt -l .)
if [ -n "$diff" ]; then
echo "::error::gofmt needed on:"
echo "$diff"
exit 1
fi

- name: go vet
run: go vet ./...

- name: golangci-lint
uses: golangci/golangci-lint-action@d583c34f0599d37dbac4a198b9c83201be380893 # v9.3.0
with:
version: ${{ env.GOLANGCI_VERSION }}
args: ./...

- name: Format and imports check
run: golangci-lint fmt --diff ./...

- name: Build
run: go build -trimpath ./...

- name: Unit tests with race detector
run: go test -race -count=1 -coverprofile=coverage.out ./...

- name: Binary integration smoke test
run: go test -race -count=1 -tags=e2e ./tests/e2e
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ bin/
vendor/
*.test
*.out
coverage.*
# Node / UI
node_modules/
dist/
Expand Down
43 changes: 43 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
version: "2"

run:
timeout: 5m
tests: true

linters:
default: none
enable:
- bodyclose
- errcheck
- gosec
- govet
- ineffassign
- misspell
- revive
- staticcheck
- unconvert
- unused
settings:
revive:
rules:
- name: exported
- name: package-comments
- name: error-strings
- name: context-as-argument
- name: unreachable-code
misspell:
locale: US
exclusions:
generated: lax
rules:
- path: _test\.go
linters: [errcheck, gosec]

formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/ArdurAI/sith
60 changes: 60 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Sith — Makefile
SHELL := /usr/bin/env bash

BINARY := sith
PKG := github.com/ArdurAI/sith
CMD := ./cmd/sith
BIN_DIR := bin
GOLANGCI ?= golangci-lint

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

LDFLAGS := -s -w \
-X $(PKG)/internal/buildinfo.Version=$(VERSION) \
-X $(PKG)/internal/buildinfo.Commit=$(COMMIT) \
-X $(PKG)/internal/buildinfo.Date=$(DATE)

.PHONY: all build test e2e lint fmt fmt-check vet tidy clean run ci help

all: build

build: ## Build the sith binary into bin/
@mkdir -p $(BIN_DIR)
go build -trimpath -ldflags '$(LDFLAGS)' -o $(BIN_DIR)/$(BINARY) $(CMD)

test: ## Run unit tests with the race detector and report coverage
go test -race -count=1 -coverprofile=coverage.out ./...

e2e: ## Build and exercise the real binary as a subprocess
go test -race -count=1 -tags=e2e ./tests/e2e

lint: ## Run golangci-lint (v2)
$(GOLANGCI) run ./...

fmt: ## Format code (gofmt + goimports via golangci-lint v2 formatters)
$(GOLANGCI) fmt ./...

fmt-check: ## Fail if formatting/imports would change anything
gofmt -l . | tee /dev/stderr | (! read)
$(GOLANGCI) fmt --diff ./...

vet: ## Run go vet
go vet ./...

tidy: ## Tidy and verify modules
go mod tidy
go mod verify

clean: ## Remove build and coverage artifacts
rm -rf $(BIN_DIR) coverage.out

run: build ## Build then run sith version
$(BIN_DIR)/$(BINARY) version

ci: fmt-check vet lint test e2e build ## Run the full CI gate locally

help: ## List targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \
awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-10s\033[0m %s\n",$$1,$$2}'
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
# Sith

**Status: planning.** No product code yet.
**Status: Slice 0 foundation.** The local-first CLI walking skeleton is runnable; Kubernetes
context discovery arrives in Slice 1.

Sith is a governed, multi-tenant control plane for **cross-cluster Kubernetes fleet
operations** — one place to safely *see* and *act* across many clusters, while deep
cluster access stays local. It is built on [Open Cluster Management (OCM)](https://open-cluster-management.io/).
Sith is ArdurAI's single-binary, local-first Kubernetes fleet tool: **k9s for your whole fleet**.
It is designed to aggregate every kubeconfig context without an account, telemetry, or cluster
data leaving the machine. The same source-abstract fleet model will later power an optional
governed hub.

The full charter, architecture, ADRs, threat model, roadmap, and competitive analysis
land via the initial planning pull request. The owner reviews the plan before any
implementation begins.
## Build and run

See the open PR and `docs/` for the plan.
Sith requires a supported Go 1.25 toolchain.

```bash
make build
./bin/sith version
./bin/sith version --output json
./bin/sith clusters
```

Slice 0 intentionally returns a typed empty fleet through the stubbed `fleet.Source` seam. Run the
full local quality gate with a pinned golangci-lint v2.12.2 on `PATH`:

```bash
make ci
```

The architecture, threat model, ADRs, and roadmap live under [`docs/`](docs/). Build-session
checkpoints are recorded under [`sessions/`](sessions/).
14 changes: 14 additions & 0 deletions cmd/sith/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0

// Package main is the process entrypoint for the Sith binary.
package main

import (
"os"

"github.com/ArdurAI/sith/internal/cli"
)

func main() {
os.Exit(cli.Execute())
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/ArdurAI/sith

go 1.25.0

require (
github.com/spf13/cobra v1.10.2
go.yaml.in/yaml/v3 v3.0.4
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
61 changes: 61 additions & 0 deletions internal/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0

// Package buildinfo exposes build metadata injected by the build pipeline.
package buildinfo

import (
"encoding/json"
"fmt"
"runtime"
)

// Version is the semantic version or development identifier injected at build time.
var Version = "dev"

// Commit is the source commit injected at build time.
var Commit = "none"

// Date is the UTC build timestamp injected at build time.
var Date = "unknown"

// Info is the resolved build metadata, including runtime-derived fields.
type Info struct {
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
Go string `json:"go"`
Platform string `json:"platform"`
}

// Get returns the build metadata combined with the active Go runtime and platform.
func Get() Info {
return Info{
Version: Version,
Commit: Commit,
Date: Date,
Go: runtime.Version(),
Platform: runtime.GOOS + "/" + runtime.GOARCH,
}
}

// String returns build metadata in the human-readable CLI format.
func (i Info) String() string {
return fmt.Sprintf(
"sith %s\n commit: %s\n built: %s\n go: %s\n platform: %s",
i.Version,
i.Commit,
i.Date,
i.Go,
i.Platform,
)
}

// JSON returns build metadata as compact JSON.
func (i Info) JSON() (string, error) {
data, err := json.Marshal(i)
if err != nil {
return "", fmt.Errorf("marshal build metadata: %w", err)
}

return string(data), nil
}
68 changes: 68 additions & 0 deletions internal/buildinfo/buildinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: Apache-2.0

package buildinfo

import (
"encoding/json"
"runtime"
"strings"
"testing"
)

func TestGetPopulatesRuntimeFields(t *testing.T) {
t.Parallel()

got := Get()
if got.Go != runtime.Version() {
t.Fatalf("Go = %q, want %q", got.Go, runtime.Version())
}

wantPlatform := runtime.GOOS + "/" + runtime.GOARCH
if got.Platform != wantPlatform {
t.Fatalf("Platform = %q, want %q", got.Platform, wantPlatform)
}
}

func TestStringContainsAllFields(t *testing.T) {
t.Parallel()

info := Info{
Version: "v1.2.3",
Commit: "abc1234",
Date: "2026-07-10T12:00:00Z",
Go: "go1.25.12",
Platform: "linux/amd64",
}

for _, want := range []string{info.Version, info.Commit, info.Date, info.Go, info.Platform} {
if !strings.Contains(info.String(), want) {
t.Errorf("String() = %q, want it to contain %q", info.String(), want)
}
}
}

func TestJSONRoundTrips(t *testing.T) {
t.Parallel()

want := Info{
Version: "v1.2.3",
Commit: "abc1234",
Date: "2026-07-10T12:00:00Z",
Go: "go1.25.12",
Platform: "linux/amd64",
}

encoded, err := want.JSON()
if err != nil {
t.Fatalf("JSON() error = %v", err)
}

var got Info
if err := json.Unmarshal([]byte(encoded), &got); err != nil {
t.Fatalf("unmarshal JSON: %v", err)
}

if got != want {
t.Fatalf("round trip = %#v, want %#v", got, want)
}
}
Loading