---
phase: 04-code-quality
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - internal/deployer/walk.go
  - internal/deployer/challenge.go
  - internal/deployer/local.go
autonomous: true
requirements: [QUAL-02]

must_haves:
  truths:
    - "The walk-and-apply loop body exists in exactly one function in internal/deployer/"
    - "challenge.go calls the shared helper instead of its own loop"
    - "local.go calls the shared helper instead of its own loop"
    - "DeployChallenge and DeployLocalChallenge produce identical manifest application behavior"
    - "task test:unit passes"
  artifacts:
    - path: "internal/deployer/walk.go"
      provides: "Shared applyManifestDirs helper function"
      exports: []
    - path: "internal/deployer/challenge.go"
      provides: "DeployChallenge calling applyManifestDirs"
    - path: "internal/deployer/local.go"
      provides: "DeployLocalChallenge calling applyManifestDirs"
  key_links:
    - from: "internal/deployer/challenge.go"
      to: "internal/deployer/walk.go"
      via: "applyManifestDirs(ctx, baseDir, namespace, mapper, dynamicClient)"
      pattern: "applyManifestDirs"
    - from: "internal/deployer/local.go"
      to: "internal/deployer/walk.go"
      via: "applyManifestDirs(ctx, baseDir, namespace, mapper, dynamicClient)"
      pattern: "applyManifestDirs"
---

<objective>
Extract the duplicated walk-and-apply directory traversal logic from challenge.go and local.go into a single shared helper function in a new file internal/deployer/walk.go.

Purpose: The two deployers contain identical WalkDir + ReadFile + ApplyManifest loops over ["manifests","policies"]. One change (e.g. error handling, file ordering) currently requires editing two places.
Output: A new unexported function applyManifestDirs in walk.go; challenge.go and local.go each reduced to a single call to that helper.
</objective>

<execution_context>
@/Users/paul/.claude/get-shit-done/workflows/execute-plan.md
@/Users/paul/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md

@internal/deployer/challenge.go
@internal/deployer/local.go
</context>

<interfaces>
<!-- Duplicated block that must be extracted. Both files contain this pattern: -->

```go
// In challenge.go — baseDir is tmpDir, namespace is slug
dirs := []string{"manifests", "policies"}
for _, dir := range dirs {
    dirPath := filepath.Join(baseDir, dir)
    if _, err := os.Stat(dirPath); os.IsNotExist(err) {
        logger.Debug("Directory '%s' not found in challenge artifact, skipping", dir)
        continue
    }

    var files []string
    err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) {
            files = append(files, path)
        }
        return nil
    })
    if err != nil {
        return fmt.Errorf("failed to walk %s: %w", dir, err)
    }

    for _, f := range files {
        logger.Debug("Applying manifest: %s", f)
        data, err := os.ReadFile(f)
        if err != nil {
            return fmt.Errorf("failed to read manifest %s: %w", f, err)
        }
        if err := kube.ApplyManifest(ctx, data, namespace, mapper, dynamicClient); err != nil {
            return fmt.Errorf("failed to apply manifest %s: %w", filepath.Base(f), err)
        }
    }
}
```

<!-- Proposed shared function signature: -->
```go
// applyManifestDirs walks the "manifests" and "policies" subdirectories under
// baseDir and applies every .yaml/.yml file to the cluster under namespace.
func applyManifestDirs(
    ctx context.Context,
    baseDir string,
    namespace string,
    mapper meta.RESTMapper,
    dynamicClient dynamic.Interface,
) error
```

The only difference between the two call sites is the log message ("challenge artifact" vs "challenge directory") — use a generic message like "directory '%s' not found, skipping" that works for both, or accept an optional label parameter. Simplest: use one generic skip message.
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Create internal/deployer/walk.go with shared helper</name>
  <files>internal/deployer/walk.go</files>
  <action>
Create a new file internal/deployer/walk.go in package deployer. It must contain a single unexported function:

```go
package deployer

import (
    "context"
    "fmt"
    "os"
    "path/filepath"
    "strings"

    "github.com/kubeasy-dev/kubeasy-cli/internal/kube"
    "github.com/kubeasy-dev/kubeasy-cli/internal/logger"
    "k8s.io/apimachinery/pkg/api/meta"
    "k8s.io/client-go/dynamic"
)

// applyManifestDirs walks the "manifests" and "policies" subdirectories of baseDir
// and applies every .yaml/.yml file to the cluster namespace.
func applyManifestDirs(
    ctx context.Context,
    baseDir string,
    namespace string,
    mapper meta.RESTMapper,
    dynamicClient dynamic.Interface,
) error {
    dirs := []string{"manifests", "policies"}
    for _, dir := range dirs {
        dirPath := filepath.Join(baseDir, dir)
        if _, err := os.Stat(dirPath); os.IsNotExist(err) {
            logger.Debug("Directory '%s' not found, skipping", dirPath)
            continue
        }

        var files []string
        if err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
            if err != nil {
                return err
            }
            if !d.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) {
                files = append(files, path)
            }
            return nil
        }); err != nil {
            return fmt.Errorf("failed to walk %s: %w", dir, err)
        }

        for _, f := range files {
            logger.Debug("Applying manifest: %s", f)
            data, err := os.ReadFile(f)
            if err != nil {
                return fmt.Errorf("failed to read manifest %s: %w", f, err)
            }
            if err := kube.ApplyManifest(ctx, data, namespace, mapper, dynamicClient); err != nil {
                return fmt.Errorf("failed to apply manifest %s: %w", filepath.Base(f), err)
            }
        }
    }
    return nil
}
```

Verify imports: k8s.io/apimachinery/pkg/api/meta provides the RESTMapper interface type used by restmapper.NewDiscoveryRESTMapper. Run `go build ./internal/deployer/...` to confirm the new file compiles.
  </action>
  <verify>
    <automated>cd /Users/paul/Workspace/kubeasy/kubeasy-cli && go build ./internal/deployer/...</automated>
  </verify>
  <done>internal/deployer/walk.go exists and compiles cleanly as part of package deployer.</done>
</task>

<task type="auto">
  <name>Task 2: Replace duplicated loops in challenge.go and local.go</name>
  <files>internal/deployer/challenge.go, internal/deployer/local.go</files>
  <action>
**challenge.go:** Remove the entire walk loop block (lines ~48-81, the `dirs := []string{...}` through the closing `}`) and replace it with a single call:

```go
// Find and apply YAML files from manifests/ and policies/
if err := applyManifestDirs(ctx, tmpDir, slug, mapper, dynamicClient); err != nil {
    return err
}
```

Also remove the now-unused imports `strings` and `path/filepath` only if they are no longer referenced elsewhere in challenge.go. (filepath is still used for `filepath.Join` in the OCI tmpDir logic — keep it. strings was only used in the loop — remove it.)

**local.go:** Remove the entire walk loop block (lines ~29-62) and replace it with a single call:

```go
// Find and apply YAML files from manifests/ and policies/
if err := applyManifestDirs(ctx, challengeDir, namespace, mapper, dynamicClient); err != nil {
    return err
}
```

Remove now-unused imports from local.go: `path/filepath` (was used only in loop — now in walk.go), `strings` (same). Keep `os` only if still used elsewhere; if not, remove it. Do a careful import audit — only remove what is truly unused.

After edits, run `go build ./internal/deployer/...` and `task test:unit` to confirm everything passes.
  </action>
  <verify>
    <automated>cd /Users/paul/Workspace/kubeasy/kubeasy-cli && go build ./... && task test:unit</automated>
  </verify>
  <done>
challenge.go and local.go each contain exactly one call to applyManifestDirs — no WalkDir loop. `go build ./...` exits 0. `task test:unit` passes.
  </done>
</task>

</tasks>

<verification>
```bash
cd /Users/paul/Workspace/kubeasy/kubeasy-cli

# Walk loop exists in exactly one place
grep -rn "filepath.WalkDir" internal/deployer/
# Expected: only internal/deployer/walk.go

# Both deployers call the shared helper
grep -n "applyManifestDirs" internal/deployer/challenge.go internal/deployer/local.go
# Expected: one match in each file

go build ./...
task test:unit
```
</verification>

<success_criteria>
- internal/deployer/walk.go exists with applyManifestDirs function
- challenge.go has no WalkDir loop; calls applyManifestDirs
- local.go has no WalkDir loop; calls applyManifestDirs
- `go build ./...` exits 0
- `task test:unit` passes
</success_criteria>

<output>
After completion, create `.planning/phases/04-code-quality/04-02-SUMMARY.md`
</output>
