From ea0b385ceb0de070dd893434cc03bff843e9f212 Mon Sep 17 00:00:00 2001 From: omattsson Date: Thu, 26 Mar 2026 19:51:50 +0100 Subject: [PATCH 1/7] feat: add CI/CD pipeline, Makefile, Docker, and goreleaser - Makefile: build, build-all (5 platforms), test, lint, coverage, install, clean - CI workflow: test + lint + build on push/PR to main - Release workflow: goreleaser on version tags with Homebrew tap publishing - Dockerfile: multi-stage build, non-root user, alpine runtime - goreleaser v2: cross-compilation, archives, checksums, changelog, brew formula - Pin Go 1.26.1 and staticcheck 2026.1 for reproducible builds Closes #8 --- .dockerignore | 10 ++++++ .github/workflows/ci.yml | 58 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 32 +++++++++++++++++ .goreleaser.yml | 65 +++++++++++++++++++++++++++++++++++ Dockerfile | 30 ++++++++++++++++ cli/.gitignore | 1 + cli/Makefile | 46 +++++++++++++++++++++++++ 7 files changed, 242 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml create mode 100644 Dockerfile create mode 100644 cli/Makefile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..44e1cbb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +cli/bin +cli/dist +cli/coverage.out +.github +.claude +.goreleaser.yml +cli/Makefile +*.md +LICENSE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8060d8e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: cli + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.1' + cache-dependency-path: cli/go.sum + - run: make test + + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: cli + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.1' + cache-dependency-path: cli/go.sum + - run: go install honnef.co/go/tools/cmd/staticcheck@2026.1 + - run: make lint + + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [test, lint] + defaults: + run: + working-directory: cli + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.1' + cache-dependency-path: cli/go.sum + - run: make build-all diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..38a604c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: '1.26.1' + cache-dependency-path: cli/go.sum + - name: Run tests + working-directory: cli + run: make test + - name: Run goreleaser + uses: goreleaser/goreleaser-action@v6 + with: + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..3e1ec6b --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,65 @@ +version: 2 + +project_name: stackctl + +dist: cli/dist + +builds: + - dir: cli + main: . + binary: stackctl + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} + - -X main.date={{.Date}} + +archives: + - formats: + - tar.gz + format_overrides: + - goos: windows + formats: + - zip + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +checksum: + name_template: "checksums.txt" + algorithm: sha256 + +changelog: + sort: asc + filters: + exclude: + - "^Merge" + +brews: + - repository: + owner: omattsson + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" + homepage: https://github.com/omattsson/stackctl + description: CLI for managing Kubernetes stack deployments via k8s-stack-manager + license: MIT + install: | + bin.install "stackctl" + test: | + system "#{bin}/stackctl", "version" + +release: + github: + owner: omattsson + name: stackctl + draft: false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9a0a424 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Stage 1: Build +FROM golang:1.26.1-alpine AS builder + +ARG VERSION=dev +ARG COMMIT=none +ARG DATE=unknown + +WORKDIR /build + +# Cache dependencies +COPY cli/go.mod cli/go.sum ./cli/ +RUN cd cli && go mod download + +# Copy source and build +COPY cli/ ./cli/ +RUN cd cli && CGO_ENABLED=0 go build \ + -ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \ + -o /build/stackctl . + +# Stage 2: Runtime +FROM alpine:3.20 + +RUN apk add --no-cache ca-certificates && \ + adduser -D -h /home/stackctl stackctl + +COPY --from=builder /build/stackctl /usr/local/bin/stackctl + +USER stackctl + +ENTRYPOINT ["stackctl"] diff --git a/cli/.gitignore b/cli/.gitignore index 1460c08..9c3821c 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -1,2 +1,3 @@ bin/ +dist/ coverage.out diff --git a/cli/Makefile b/cli/Makefile new file mode 100644 index 0000000..b142dea --- /dev/null +++ b/cli/Makefile @@ -0,0 +1,46 @@ +# stackctl CLI Makefile + +BINARY_NAME = stackctl +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 main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE) + +# windows/arm64 excluded: not a common deployment target and avoids cross-compilation issues +PLATFORMS = linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 + +.PHONY: build build-all test lint coverage install clean + +build: + CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY_NAME) . + +build-all: + @for platform in $(PLATFORMS); do \ + os=$${platform%/*}; \ + arch=$${platform#*/}; \ + output=bin/$(BINARY_NAME)-$${os}-$${arch}; \ + if [ "$${os}" = "windows" ]; then output=$${output}.exe; fi; \ + echo "Building $${output}..."; \ + CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} go build -ldflags "$(LDFLAGS)" -o $${output} . || exit 1; \ + done + +test: + go test ./... -v + +lint: + go vet ./... + @if command -v staticcheck >/dev/null 2>&1; then \ + staticcheck ./...; \ + else \ + echo "staticcheck not installed, skipping (go install honnef.co/go/tools/cmd/staticcheck@latest)"; \ + fi + +coverage: + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out + +install: + go install -ldflags "$(LDFLAGS)" . + +clean: + rm -rf bin/ From 67e339b48bcb4af6cde68158ad0a4e0c2dcd7d77 Mon Sep 17 00:00:00 2001 From: omattsson Date: Thu, 26 Mar 2026 19:58:33 +0100 Subject: [PATCH 2/7] fix: remove unused stdout variable in e2e test (staticcheck SA4006) --- cli/test/e2e/cli_e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/test/e2e/cli_e2e_test.go b/cli/test/e2e/cli_e2e_test.go index 5bc58d5..124cfce 100644 --- a/cli/test/e2e/cli_e2e_test.go +++ b/cli/test/e2e/cli_e2e_test.go @@ -152,7 +152,7 @@ func TestE2E_ConfigWorkflow(t *testing.T) { } // 7. Add another context - stdout, _, err = runStackctl(t, dir, "config", "use-context", "production") + _, _, err = runStackctl(t, dir, "config", "use-context", "production") require.NoError(t, err) _, _, err = runStackctl(t, dir, "config", "set", "api-url", "https://prod.example.com") From adc4a960859aec062c9af9d3e50c261c10a021cc Mon Sep 17 00:00:00 2001 From: omattsson Date: Thu, 26 Mar 2026 20:01:34 +0100 Subject: [PATCH 3/7] fix: create bin/ directory before build targets Ensures 'make build' and 'make build-all' work on a clean checkout where bin/ does not yet exist. --- cli/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/Makefile b/cli/Makefile index b142dea..94abf6a 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -12,9 +12,11 @@ PLATFORMS = linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 .PHONY: build build-all test lint coverage install clean build: + @mkdir -p bin CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY_NAME) . build-all: + @mkdir -p bin @for platform in $(PLATFORMS); do \ os=$${platform%/*}; \ arch=$${platform#*/}; \ From 953a7374a7674bc314e7b215b81496fbf648bc25 Mon Sep 17 00:00:00 2001 From: omattsson Date: Thu, 26 Mar 2026 20:14:33 +0100 Subject: [PATCH 4/7] fix: address PR review comments - Add CGO_ENABLED=0 to make install for consistency with build/release - Switch CI from go-version to go-version-file: cli/go.mod to avoid drift - Add lint step to release workflow to gate tagged releases - Pin Dockerfile base images by digest for reproducibility --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 8 +++++--- Dockerfile | 4 ++-- cli/Makefile | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8060d8e..12e4a79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.1' + go-version-file: cli/go.mod cache-dependency-path: cli/go.sum - run: make test @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.1' + go-version-file: cli/go.mod cache-dependency-path: cli/go.sum - run: go install honnef.co/go/tools/cmd/staticcheck@2026.1 - run: make lint @@ -53,6 +53,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.1' + go-version-file: cli/go.mod cache-dependency-path: cli/go.sum - run: make build-all diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38a604c..1a48897 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,11 +18,13 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v5 with: - go-version: '1.26.1' + go-version-file: cli/go.mod cache-dependency-path: cli/go.sum - - name: Run tests + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@2026.1 + - name: Run tests and lint working-directory: cli - run: make test + run: make test && make lint - name: Run goreleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/Dockerfile b/Dockerfile index 9a0a424..821074b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build -FROM golang:1.26.1-alpine AS builder +FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder ARG VERSION=dev ARG COMMIT=none @@ -18,7 +18,7 @@ RUN cd cli && CGO_ENABLED=0 go build \ -o /build/stackctl . # Stage 2: Runtime -FROM alpine:3.20 +FROM alpine:3.20@sha256:a4f4213abb84c497377b8544c81b3564f313746700372ec4fe84653e4fb03805 RUN apk add --no-cache ca-certificates && \ adduser -D -h /home/stackctl stackctl diff --git a/cli/Makefile b/cli/Makefile index 94abf6a..7efb66d 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -42,7 +42,7 @@ coverage: go tool cover -func=coverage.out install: - go install -ldflags "$(LDFLAGS)" . + CGO_ENABLED=0 go install -ldflags "$(LDFLAGS)" . clean: rm -rf bin/ From d982ee55bd3d12128ff0dc601ef9919be11a38f0 Mon Sep 17 00:00:00 2001 From: omattsson Date: Sat, 28 Mar 2026 14:44:11 +0100 Subject: [PATCH 5/7] fix: add Windows .exe suffix to build target and centralize staticcheck version - Detect GOOS=windows or OS=Windows_NT to append .exe in build target - Add STATICCHECK_VERSION variable (2026.1) and make tools target - Update CI and release workflows to use make tools instead of hardcoded version - Update lint hint to reference make tools Addresses PR review comments from #18 --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 3 ++- cli/Makefile | 22 +++++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12e4a79..cfb8d8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: with: go-version-file: cli/go.mod cache-dependency-path: cli/go.sum - - run: go install honnef.co/go/tools/cmd/staticcheck@2026.1 + - run: make tools - run: make lint build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a48897..20ed70c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,8 @@ jobs: go-version-file: cli/go.mod cache-dependency-path: cli/go.sum - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@2026.1 + working-directory: cli + run: make tools - name: Run tests and lint working-directory: cli run: make test && make lint diff --git a/cli/Makefile b/cli/Makefile index 7efb66d..65bedef 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -6,14 +6,27 @@ 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 main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE) +STATICCHECK_VERSION ?= 2026.1 + +# Detect Windows for .exe extension +ifdef GOOS + ifeq ($(GOOS),windows) + EXT = .exe + endif +else + ifeq ($(OS),Windows_NT) + EXT = .exe + endif +endif + # windows/arm64 excluded: not a common deployment target and avoids cross-compilation issues PLATFORMS = linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 -.PHONY: build build-all test lint coverage install clean +.PHONY: build build-all test lint coverage install clean tools build: @mkdir -p bin - CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY_NAME) . + CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY_NAME)$(EXT) . build-all: @mkdir -p bin @@ -34,7 +47,7 @@ lint: @if command -v staticcheck >/dev/null 2>&1; then \ staticcheck ./...; \ else \ - echo "staticcheck not installed, skipping (go install honnef.co/go/tools/cmd/staticcheck@latest)"; \ + echo "staticcheck not installed, skipping (run 'make tools' to install staticcheck@$(STATICCHECK_VERSION))"; \ fi coverage: @@ -44,5 +57,8 @@ coverage: install: CGO_ENABLED=0 go install -ldflags "$(LDFLAGS)" . +tools: + go install honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) + clean: rm -rf bin/ From 72285c8328899c193e34b800f5052dfd7c22f3a5 Mon Sep 17 00:00:00 2001 From: omattsson Date: Sat, 28 Mar 2026 14:50:28 +0100 Subject: [PATCH 6/7] fix: extend make clean and pin goreleaser version - Add dist/ and coverage.out to make clean target - Pin goreleaser to v2.7.0 in release workflow for reproducible builds Addresses PR review comments from #18 --- .github/workflows/release.yml | 1 + cli/Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20ed70c..00bd788 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,7 @@ jobs: - name: Run goreleaser uses: goreleaser/goreleaser-action@v6 with: + version: v2.7.0 args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cli/Makefile b/cli/Makefile index 65bedef..ce2d467 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -61,4 +61,4 @@ tools: go install honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) clean: - rm -rf bin/ + rm -rf bin/ dist/ coverage.out From 99adf08037cceb655c186d474ae1ac360dcbdec9 Mon Sep 17 00:00:00 2001 From: omattsson Date: Sat, 28 Mar 2026 15:11:09 +0100 Subject: [PATCH 7/7] fix: make lint fail when staticcheck is not installed Address PR review comment: lint target now exits with error instead of silently skipping when staticcheck is missing. --- cli/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/Makefile b/cli/Makefile index ce2d467..38e7b3f 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -44,11 +44,11 @@ test: lint: go vet ./... - @if command -v staticcheck >/dev/null 2>&1; then \ - staticcheck ./...; \ - else \ - echo "staticcheck not installed, skipping (run 'make tools' to install staticcheck@$(STATICCHECK_VERSION))"; \ + @if ! command -v staticcheck >/dev/null 2>&1; then \ + echo "Error: staticcheck is not installed. Run 'make tools' to install staticcheck@$(STATICCHECK_VERSION)"; \ + exit 1; \ fi + staticcheck ./... coverage: go test ./... -coverprofile=coverage.out