Files
yxxhero 63f589016a Fix 2337 helm4 stale repo indexes (#2369)
* fix: add --force-update flag for Helm 4 to prevent stale repository indexes

Fixes #2337

Problem:
Helmfile with Helm v4 doesn't update repository indexes when adding repos,
leading to stale indexes and errors like:
  "chart matching version not found in example index. (try 'helm repo update')"

This happens because Helm 4 changed behavior compared to Helm 3:
- Helm 3: Always downloads index when running "helm repo add", even if repo exists
- Helm 4: Skips downloading index if repo already exists with same config
  (see: https://github.com/helm/helm/blob/v4.0.4/pkg/cmd/repo_add.go#L200)

Without --force-update, helmfile only works initially because Helm 4
downloads index on fresh repo setup, but subsequent "helmfile repos"
commands result in stale indexes.

Root Cause:
The code only added --force-update for Helm 3.3.2+, but not for Helm 4,
since it was believed to be default behavior in Helm 4. However, Helm 4
requires explicit --force-update flag to update indexes for existing repos.

Solution:
Add --force-update flag for Helm 4 in AddRepo function to ensure
repository indexes are updated even when repository already exists.

Refactoring:
Simplified the conditional logic from nested if statements to a single
readable condition using existing IsVersionAtLeast() helper:
  if !helm.options.DisableForceUpdate &&
     (helm.IsHelm4() || helm.IsVersionAtLeast("3.3.2")) {
    args = append(args, "--force-update")
  }

Changes:
- pkg/helmexec/exec.go: Add --force-update for Helm 4
- pkg/helmexec/exec_test.go: Update test expectations for both Helm 3.3.2+ and Helm 4
- AGENTS.md: Add development guide for the repository

Testing:
- All helmexec package tests pass
- Verified build succeeds
- Tested against Helm 3.2.0 (no force-update)
- Tested against Helm 3.3.2+ (with force-update)
- Tested against Helm 4.0.1 (with force-update)

Signed-off-by: opencode <opencode@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>

* test: update expected output for Helm 4 repo add message

Update integration test expectations to match Helm 4 behavior with --force-update flag.
When --force-update is used, Helm 4 now outputs "has been added to your
repositories" instead of "already exists with the same configuration, skipping",
because it forcibly updates the repository index.

Related to #2337

Signed-off-by: opencode <opencode@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: opencode <opencode@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
2026-01-21 19:55:56 -05:00

5.0 KiB

AGENTS.md

Build and Test Commands

Essential Setup

# Check Go version (requires 1.24.2+)
go version

# Check Helm dependency (required at runtime)
helm version  # Must show version 3.x

# Install gci tool for import formatting
go install github.com/daixiang0/gci@latest

Build Commands

# Standard build
make build

# Direct build
go build -o helmfile .

# Cross-platform builds
make cross

# Build test tools (required for integration tests)
make build-test-tools

Linting and Formatting

# Run go vet (always available)
make check

# Format code with gci
make fmt

# Run golangci-lint
golangci-lint run

Testing Commands

# Run all unit tests
make test

# Run specific test package
go test -v ./pkg/app/...

# Run single test function
go test -v ./pkg/app/... -run TestSpecificFunction

# Run tests with coverage
go test -v ./pkg/... -coverprofile cover.out -race -p=1
go tool cover -func cover.out

# Run integration tests (requires Kubernetes cluster)
make integration

Code Style Guidelines

Imports

Import ordering (enforced by gci):

  1. Standard library (stdlib)
  2. Default (third-party)
  3. Local (github.com/helmfile/helmfile prefix)

Always use aliases for common stdlib packages to avoid conflicts:

import (
    goContext "context"  // Alias to avoid naming conflicts
    goruntime "runtime"
    "fmt"
    "os"

    "github.com/helmfile/helmfile/pkg/app"
)

Formatting

  • Use go fmt for standard formatting
  • Use gci for import organization: gci write --skip-generated -s standard -s default -s 'prefix(github.com/helmfile/helmfile)' .
  • Run make fmt before committing (requires gci installation)

Types

  • Exported types use PascalCase: type App struct
  • Private types use PascalCase: type helmKey struct
  • Interface names should describe behavior: type Interface interface
  • Use any instead of interface{}

Naming Conventions

  • Exported functions/variables: PascalCase
  • Private functions/variables: camelCase
  • Constants: PascalCase
  • Test functions: TestXxx with descriptive names
  • Package names: lowercase, single word when possible
  • Error variables: ErrXxx

Error Handling

  • Use fmt.Errorf with %w for wrapping errors
  • Check errors explicitly; never ignore
  • Use custom error types in pkg/errors/ when appropriate
  • Return error as last return value
if err != nil {
    return fmt.Errorf("failed to parse helm version '%s': %w", version, err)
}

Testing

  • Use testify/assert for assertions: assert.Equal(t, expected, actual)
  • Use testify/require for critical assertions: require.NoError(t, err)
  • Test files: *_test.go
  • Use table-driven tests for multiple scenarios
  • Run tests with -race flag: -race -p=1
  • Use test helper packages: pkg/testutil, pkg/testhelper
func TestFunctionName(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    string
        wantErr bool
    }{
        {
            name:  "valid input",
            input: "test",
            want:  "result",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := FunctionUnderTest(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            assert.Equal(t, tt.want, got)
        })
    }
}

Logging

  • Use zap.SugaredLogger: logger.Infof("message")
  • Logger injected via dependency injection
  • Don't use fmt.Print for production logging

Comments

  • Exported functions must have comments
  • Comments should be complete sentences with capital first letter
  • Use // for single-line comments
  • Use /* */ for package documentation

Linting Configuration (from .golangci.yaml)

Key enabled linters:

  • errcheck: Check unhandled errors
  • staticcheck: Advanced static analysis
  • revive: Fast linter
  • govet: Go vet checks
  • ineffassign: Detect ineffectual assignments
  • misspell: Spell checking
  • unused: Detect unused code

Important thresholds:

  • Max function length: 280 lines
  • Max statements per function: 140
  • Max cognitive complexity: 110
  • Max naked return lines: 50
  • Line length: 120 characters

Structure

  • Package main: Entry point only (main.go)
  • cmd/: CLI commands using cobra
  • pkg/: Core library code organized by domain
  • test/: Integration and E2E tests
  • Use dependency injection for testability
  • Prefer composition over inheritance

Critical Rules

  1. Always handle errors
  2. Run make check before committing
  3. Run golangci-lint run and fix all issues
  4. Write tests for new pkg/ functionality
  5. Update docs/ for user-facing changes
  6. Follow declarative design principles (desired state in config, operational via flags)

Common Issues

  • First build downloads 200+ packages (2-3 minutes)
  • Integration tests require Kubernetes cluster (minikube/kind)
  • Make fmt requires gci installation
  • Use -p=1 for tests to avoid race conditions
  • Always initialize Helm plugins with helmfile init after installation