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
34 changes: 34 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# GitHub-native release notes configuration.
#
# GoReleaser's changelog.use is "github-native" (.goreleaser.yml), which
# delegates note generation to GitHub's "Generate release notes" API — and that
# API reads THIS file, not GoReleaser's groups/filters (which it ignores).
#
# Notes are built per merged PR and categorized by the PR's LABELS, which is
# robust to this repo's squash-merge + free-form PR titles (commit-title regexes
# would miss most of them). A PR lands in the FIRST category whose label it
# carries; anything unlabeled falls through to "Other Changes".
changelog:
exclude:
labels:
- automated # e.g. "📚 Automated Documentation Update" PRs
- duplicate
- invalid
- wontfix
- question
authors:
- dependabot[bot]
- github-actions[bot]
categories:
- title: 🚀 Features
labels:
- enhancement
- title: 🐛 Bug Fixes
labels:
- bug
- title: 📚 Documentation
labels:
- documentation
- title: Other Changes
labels:
- "*"
53 changes: 47 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,7 @@ jobs:
echo "==================================================================="
"$OF_BIN" --version
"$OF_BIN" --help
"$OF_BIN" cluster --help
"$OF_BIN" app --help
"$OF_BIN" app install --help
"$OF_BIN" app upgrade --help
"$OF_BIN" bootstrap --help
"$OF_BIN" update --help
# Per-command --help is exercised exhaustively in the next step.
echo "--- completion: every supported shell generates cleanly"
# Shell-completion generation is pure (no cluster/network) — smoke every shell.
for sh in bash zsh fish powershell; do "$OF_BIN" completion "$sh" >/dev/null; done
Expand All @@ -198,6 +193,36 @@ jobs:
# back" notice (offline, no download), not error out or hang.
"$OF_BIN" update rollback

# Exhaustive --help over the WHOLE command tree, on the real (built/staged)
# binary. Complements the hermetic cmd/help_matrix_test.go by covering the
# actual binary (ldflags, embeds, and on Windows the WSL forward). Discovery
# is dynamic — a newly added command is covered with no hand-maintained list.
# Pure and non-interactive (no cluster, no network), so it runs on every OS.
- name: 'CLI: every command --help (exhaustive)'
shell: bash
run: |
echo "==================================================================="
echo "=== TEST: --help on EVERY command (real binary, non-interactive)"
echo "==================================================================="
fail() { echo "::error::$1"; exit 1; }
walk_help() {
local path="$1"
# $path is a space-separated command path ("cluster create") that MUST
# word-split into separate argv entries — intentional, so quiet SC2086.
# shellcheck disable=SC2086
"$OF_BIN" $path --help >/tmp/h.out 2>&1 || { cat /tmp/h.out; fail "'openframe $path --help' exited non-zero"; }
grep -q '^Usage:' /tmp/h.out || { cat /tmp/h.out; fail "'openframe $path --help' printed no Usage section"; }
echo " OK: openframe $path --help"
# Leaf commands list no subcommands, so grep matching nothing is fine.
local subs
subs=$(sed -n '/Available Commands:/,/^$/p' /tmp/h.out | grep -E '^ [a-z]' | awk '{print $1}' || true)
for s in $subs; do
[ "$s" = "help" ] && continue
walk_help "${path:+$path }$s"
done
}
walk_help ""

# Self-update against the REAL latest release: the only test that
# exercises the live trust chain end to end (GitHub release lookup →
# cosign signature of checksums.txt against the pinned identity →
Expand Down Expand Up @@ -502,6 +527,22 @@ jobs:
echo "--- force-sync"
"$OF_BIN" app upgrade --context "$OF_CONTEXT" --sync --verbose

# cluster cleanup is a first-class non-interactive command with logic no
# other step exercises against a real cluster: kube-context-pinned Helm
# uninstalls, ArgoCD finalizer stripping, and node image pruning via crictl
# (k3d nodes run containerd, not docker). Runs on the live, populated cluster
# so all phases have something to do; --force skips the confirmation prompt.
# It leaves the cluster empty; the teardown then deletes it.
- name: 'Cluster: cleanup (releases + crictl image prune)'
if: matrix.os != 'darwin'
shell: bash
timeout-minutes: 10
run: |
echo "==================================================================="
echo "=== TEST: cluster cleanup --force (non-interactive)"
echo "==================================================================="
"$OF_BIN" cluster cleanup "$OF_CLUSTER" --force

# --- Teardown (runs even if a step above failed) -----------------------
- name: 'Teardown: uninstall & delete cluster'
if: always() && matrix.os != 'darwin'
Expand Down
25 changes: 5 additions & 20 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,13 @@ signs:
- '--yes'

changelog:
# github-native delegates note generation to GitHub's "Generate release notes"
# API, which categorizes by PR LABEL via .github/release.yml — the right fit
# for this repo's squash-merge + free-form PR titles. Under github-native
# GoReleaser IGNORES its own `groups`/`filters`, so they are intentionally
# absent here; edit .github/release.yml to change categories or exclusions.
use: github-native
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
- '^ci:'
- '^build:'
- '^style:'
- '^refactor:'
- Merge pull request
- Merge branch
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: Bug fixes
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: Others
order: 999

release:
make_latest: true
Expand Down
89 changes: 48 additions & 41 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,73 +1,80 @@
# OpenFrame CLI Makefile

.PHONY: build build-all clean test test-unit test-race test-integration lint help
.PHONY: all build build-all clean test test-unit test-race test-integration lint fmt vet tidy help

# Variables
BINARY_NAME := openframe
GO_BUILD := CGO_ENABLED=0 go build
# -trimpath drops absolute build paths from the binary (reproducibility), matching
# the release build in .goreleaser.yml.
GO_BUILD := CGO_ENABLED=0 go build -trimpath

# Detect current OS and architecture
GOOS := $(shell go env GOOS)
GOARCH := $(shell go env GOARCH)
BINARY_SUFFIX := $(if $(filter windows,$(GOOS)),.exe,)

# Unit-test package set. Includes the root package (main_test.go — the only
# exit-code fidelity tests) and tests/testutil, which `./cmd/... ./internal/...`
# silently skipped. Deliberately excludes ./tests/integration/... (real clusters).
UNIT_PKGS := . ./cmd/... ./internal/... ./tests/testutil/...

# Default target
all: build

## Build binary for current platform
build:
build: ## Build binary for the current platform
@echo "Building $(BINARY_NAME) for $(GOOS)/$(GOARCH)..."
@$(GO_BUILD) -o $(BINARY_NAME)-$(GOOS)-$(GOARCH)$(BINARY_SUFFIX) .

## Build binaries for all platforms
build-all:
@echo "Building $(BINARY_NAME) for all platforms..."
@GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-linux-amd64 .
@GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-darwin-arm64 .
build-all: ## Cross-compile every release platform (matches .goreleaser.yml)
@echo "Building $(BINARY_NAME) for all release platforms..."
@GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-linux-amd64 .
@GOOS=linux GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-linux-arm64 .
@GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-darwin-amd64 .
@GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-darwin-arm64 .
@GOOS=windows GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-windows-amd64.exe .
@GOOS=windows GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-windows-arm64.exe .

## Run unit tests (vet enabled; -vet=off removed per audit remediation §0).
## Includes the root package (main_test.go — the only exit-code fidelity tests)
## and tests/testutil, which `./cmd/... ./internal/...` silently skipped.
## Deliberately NOT ./tests/integration/... — those create real k3d clusters.
test-unit:
test-unit: ## Run unit tests (vet on; incl. root main_test.go + tests/testutil)
@echo "Running unit tests..."
@go test -count=1 . ./cmd/... ./internal/... ./tests/testutil/...
@go test -count=1 $(UNIT_PKGS)

## Run unit tests with the race detector (CGO required)
test-race:
test-race: ## Run unit tests with the race detector (requires CGO)
@echo "Running unit tests with -race..."
@CGO_ENABLED=1 go test -race -count=1 . ./cmd/... ./internal/... ./tests/testutil/...
@CGO_ENABLED=1 go test -race -count=1 $(UNIT_PKGS)

# Integration tests are opt-in via a build tag: they create REAL k3d clusters and
# run a full bootstrap, so `go test ./...` must never trigger them by accident.
# The harness builds its own CLI binary (see tests/integration/common/cli_runner.go),
# so `make build` is NOT required — but docker and k3d must be installed.
test-integration: ## Run integration tests (real k3d clusters; needs docker + k3d)
@echo "Running integration tests (real clusters!)..."
@go test -tags integration -count=1 ./tests/integration/...

## Run golangci-lint (static-analysis gate: govet, staticcheck, errcheck, gosec, ineffassign)
lint:
test: test-unit test-integration ## Run unit + integration tests

lint: ## Run golangci-lint (govet, staticcheck, errcheck, gosec, ineffassign)
@echo "Running golangci-lint..."
@command -v golangci-lint >/dev/null 2>&1 || { echo "golangci-lint not installed: https://golangci-lint.run/usage/install/"; exit 1; }
@golangci-lint run ./...

## Run integration tests (opt-in via build tag: they create REAL k3d clusters
## and run a full bootstrap — `go test ./...` must never trigger that by
## accident; requires the CLI binary from `make build`)
test-integration:
@echo "Running integration tests (real clusters!)..."
@go test -tags integration -count=1 ./tests/integration/...
fmt: ## Format Go source in place (gofmt -w)
@gofmt -l -w .

vet: ## Run go vet on all packages
@go vet ./...

## Run all tests
test: test-unit test-integration
tidy: ## Check go.mod/go.sum are tidy (fails if `go mod tidy` would change them)
@go mod tidy -diff

## Clean build artifacts
clean:
@rm -f $(BINARY_NAME) $(BINARY_NAME)-*
# clean matches only the platform-suffixed binaries, NOT a broad `openframe-*`
# glob — that also matched tracked files like openframe-helm-values.example.yaml
# and deleted them.
clean: ## Remove build artifacts
@rm -f $(BINARY_NAME) \
$(BINARY_NAME)-linux-* $(BINARY_NAME)-darwin-* $(BINARY_NAME)-windows-*
@echo "Cleaned build artifacts"

## Show help
help:
help: ## Show this help
@echo "Available targets:"
@echo " build - Build binary for current platform (default)"
@echo " build-all - Build binaries for all platforms"
@echo " test - Run all tests"
@echo " test-unit - Run unit tests (vet enabled)"
@echo " test-race - Run unit tests with the race detector"
@echo " lint - Run golangci-lint static-analysis gate"
@echo " test-integration - Run integration tests"
@echo " clean - Clean build artifacts"
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
12 changes: 9 additions & 3 deletions cmd/prerequisites/prerequisites.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ func printResult(res fw.Result) {
pterm.Success.Printf("✓ %s (installed)\n", name)
}
for _, m := range res.Missing {
pterm.Error.Printf("✗ %s is not installed\n", m.Name)
if m.DocsURL != "" {
pterm.Info.Printf(" How to install: %s\n", m.DocsURL)
if m.Reason != "" {
// A specific reason (e.g. "installed but not running") — the tool IS
// present, so don't claim it isn't and don't show install docs.
pterm.Error.Printf("✗ %s: %s\n", m.Name, m.Reason)
} else {
pterm.Error.Printf("✗ %s is not installed\n", m.Name)
if m.DocsURL != "" {
pterm.Info.Printf(" How to install: %s\n", m.DocsURL)
}
}
if m.Err != nil {
pterm.Debug.Printf(" (%v)\n", m.Err)
Expand Down
26 changes: 26 additions & 0 deletions docs/development/setup/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,32 @@ kubectl get pods --all-namespaces
kubectl get applications -n argocd
```

### Overriding ArgoCD chart values

The CLI installs ArgoCD from a built-in baseline (embedded
`internal/chart/providers/argocd/argocd-values.yaml`), which is separate from
the app-of-apps values. To change an ArgoCD chart value without rebuilding the
CLI, add a top-level `argocd:` section to `openframe-helm-values.yaml`:

```yaml
# openframe-helm-values.yaml
repository:
branch: main # (app-of-apps settings, as before)

argocd: # deep-merged over the built-in ArgoCD baseline
dex:
enabled: true # e.g. re-enable dex (disabled by default)
server:
replicas: 2
```

Only the `argocd:` subtree is applied to the ArgoCD install — the rest of the
file targets the app-of-apps chart, and keeping them separate stops secrets
(e.g. the docker registry password) from leaking into the ArgoCD release. The
merge follows Helm semantics (maps merge, scalars/lists replace), and the CLI
prints a warning listing the keys you overrode, since a bad override can break
the ArgoCD install. Without an `argocd:` section the baseline is used unchanged.

## Cross-platform Builds

```bash
Expand Down
1 change: 0 additions & 1 deletion internal/bootstrap/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,3 @@ func (s *Service) installChart(ctx context.Context, clusterName string, nonInter
ClusterAccess: cluster.NewClusterService(executor.NewRealCommandExecutor(false, verbose)),
})
}

31 changes: 21 additions & 10 deletions internal/chart/providers/argocd/argocd-values.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Baseline Helm values for the Argo CD chart (targets chart 10.x / app v3.x).
#
# This file is embedded into the CLI binary via go:embed (see values.go) and is
# the single source of truth for the ArgoCD install the CLI performs BEFORE the
# app-of-apps is cloned from the platform repo. Keep component keys (redis, dex,
# applicationSet, notifications) aligned with the pinned chart version.
# the BASELINE for the ArgoCD install the CLI performs BEFORE the app-of-apps is
# cloned from the platform repo. Keep component keys (redis, dex, applicationSet,
# notifications) aligned with the pinned chart version.
#
# USER OVERRIDES: a user can override any value here without rebuilding the CLI
# by adding a top-level `argocd:` section to openframe-helm-values.yaml; that
# subtree is deep-merged over this baseline (maps merge, scalars/lists replace)
# and the CLI prints a warning naming the overridden keys. Only the `argocd:`
# subtree is used — the rest of that file targets the app-of-apps chart. Example:
# argocd:
# dex:
# enabled: true # re-enable dex (disabled by default below)
# See MergedArgoCDValues in values.go.
#
# SECURITY — developer account default:
# configs.secret.extra.accounts.developer.password below is a bcrypt hash of
Expand Down Expand Up @@ -112,13 +122,14 @@ redis:


dex:
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
# Dex (SSO) is disabled: the chart defaults it on (dex.enabled: true), but
# OpenFrame's local/oss-tenant login uses the local `developer` account
# configured above, never dex. Beyond being dead weight on local k3d, the
# dexidp/dex:v2.45.1 arm64 image intermittently SIGSEGVs under emulation on
# Apple Silicon (exit 139) before its first log line -> CrashLoopBackOff ->
# the 7m `helm --wait` never completes -> fresh installs fail. Disabling it
# removes the blocker; re-enable and pin a known-good image if SSO is needed.
enabled: false


applicationSet:
Expand Down
9 changes: 9 additions & 0 deletions internal/chart/providers/argocd/assess.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ type appAssessment struct {
notReadyNames []string // bare names of apps not yet ready (for kubectl commands)
}

// appNames returns the names of the given applications, preserving order.
func appNames(apps []Application) []string {
names := make([]string, 0, len(apps))
for _, app := range apps {
names = append(names, app.Name)
}
return names
}

// assessApplications classifies the applications for one polling tick and
// marks apps that are currently Healthy+Synced in everReady (the session-wide
// set of apps that have ever been ready).
Expand Down
Loading