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
72 changes: 72 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Patternizer - Agent Guidelines

Patternizer is a Go CLI tool that bootstraps Git repositories containing Helm charts into ready-to-use Validated Patterns. Module path: `github.com/validatedpatterns/patternizer`.

## Build, Lint, and Test

Run `make dev-setup` once to install tooling (golangci-lint, ginkgo).

Key make targets:

- `make ci` - Full CI pipeline: lint, build, test. Run this before considering any change complete.
- `make build` - Build the binary.
- `make test` - Run all tests via Ginkgo.
- `make lint` - Run all linters (gofmt, go vet, golangci-lint).
- `make fmt` - Auto-format Go code.
- `make check` - Quick check: format, vet, build, unit tests.

All Go source lives under `src/`. The Makefile handles `cd src` automatically.

## Code Style

- No unnecessary inline comments. Code should be self-documenting through clear naming and structure.
- No emojis, ASCII art, or non-standard characters anywhere in the codebase.
- Exported functions must have godoc comments (enforced by the revive linter).
- Run `make fmt` before committing. Formatting is checked by `make lint`.
- Follow the existing project layout:
- `src/cmd/` - Cobra CLI command definitions.
- `src/internal/` - Private packages (not importable externally).
- `src/internal/types/` - Shared data structures.
- `src/internal/embedded/` - Embedded resources via `go:embed`.

## Go Best Practices

- Wrap errors with context: `fmt.Errorf("description: %w", err)`.
- Break large functions into smaller, well-named helper functions. Each function should do one thing.
- Keep `main.go` minimal; delegate to `cmd.Execute()`.
- Use `internal/` to keep implementation details private.
- Use `go:embed` for bundled resources (see `internal/embedded/`).
- CLI commands use Cobra. Add new commands by registering them on `rootCmd` in `cmd/root.go`.

## Testing

- Tests must be added or updated when writing new code.
- The project uses Ginkgo v2 and Gomega (BDD style: `Describe`/`Context`/`It`).
- Test files live alongside source files as `*_test.go`.
- Each package with tests needs a `*_suite_test.go` file for Ginkgo bootstrap.
- Use `GinkgoT().TempDir()` for temporary directories in tests.
- Run `make test` to execute all tests (`ginkgo -v ./...`).

## Versioning

The project version is tracked in the shield badge on line 1 of `README.md` (e.g. `![Version: 2.0.0](...)`). Bump it once per session or PR when the CLI binary changes:

- **Patch** (2.0.0 -> 2.0.1): Bug fixes, internal refactors, dependency updates.
- **Minor** (2.0.0 -> 2.1.0): New features, new commands, new flags, or changed behavior.
- **Major**: Ask the user for confirmation before bumping the major version.
- **No bump needed**: Documentation-only changes, CI config, or other changes that do not affect the compiled binary.

Only bump the version once even if multiple code changes are made in the same session.

## Documentation

- Update `README.md` when code changes affect user-facing behavior, CLI usage, or flags.
- Keep the README focused on usage and contribution guidance.

## Verification

Run `make ci` before submitting any change. It runs the full pipeline:

1. Lint (gofmt, go vet, golangci-lint with gocritic, misspell, and revive)
2. Build
3. Test (all Ginkgo suites)
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
4 changes: 1 addition & 3 deletions src/cmd/cmd_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,7 @@ func verifyClusterGroupValues(valuesFile string, expectedClusterGroupValues *typ
}

func createTestDir() string {
dir, err := os.MkdirTemp("", "patternizer-test")
Expect(err).NotTo(HaveOccurred())
return dir
return GinkgoT().TempDir()
}

func runCLI(dir string, args ...string) *gexec.Session {
Expand Down
4 changes: 0 additions & 4 deletions src/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,21 @@ import (

// runInit handles the initialization logic for the init command.
func runInit(withSecrets bool) error {
// Get pattern name and repository root
patternName, repoRoot, err := pattern.GetPatternNameAndRepoRoot()
if err != nil {
return fmt.Errorf("error getting pattern information: %w", err)
}

// Find Helm charts in the repository
chartPaths, err := helm.FindTopLevelCharts(repoRoot)
if err != nil {
return fmt.Errorf("error finding Helm charts: %w", err)
}

// Process values-global.yaml
actualPatternName, clusterGroupName, err := pattern.ProcessGlobalValues(patternName, repoRoot, withSecrets)
if err != nil {
return fmt.Errorf("error processing global values: %w", err)
}

// Process cluster group values using the actual pattern name and cluster group name from the global values
if err := pattern.ProcessClusterGroupValues(actualPatternName, clusterGroupName, repoRoot, chartPaths, withSecrets); err != nil {
return fmt.Errorf("error processing cluster group values: %w", err)
}
Expand Down
44 changes: 0 additions & 44 deletions src/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@ var _ = Describe("patternizer init", func() {
_ = runCLI(tempDir, "init")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -118,10 +114,6 @@ var _ = Describe("patternizer init", func() {
_ = runCLI(tempDir, "init")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -182,10 +174,6 @@ var _ = Describe("patternizer init", func() {
_ = runCLI(tempDir, "init")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -235,10 +223,6 @@ var _ = Describe("patternizer init", func() {
_ = runCLI(tempDir, "init")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -297,10 +281,6 @@ var _ = Describe("patternizer init", func() {
_ = runCLI(tempDir, "init")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should respect the explicit singleArgoCD override", func() {
globalValuesFile := filepath.Join(tempDir, "values-global.yaml")
expectedGlobalValues := types.ValuesGlobal{
Expand Down Expand Up @@ -333,10 +313,6 @@ var _ = Describe("patternizer init --with-secrets", func() {
_ = runCLI(tempDir, "init", "--with-secrets")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -419,10 +395,6 @@ var _ = Describe("patternizer init --with-secrets", func() {
_ = runCLI(tempDir, "init", "--with-secrets")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -515,10 +487,6 @@ var _ = Describe("patternizer init --with-secrets", func() {
_ = runCLI(tempDir, "init", "--with-secrets")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -602,10 +570,6 @@ var _ = Describe("patternizer init --with-secrets", func() {
_ = runCLI(tempDir, "init", "--with-secrets")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the secrets template file", func() {
verifySecretTemplateCopied(tempDir)
})
Expand Down Expand Up @@ -695,10 +659,6 @@ var _ = Describe("patternizer init --with-secrets", func() {
_ = runCLI(tempDir, "init", "--with-secrets")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common pattern scaffold files", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down Expand Up @@ -789,10 +749,6 @@ var _ = Describe("patternizer init --with-secrets", func() {
_ = runCLI(tempDir, "init", "--with-secrets")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should not modify the secrets template file", func() {
actual, err := os.ReadFile(filepath.Join(tempDir, "values-secret.yaml.template"))
Expect(err).NotTo(HaveOccurred())
Expand Down
1 change: 0 additions & 1 deletion src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ for a validated pattern, including values-global.yaml and values-<clustergroup>.
When --with-secrets is specified, it also copies the secrets template and
configures the pattern.sh script for secrets usage.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Check if "help" is passed as an argument
if len(args) > 0 && args[0] == "help" {
return cmd.Help()
}
Expand Down
4 changes: 0 additions & 4 deletions src/cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,18 @@ import (

// runUpgrade handles the upgrade logic for the upgrade command.
func runUpgrade(replaceMakefile bool) error {
// Determine repository root
_, repoRoot, err := pattern.GetPatternNameAndRepoRoot()
if err != nil {
return fmt.Errorf("error getting pattern information: %w", err)
}

// Paths
commonDirPath := filepath.Join(repoRoot, "common")
patternShPath := filepath.Join(repoRoot, "pattern.sh")

// Remove common/ directory if it exists
if err := fileutils.RemovePathIfExists(commonDirPath); err != nil {
return fmt.Errorf("error removing common directory: %w", err)
}

// Remove ./pattern.sh if it exists (symlink or file)
if err := fileutils.RemovePathIfExists(patternShPath); err != nil {
return fmt.Errorf("error removing pattern.sh: %w", err)
}
Expand Down
12 changes: 0 additions & 12 deletions src/cmd/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ var _ = Describe("patternizer upgrade", func() {
_ = runCLI(tempDir, "upgrade")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should copy the common scaffold files (except Makefile)", func() {
verifyPattenShCopied(tempDir)
verifyMakefileCommonCopied(tempDir)
Expand Down Expand Up @@ -92,10 +88,6 @@ var _ = Describe("patternizer upgrade", func() {
_ = runCLI(tempDir, "upgrade")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should not update Makefiles that already include Makefile-common", func() {
f, err := os.ReadFile(filepath.Join(tempDir, "Makefile"))
Expect(err).NotTo(HaveOccurred())
Expand All @@ -115,10 +107,6 @@ var _ = Describe("patternizer upgrade --replace-makefile", func() {
_ = runCLI(tempDir, "upgrade", "--replace-makefile")
})

AfterAll(func() {
os.RemoveAll(tempDir)
})

It("should update the common scaffold files (including Makefile)", func() {
verifyScaffoldFilesCopied(tempDir)
})
Expand Down
4 changes: 4 additions & 0 deletions src/internal/embedded/embedded.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package embedded

import "embed"

// Resources holds the embedded resources directory tree (pattern.sh, Makefile, etc.).
//
//go:embed resources/*
var Resources embed.FS

// Skills holds the embedded skills directory tree for IDE integrations.
//
//go:embed all:skills
var Skills embed.FS
Loading
Loading