Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package helmfile for openSUSE:Factory 
checked in at 2026-06-17 16:22:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/helmfile (Old)
 and      /work/SRC/openSUSE:Factory/.helmfile.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "helmfile"

Wed Jun 17 16:22:23 2026 rev:95 rq:1359885 version:1.5.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/helmfile/helmfile.changes        2026-06-16 
18:30:15.051671844 +0200
+++ /work/SRC/openSUSE:Factory/.helmfile.new.1981/helmfile.changes      
2026-06-17 16:23:08.903183749 +0200
@@ -1,0 +2,9 @@
+Wed Jun 17 05:37:57 UTC 2026 - Manfred Hollstein <[email protected]>
+
+- Update to version 1.5.5:
+  * fix: restore s3:: vhost-style remote source support (#2643) by
+    @yxxhero in #2644
+  * feat: add helm 4 --server-side flag support for diff and bump
+    helm-diff to v3.15.10 by @yxxhero in #2645
+
+-------------------------------------------------------------------

Old:
----
  helmfile-1.5.4.tar.gz

New:
----
  helmfile-1.5.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ helmfile.spec ++++++
--- /var/tmp/diff_new_pack.fdtsfp/_old  2026-06-17 16:23:11.311284478 +0200
+++ /var/tmp/diff_new_pack.fdtsfp/_new  2026-06-17 16:23:11.327285147 +0200
@@ -17,9 +17,9 @@
 #
 
 
-%define git_commit a94fdf4194ed6befc026acc6824767fecfd7cb5f
+%define git_commit bbceb05b314ceef32b631c46b512de08b5482d7d
 Name:           helmfile
-Version:        1.5.4
+Version:        1.5.5
 Release:        0
 Summary:        Deploy Kubernetes Helm Charts
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.fdtsfp/_old  2026-06-17 16:23:11.479291506 +0200
+++ /var/tmp/diff_new_pack.fdtsfp/_new  2026-06-17 16:23:11.483291672 +0200
@@ -5,7 +5,7 @@
     <param name="exclude">.git</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
-    <param name="revision">v1.5.4</param>
+    <param name="revision">v1.5.5</param>
     <param name="changesgenerate">enable</param>
     <param name="changesauthor">[email protected]</param>
   </service>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.fdtsfp/_old  2026-06-17 16:23:11.619297362 +0200
+++ /var/tmp/diff_new_pack.fdtsfp/_new  2026-06-17 16:23:11.639298198 +0200
@@ -1,7 +1,7 @@
 <servicedata>
 <service name="tar_scm">
   <param name="url">https://github.com/helmfile/helmfile.git</param>
-  <param 
name="changesrevision">a94fdf4194ed6befc026acc6824767fecfd7cb5f</param>
+  <param 
name="changesrevision">bbceb05b314ceef32b631c46b512de08b5482d7d</param>
 </service>
 </servicedata>
 

++++++ helmfile-1.5.4.tar.gz -> helmfile-1.5.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/Dockerfile 
new/helmfile-1.5.5/Dockerfile
--- old/helmfile-1.5.4/Dockerfile       2026-06-16 02:24:47.000000000 +0200
+++ new/helmfile-1.5.5/Dockerfile       2026-06-17 03:07:44.000000000 +0200
@@ -95,7 +95,7 @@
     [ "$(age-keygen --version)" = "${AGE_VERSION}" ]
 
 ARG HELM_SECRETS_VERSION="4.7.4"
-RUN helm plugin install https://github.com/databus23/helm-diff --version 
v3.15.9 --verify=false && \
+RUN helm plugin install https://github.com/databus23/helm-diff --version 
v3.15.10 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/Dockerfile.debian-stable-slim 
new/helmfile-1.5.5/Dockerfile.debian-stable-slim
--- old/helmfile-1.5.4/Dockerfile.debian-stable-slim    2026-06-16 
02:24:47.000000000 +0200
+++ new/helmfile-1.5.5/Dockerfile.debian-stable-slim    2026-06-17 
03:07:44.000000000 +0200
@@ -104,7 +104,7 @@
     [ "$(age-keygen --version)" = "${AGE_VERSION}" ]
 
 ARG HELM_SECRETS_VERSION="4.7.4"
-RUN helm plugin install https://github.com/databus23/helm-diff --version 
v3.15.9 --verify=false && \
+RUN helm plugin install https://github.com/databus23/helm-diff --version 
v3.15.10 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/Dockerfile.ubuntu 
new/helmfile-1.5.5/Dockerfile.ubuntu
--- old/helmfile-1.5.4/Dockerfile.ubuntu        2026-06-16 02:24:47.000000000 
+0200
+++ new/helmfile-1.5.5/Dockerfile.ubuntu        2026-06-17 03:07:44.000000000 
+0200
@@ -104,7 +104,7 @@
     [ "$(age-keygen --version)" = "${AGE_VERSION}" ]
 
 ARG HELM_SECRETS_VERSION="4.7.4"
-RUN helm plugin install https://github.com/databus23/helm-diff --version 
v3.15.9 --verify=false && \
+RUN helm plugin install https://github.com/databus23/helm-diff --version 
v3.15.10 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
     helm plugin install 
https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz
 --verify=false && \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/pkg/app/init.go 
new/helmfile-1.5.5/pkg/app/init.go
--- old/helmfile-1.5.4/pkg/app/init.go  2026-06-16 02:24:47.000000000 +0200
+++ new/helmfile-1.5.5/pkg/app/init.go  2026-06-17 03:07:44.000000000 +0200
@@ -19,7 +19,7 @@
 
 const (
        HelmRequiredVersion            = "v3.18.6" // Minimum required version 
(supports Helm 3.x and 4.x)
-       HelmDiffRecommendedVersion     = "v3.15.9"
+       HelmDiffRecommendedVersion     = "v3.15.10"
        HelmRecommendedVersion         = "v4.2.1" // Recommended Helm 4 version
        HelmSecretsRecommendedVersion  = "v4.7.4" // v4.7.0+ works with both 
Helm 3 (single plugin) and Helm 4 (split plugin architecture)
        HelmGitRecommendedVersion      = "v1.3.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/pkg/remote/remote.go 
new/helmfile-1.5.5/pkg/remote/remote.go
--- old/helmfile-1.5.4/pkg/remote/remote.go     2026-06-16 02:24:47.000000000 
+0200
+++ new/helmfile-1.5.5/pkg/remote/remote.go     2026-06-17 03:07:44.000000000 
+0200
@@ -6,7 +6,6 @@
        "encoding/hex"
        "errors"
        "fmt"
-       "io"
        "maps"
        "net/http"
        neturl "net/url"
@@ -330,8 +329,26 @@
 
                switch {
                case u.Getter == "normal" && u.Scheme == "s3":
-                       err := r.S3Getter.Get(r.Home, path, cacheDirPath)
-                       if err != nil {
+                       if err := r.S3Getter.Get(r.Home, path, cacheDirPath); 
err != nil {
+                               rmerr := os.RemoveAll(cacheDirPath)
+                               if rmerr != nil {
+                                       return "", errors.Join(err, rmerr)
+                               }
+                               return "", err
+                       }
+               case u.Getter == "s3":
+                       // go-getter forced-getter syntax (e.g. 
"s3::https://bucket.s3.region.amazonaws.com/key";).
+                       // go-getter v2 no longer ships an S3 getter, so route 
these to the
+                       // built-in AWS SDK v2 S3Getter which understands 
vhost/path-style URLs.
+                       // helmfile's Parse splits a "@<file>" selector (if 
any) into u.File;
+                       // strip it from the source so the S3 object key is 
derived from the
+                       // URL path only, matching how the go-getter branch 
feeds u.Dir.
+                       s3Src := stripSubdirSelector(path)
+                       if err := r.S3Getter.Get(r.Home, s3Src, cacheDirPath); 
err != nil {
+                               rmerr := os.RemoveAll(cacheDirPath)
+                               if rmerr != nil {
+                                       return "", errors.Join(err, rmerr)
+                               }
                                return "", err
                        }
                case u.Getter == "normal" && (u.Scheme == "https" || u.Scheme 
== "http"):
@@ -393,21 +410,21 @@
 }
 
 func (g *S3Getter) Get(wd, src, dst string) error {
-       u, err := url.Parse(src)
+       region, bucket, key, err := ParseS3Url(src)
        if err != nil {
                return err
        }
-       file := path.Base(u.Path)
+       file := path.Base(key)
        targetFilePath := filepath.Join(dst, file)
 
-       region, err := g.S3FileExists(src)
+       // If the region could not be derived from the URL, S3FileExists 
resolves it
+       // via GetBucketLocation.
+       resolvedRegion, err := g.S3FileExists(src, region)
        if err != nil {
                return err
        }
-
-       bucket, key, err := ParseS3Url(src)
-       if err != nil {
-               return err
+       if resolvedRegion != "" {
+               region = resolvedRegion
        }
 
        err = os.MkdirAll(dst, os.FileMode(0700))
@@ -441,18 +458,34 @@
                Key:    &key,
        }
        resp, err := s3Client.GetObject(context.TODO(), getObjectInput)
-       defer func(Body io.ReadCloser) {
-               err := Body.Close()
-               if err != nil {
+       if err != nil {
+               return err
+       }
+       defer func() {
+               if err := resp.Body.Close(); err != nil {
                        g.Logger.Errorf("Error closing connection to remote 
data source \n%v", err)
                }
-       }(resp.Body)
+       }()
 
-       if err != nil {
-               return err
+       // go-getter v2 no longer ships an S3 getter, but it still ships archive
+       // decompressors. To preserve go-getter v1's automatic archive 
extraction
+       // (used with the "@<file>" selector to reference a file inside a 
tarball),
+       // download archives to a temp file and decompress them into dst.
+       decompressor := decompressorForFile(file)
+       downloadPath := targetFilePath
+       if decompressor != nil {
+               tmp, terr := os.CreateTemp(dst, ".s3-archive-*")
+               if terr != nil {
+                       return terr
+               }
+               if cerr := tmp.Close(); cerr != nil {
+                       return cerr
+               }
+               downloadPath = tmp.Name()
+               defer func() { _ = os.Remove(downloadPath) }()
        }
 
-       localFile, err := os.Create(targetFilePath)
+       localFile, err := os.Create(downloadPath)
        if err != nil {
                return err
        }
@@ -464,8 +497,35 @@
        }(localFile)
 
        _, err = localFile.ReadFrom(resp.Body)
+       if err != nil {
+               return err
+       }
 
-       return err
+       if decompressor != nil {
+               if err := decompressor.Decompress(dst, downloadPath, true, 
os.FileMode(0)); err != nil {
+                       return fmt.Errorf("decompress %s: %w", file, err)
+               }
+       }
+
+       return nil
+}
+
+// decompressorForFile returns the go-getter decompressor matching the file's
+// archive extension (e.g. ".tar.gz", ".zip"), or nil if the file is not a
+// recognized archive. The detection mirrors go-getter's own suffix matching.
+func decompressorForFile(file string) getter.Decompressor {
+       matchLen := 0
+       var match string
+       for k := range getter.Decompressors {
+               if strings.HasSuffix(file, "."+k) && len(k) > matchLen {
+                       match = k
+                       matchLen = len(k)
+               }
+       }
+       if match == "" {
+               return nil
+       }
+       return getter.Decompressors[match]
 }
 
 func (g *HttpGetter) Get(wd, src, dst string) error {
@@ -516,44 +576,47 @@
        return err
 }
 
-func (g *S3Getter) S3FileExists(path string) (string, error) {
+func (g *S3Getter) S3FileExists(path, regionHint string) (string, error) {
        g.Logger.Debugf("Parsing S3 URL %s", path)
-       bucket, key, err := ParseS3Url(path)
+       _, bucket, key, err := ParseS3Url(path)
        if err != nil {
                return "", err
        }
 
-       // Region
-       g.Logger.Debugf("Creating config for determining S3 region %s", path)
-       // Suppress AWS SDK debug logging by default to prevent sensitive 
information from being logged
-       // Can be configured via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
-       // See issue #2270
-       var configOpts []func(*config.LoadOptions) error
-       if awsSDKLogLevel == "off" {
-               // ClientLogMode(0) disables all AWS SDK logging (no 
LogRequest, LogResponse, etc.)
-               configOpts = append(configOpts, config.WithClientLogMode(0))
-       }
-       cfg, err := config.LoadDefaultConfig(context.TODO(), configOpts...)
-       if err != nil {
-               return "", err
-       }
+       bucketRegion := regionHint
+       if bucketRegion == "" {
+               // Region
+               g.Logger.Debugf("Creating config for determining S3 region %s", 
path)
+               // Suppress AWS SDK debug logging by default to prevent 
sensitive information from being logged
+               // Can be configured via HELMFILE_AWS_SDK_LOG_LEVEL environment 
variable
+               // See issue #2270
+               var configOpts []func(*config.LoadOptions) error
+               if awsSDKLogLevel == "off" {
+                       // ClientLogMode(0) disables all AWS SDK logging (no 
LogRequest, LogResponse, etc.)
+                       configOpts = append(configOpts, 
config.WithClientLogMode(0))
+               }
+               cfg, err := config.LoadDefaultConfig(context.TODO(), 
configOpts...)
+               if err != nil {
+                       return "", err
+               }
 
-       g.Logger.Debugf("Getting bucket %s location %s", bucket, path)
-       s3Client := s3.NewFromConfig(cfg)
-       bucketRegion := "us-east-1"
-       getBucketLocationInput := &s3.GetBucketLocationInput{
-               Bucket: &bucket,
-       }
-       resp, err := s3Client.GetBucketLocation(context.TODO(), 
getBucketLocationInput)
-       if err != nil {
-               return "", fmt.Errorf("failed to retrieve bucket location: %v", 
err)
-       }
-       if resp == nil || string(resp.LocationConstraint) == "" {
-               g.Logger.Debugf("Bucket has no location Assuming us-east-1")
-       } else {
-               bucketRegion = string(resp.LocationConstraint)
+               g.Logger.Debugf("Getting bucket %s location %s", bucket, path)
+               s3Client := s3.NewFromConfig(cfg)
+               bucketRegion = "us-east-1"
+               getBucketLocationInput := &s3.GetBucketLocationInput{
+                       Bucket: &bucket,
+               }
+               resp, err := s3Client.GetBucketLocation(context.TODO(), 
getBucketLocationInput)
+               if err != nil {
+                       return "", fmt.Errorf("failed to retrieve bucket 
location: %v", err)
+               }
+               if resp == nil || string(resp.LocationConstraint) == "" {
+                       g.Logger.Debugf("Bucket has no location Assuming 
us-east-1")
+               } else {
+                       bucketRegion = string(resp.LocationConstraint)
+               }
+               g.Logger.Debugf("Got bucket location %s", bucketRegion)
        }
-       g.Logger.Debugf("Got bucket location %s", bucketRegion)
 
        // File existence
        g.Logger.Debugf("Creating new config with region to see if file exists")
@@ -573,7 +636,7 @@
                return bucketRegion, err
        }
        g.Logger.Debugf("Creating new s3 client to check if object exists")
-       s3Client = s3.NewFromConfig(regionCfg)
+       s3Client := s3.NewFromConfig(regionCfg)
        headObjectInput := &s3.HeadObjectInput{
                Bucket: &bucket,
                Key:    &key,
@@ -596,20 +659,116 @@
        return err
 }
 
-func ParseS3Url(s3URL string) (string, string, error) {
-       parsedURL, err := url.Parse(s3URL)
-       if err != nil {
-               return "", "", fmt.Errorf("failed to parse S3 URL: %w", err)
-       }
-
-       if parsedURL.Scheme != "s3" {
-               return "", "", fmt.Errorf("invalid URL scheme (expected 's3')")
+// stripSubdirSelector removes the helmfile "@<file>" selector from a remote
+// source URL, preserving any query string.
+//
+// helmfile's Parse splits the URL path on "@" into the download source (before
+// "@") and the file selector (after "@") — see the comment in Parse. The
+// selector lives in the path component only, so a "@" inside the query string
+// is left intact. When the path contains no "@" (single-file downloads) the 
URL
+// is returned unchanged.
+func stripSubdirSelector(src string) string {
+       pathPart := src
+       queryPart := ""
+       if q := strings.Index(src, "?"); q >= 0 {
+               pathPart = src[:q]
+               queryPart = src[q:]
+       }
+       // Mirror Parse: only treat a single "@" as the dir/file separator.
+       if parts := strings.Split(pathPart, "@"); len(parts) == 2 {
+               pathPart = parts[0]
+       }
+       return pathPart + queryPart
+}
+
+// ParseS3Url parses an S3 URL and returns the region, bucket, and object key.
+//
+// Supported URL formats:
+//   - s3://<bucket>/<key>                    (region resolved dynamically via 
GetBucketLocation)
+//   - s3::https://s3.amazonaws.com/<bucket>/<key>
+//   - s3::https://s3-<region>.amazonaws.com/<bucket>/<key>
+//   - s3::https://<bucket>.s3.<region>.amazonaws.com/<key>
+//   - s3::https://<bucket>.s3-<region>.amazonaws.com/<key>
+//   - s3::http://...                         (same amazonaws.com forms over 
plain HTTP)
+//
+// The "s3::" forced-getter prefix (the go-getter syntax for selecting the S3
+// getter with an HTTPS URL) is optional and stripped before parsing.
+func ParseS3Url(s3URL string) (region, bucket, key string, err error) {
+       raw := s3URL
+       // Strip any forced-getter prefix like "s3::" so that vhost-style URLs 
such
+       // as "s3::https://bucket.s3.region.amazonaws.com/key"; parse correctly.
+       if idx := strings.Index(raw, "::"); idx >= 0 {
+               raw = raw[idx+2:]
+       }
+
+       parsedURL, err := url.Parse(raw)
+       if err != nil {
+               return "", "", "", fmt.Errorf("failed to parse S3 URL: %w", err)
+       }
+
+       switch parsedURL.Scheme {
+       case "s3":
+               // Path-style: s3://<bucket>/<key>
+               bucket = parsedURL.Host
+               key = strings.TrimPrefix(parsedURL.Path, "/")
+               // Region is unknown for the bare s3:// form; it is resolved 
later
+               // via GetBucketLocation.
+               return "", bucket, key, nil
+       case "http", "https":
+               // Continue to amazonaws.com vhost/path-style parsing below.
+       default:
+               return "", "", "", fmt.Errorf("invalid URL scheme (expected 
's3', 'http', or 'https'): %s", s3URL)
+       }
+
+       // Amazon S3 supports both virtual-hosted-style and path-style URLs.
+       // See 
https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html
+       if !strings.Contains(parsedURL.Host, "amazonaws.com") {
+               return "", "", "", fmt.Errorf("URL is not a valid S3 URL (host 
must be amazonaws.com): %s", s3URL)
+       }
+
+       hostParts := strings.Split(parsedURL.Host, ".")
+       switch len(hostParts) {
+       case 3:
+               // Path-style: s3.amazonaws.com/<bucket>/<key> or 
s3-<region>.amazonaws.com/<bucket>/<key>
+               region = strings.TrimPrefix(strings.TrimPrefix(hostParts[0], 
"s3-"), "s3")
+               if region == "" {
+                       region = "us-east-1"
+               }
+               pathParts := strings.SplitN(parsedURL.Path, "/", 3)
+               if len(pathParts) < 3 {
+                       return "", "", "", fmt.Errorf("URL is not a valid S3 
URL: %s", s3URL)
+               }
+               bucket = pathParts[1]
+               key = pathParts[2]
+       case 4:
+               // Virtual-hosted-style, dash region: 
<bucket>.s3-<region>.amazonaws.com/<key>
+               region = strings.TrimPrefix(strings.TrimPrefix(hostParts[1], 
"s3-"), "s3")
+               if region == "" {
+                       return "", "", "", fmt.Errorf("URL is not a valid S3 
URL: %s", s3URL)
+               }
+               pathParts := strings.SplitN(parsedURL.Path, "/", 2)
+               if len(pathParts) < 2 {
+                       return "", "", "", fmt.Errorf("URL is not a valid S3 
URL: %s", s3URL)
+               }
+               bucket = hostParts[0]
+               key = pathParts[1]
+       case 5:
+               // Virtual-hosted-style, dot region: 
<bucket>.s3.<region>.amazonaws.com/<key>
+               region = hostParts[2]
+               if region == "" {
+                       return "", "", "", fmt.Errorf("URL is not a valid S3 
URL: %s", s3URL)
+               }
+               pathParts := strings.SplitN(parsedURL.Path, "/", 2)
+               if len(pathParts) < 2 {
+                       return "", "", "", fmt.Errorf("URL is not a valid S3 
URL: %s", s3URL)
+               }
+               bucket = hostParts[0]
+               key = pathParts[1]
+       default:
+               return "", "", "", fmt.Errorf("URL is not a valid S3 URL: %s", 
s3URL)
        }
 
-       bucket := parsedURL.Host
-       key := strings.TrimPrefix(parsedURL.Path, "/")
-
-       return bucket, key, nil
+       return region, bucket, key, nil
 }
 
 func NewRemote(logger *zap.SugaredLogger, homeDir string, fs 
*filesystem.FileSystem) *Remote {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/pkg/remote/remote_test.go 
new/helmfile-1.5.5/pkg/remote/remote_test.go
--- old/helmfile-1.5.4/pkg/remote/remote_test.go        2026-06-16 
02:24:47.000000000 +0200
+++ new/helmfile-1.5.5/pkg/remote/remote_test.go        2026-06-17 
03:07:44.000000000 +0200
@@ -1,14 +1,19 @@
 package remote
 
 import (
+       "archive/tar"
+       "bytes"
+       "compress/gzip"
        "fmt"
        "io"
+       "os"
        "path/filepath"
        "strings"
        "testing"
 
        "github.com/google/go-cmp/cmp"
 
+       "github.com/helmfile/helmfile/pkg/filesystem"
        "github.com/helmfile/helmfile/pkg/helmexec"
        "github.com/helmfile/helmfile/pkg/testhelper"
 )
@@ -357,6 +362,354 @@
                        }
                })
        }
+}
+
+func TestRemote_S3VhostUrl(t *testing.T) {
+       cleanfs := map[string]string{
+               CacheDir(): "",
+       }
+       cachefs := map[string]string{
+               filepath.Join(CacheDir(), 
"https_test-helmfile_s3_eu-north-1_amazonaws_com_test", "test.tar.gz"): "foo: 
bar",
+       }
+
+       testcases := []struct {
+               name           string
+               files          map[string]string
+               expectCacheHit bool
+       }{
+               {name: "not expectCacheHit", files: cleanfs, expectCacheHit: 
false},
+               {name: "expectCacheHit", files: cachefs, expectCacheHit: true},
+       }
+
+       for _, tt := range testcases {
+               t.Run(tt.name, func(t *testing.T) {
+                       testfs := testhelper.NewTestFs(tt.files)
+
+                       hit := true
+
+                       get := func(wd, src, dst string) error {
+                               if wd != CacheDir() {
+                                       return fmt.Errorf("unexpected wd: %s", 
wd)
+                               }
+                               expectedSrc := 
"s3::https://test-helmfile.s3.eu-north-1.amazonaws.com/test/test.tar.gz";
+                               if src != expectedSrc {
+                                       return fmt.Errorf("unexpected src: %s", 
src)
+                               }
+                               expectedDst := filepath.Join(CacheDir(), 
"https_test-helmfile_s3_eu-north-1_amazonaws_com_test")
+                               if dst != expectedDst {
+                                       return fmt.Errorf("unexpected dst: %s", 
dst)
+                               }
+
+                               hit = false
+
+                               return nil
+                       }
+
+                       getter := &testGetter{
+                               get: get,
+                       }
+                       remote := &Remote{
+                               Logger:     helmexec.NewLogger(io.Discard, 
"debug"),
+                               Home:       CacheDir(),
+                               Getter:     getter,
+                               S3Getter:   getter,
+                               HttpGetter: getter,
+                               fs:         testfs.ToFileSystem(),
+                       }
+
+                       // go-getter forced-getter vhost-style S3 URL (see 
issue #2643)
+                       url := 
"s3::https://test-helmfile.s3.eu-north-1.amazonaws.com/test/test.tar.gz";
+                       file, err := remote.Locate(url)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+
+                       expectedFile := filepath.Join(CacheDir(), 
"https_test-helmfile_s3_eu-north-1_amazonaws_com_test", "test.tar.gz")
+                       if file != expectedFile {
+                               t.Errorf("unexpected file located: %s vs 
expected: %s", file, expectedFile)
+                       }
+
+                       if tt.expectCacheHit && !hit {
+                               t.Errorf("unexpected result: unexpected cache 
miss")
+                       }
+                       if !tt.expectCacheHit && hit {
+                               t.Errorf("unexpected result: unexpected cache 
hit")
+                       }
+               })
+       }
+}
+
+// TestRemote_S3VhostUrlWithSelector verifies that the helmfile "@<file>"
+// selector is stripped before the URL is handed to the S3 getter, so the 
object
+// key is derived from the URL path only. (Archive decompression for the
+// selector is handled separately and is out of scope for this routing fix.)
+func TestRemote_S3VhostUrlWithSelector(t *testing.T) {
+       testfs := testhelper.NewTestFs(map[string]string{CacheDir(): ""})
+
+       var gotSrc string
+       get := func(wd, src, dst string) error {
+               gotSrc = src
+               return nil
+       }
+
+       getter := &testGetter{get: get}
+       remote := &Remote{
+               Logger:     helmexec.NewLogger(io.Discard, "debug"),
+               Home:       CacheDir(),
+               Getter:     getter,
+               S3Getter:   getter,
+               HttpGetter: getter,
+               fs:         testfs.ToFileSystem(),
+       }
+
+       url := 
"s3::https://test-helmfile.s3.eu-north-1.amazonaws.com/test/[email protected]";
+       if _, err := remote.Locate(url); err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+
+       wantSrc := 
"s3::https://test-helmfile.s3.eu-north-1.amazonaws.com/test/test.tar.gz";
+       if gotSrc != wantSrc {
+               t.Errorf("selector not stripped: got src=%q, want %q", gotSrc, 
wantSrc)
+       }
+}
+
+// TestRemote_S3VhostUrlErrorCleansCache verifies that a failed S3 download 
does
+// not leave a partial cache directory behind that would be mistaken for a 
cache
+// hit on the next run. It uses the real (on-disk) filesystem so that the
+// os.RemoveAll cleanup and the DirectoryExistsAt cache check share the same
+// store. The failing getter first creates dst, mirroring how the real
+// S3Getter.Get runs os.MkdirAll(dst) before it can fail.
+func TestRemote_S3VhostUrlErrorCleansCache(t *testing.T) {
+       home := t.TempDir()
+
+       calls := 0
+       get := func(wd, src, dst string) error {
+               calls++
+               // Mimic S3Getter.Get which MkdirAll's dst before failing.
+               _ = os.MkdirAll(dst, 0o700)
+               return fmt.Errorf("simulated S3 failure")
+       }
+
+       getter := &testGetter{get: get}
+       remote := &Remote{
+               Logger:     helmexec.NewLogger(io.Discard, "debug"),
+               Home:       home,
+               Getter:     getter,
+               S3Getter:   getter,
+               HttpGetter: getter,
+               fs:         filesystem.DefaultFileSystem(),
+       }
+
+       url := 
"s3::https://test-helmfile.s3.eu-north-1.amazonaws.com/test/test.tar.gz";
+       if _, err := remote.Locate(url); err == nil {
+               t.Fatal("expected error on first Locate")
+       }
+       if _, err := remote.Locate(url); err == nil {
+               t.Fatal("expected error on second Locate")
+       }
+       if calls != 2 {
+               t.Errorf("expected getter called twice (partial cache must not 
be reused), got %d", calls)
+       }
+}
+
+func TestDecompressorForFile(t *testing.T) {
+       testcases := []struct {
+               name    string
+               file    string
+               wantNil bool
+       }{
+               {name: "tar.gz", file: "test.tar.gz", wantNil: false},
+               {name: "tgz", file: "test.tgz", wantNil: false},
+               {name: "zip", file: "test.zip", wantNil: false},
+               {name: "tar", file: "test.tar", wantNil: false},
+               {name: "gz", file: "values.yaml.gz", wantNil: false},
+               {name: "plain yaml", file: "values.yaml", wantNil: true},
+               {name: "plain txt", file: "test.gotmpl", wantNil: true},
+       }
+
+       for _, tt := range testcases {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := decompressorForFile(tt.file)
+                       if (got == nil) != tt.wantNil {
+                               t.Errorf("decompressorForFile(%q) = %v, want 
nil=%v", tt.file, got, tt.wantNil)
+                       }
+               })
+       }
+}
+
+// TestS3GetterArchiveExtraction simulates the post-download step that
+// S3Getter.Get performs for archive objects: download (here: pre-create the
+// archive) then decompress into the cache dir so a "@<file>" selector 
resolves.
+func TestS3GetterArchiveExtraction(t *testing.T) {
+       // Build an in-memory tar.gz containing "test.gotmpl".
+       var archive bytes.Buffer
+       gw := gzip.NewWriter(&archive)
+       tw := tar.NewWriter(gw)
+       contents := []byte("releases:\n  - name: test\n")
+       hdr := &tar.Header{Name: "test.gotmpl", Mode: 0644, Size: 
int64(len(contents))}
+       if err := tw.WriteHeader(hdr); err != nil {
+               t.Fatal(err)
+       }
+       if _, err := tw.Write(contents); err != nil {
+               t.Fatal(err)
+       }
+       tw.Close()
+       gw.Close()
+
+       dst := t.TempDir()
+       archivePath := filepath.Join(dst, ".s3-archive-test")
+       if err := os.WriteFile(archivePath, archive.Bytes(), 0o600); err != nil 
{
+               t.Fatal(err)
+       }
+       defer os.Remove(archivePath)
+
+       dec := decompressorForFile("test.tar.gz")
+       if dec == nil {
+               t.Fatal("expected a decompressor for test.tar.gz")
+       }
+
+       // Mirror S3Getter.Get: decompress the downloaded archive into dst (dir 
mode).
+       if err := dec.Decompress(dst, archivePath, true, os.FileMode(0)); err 
!= nil {
+               t.Fatalf("decompress: %v", err)
+       }
+
+       got, err := os.ReadFile(filepath.Join(dst, "test.gotmpl"))
+       if err != nil {
+               t.Fatalf("expected extracted file test.gotmpl: %v", err)
+       }
+       if string(got) != string(contents) {
+               t.Errorf("extracted content mismatch: got %q", got)
+       }
+}
+
+func TestStripSubdirSelector(t *testing.T) {
+       testcases := []struct {
+               name  string
+               input string
+               want  string
+       }{
+               {name: "no selector", input: 
"s3::https://h.s3.us-east-1.amazonaws.com/k/file.yaml";, want: 
"s3::https://h.s3.us-east-1.amazonaws.com/k/file.yaml"},
+               {name: "selector stripped", input: 
"s3::https://h.s3.us-east-1.amazonaws.com/test/[email protected]";, want: 
"s3::https://h.s3.us-east-1.amazonaws.com/test/test.tar.gz"},
+               {name: "selector stripped with query", input: 
"s3::https://h.s3.us-east-1.amazonaws.com/k/file.yaml@sel?x=1";, want: 
"s3::https://h.s3.us-east-1.amazonaws.com/k/file.yaml?x=1"},
+               {name: "at in query preserved", input: 
"s3::https://h.s3.us-east-1.amazonaws.com/k/file?t=a@b";, want: 
"s3::https://h.s3.us-east-1.amazonaws.com/k/file?t=a@b"},
+               {name: "multiple at is not a selector", input: 
"s3::https://h/k/a@b@c";, want: "s3::https://h/k/a@b@c"},
+       }
+
+       for _, tt := range testcases {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := stripSubdirSelector(tt.input)
+                       if diff := cmp.Diff(tt.want, got); diff != "" {
+                               t.Errorf("stripSubdirSelector mismatch:\n%s", 
diff)
+                       }
+               })
+       }
+}
+
+func TestParseS3Url(t *testing.T) {
+       testcases := []struct {
+               name   string
+               input  string
+               region string
+               bucket string
+               key    string
+               err    string
+       }{
+               {
+                       name:   "s3 path-style",
+                       input:  
"s3://helm-s3-values-example/subdir/values.yaml",
+                       region: "",
+                       bucket: "helm-s3-values-example",
+                       key:    "subdir/values.yaml",
+               },
+               {
+                       name:   "s3 path-style no subdir",
+                       input:  "s3://helm-s3-values-example/values.yaml",
+                       region: "",
+                       bucket: "helm-s3-values-example",
+                       key:    "values.yaml",
+               },
+               {
+                       name:   "vhost dot region",
+                       input:  
"s3::https://test-helmfile.s3.eu-north-1.amazonaws.com/test/test.tar.gz";,
+                       region: "eu-north-1",
+                       bucket: "test-helmfile",
+                       key:    "test/test.tar.gz",
+               },
+               {
+                       name:   "vhost dot region us-east-1",
+                       input:  
"s3::https://mybucket.s3.us-east-1.amazonaws.com/dir/file.txt";,
+                       region: "us-east-1",
+                       bucket: "mybucket",
+                       key:    "dir/file.txt",
+               },
+               {
+                       name:   "vhost dash region",
+                       input:  
"s3::https://mybucket.s3-us-west-2.amazonaws.com/dir/file.txt";,
+                       region: "us-west-2",
+                       bucket: "mybucket",
+                       key:    "dir/file.txt",
+               },
+               {
+                       name:   "path-style s3.amazonaws.com",
+                       input:  
"s3::https://s3.amazonaws.com/mybucket/dir/file.txt";,
+                       region: "us-east-1",
+                       bucket: "mybucket",
+                       key:    "dir/file.txt",
+               },
+               {
+                       name:   "path-style s3-region",
+                       input:  
"s3::https://s3-eu-west-1.amazonaws.com/mybucket/dir/file.txt";,
+                       region: "eu-west-1",
+                       bucket: "mybucket",
+                       key:    "dir/file.txt",
+               },
+               {
+                       name:   "plain http vhost dot region",
+                       input:  
"https://mybucket.s3.us-east-1.amazonaws.com/dir/file.txt";,
+                       region: "us-east-1",
+                       bucket: "mybucket",
+                       key:    "dir/file.txt",
+               },
+               {
+                       name:  "invalid scheme",
+                       input: "ftp://example.com/file.txt";,
+                       err:   "invalid URL scheme (expected 's3', 'http', or 
'https'): ftp://example.com/file.txt";,
+               },
+               {
+                       name:  "non-amazonaws host",
+                       input: "https://example.com/bucket/file.txt";,
+                       err:   "URL is not a valid S3 URL (host must be 
amazonaws.com): https://example.com/bucket/file.txt";,
+               },
+       }
+
+       for _, tt := range testcases {
+               t.Run(tt.name, func(t *testing.T) {
+                       region, bucket, key, err := ParseS3Url(tt.input)
+
+                       var errMsg string
+                       if err != nil {
+                               errMsg = err.Error()
+                       }
+
+                       if diff := cmp.Diff(tt.err, errMsg); diff != "" {
+                               t.Fatalf("Unexpected error:\n%s", diff)
+                       }
+
+                       if tt.err != "" {
+                               return
+                       }
+
+                       if diff := cmp.Diff(tt.region, region); diff != "" {
+                               t.Errorf("Unexpected region:\n%s", diff)
+                       }
+                       if diff := cmp.Diff(tt.bucket, bucket); diff != "" {
+                               t.Errorf("Unexpected bucket:\n%s", diff)
+                       }
+                       if diff := cmp.Diff(tt.key, key); diff != "" {
+                               t.Errorf("Unexpected key:\n%s", diff)
+                       }
+               })
+       }
 }
 
 func TestIsRemote(t *testing.T) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/pkg/state/helmx.go 
new/helmfile-1.5.5/pkg/state/helmx.go
--- old/helmfile-1.5.4/pkg/state/helmx.go       2026-06-16 02:24:47.000000000 
+0200
+++ new/helmfile-1.5.5/pkg/state/helmx.go       2026-06-17 03:07:44.000000000 
+0200
@@ -357,6 +357,21 @@
 // validServerSideValues are the allowed values for the helm 4 --server-side 
flag.
 var validServerSideValues = map[string]struct{}{"true": {}, "false": {}, 
"auto": {}}
 
+// resolveServerSideValue resolves the server-side value following precedence:
+// release-level > CLI flag > helmDefaults. Returns empty string if no value 
is configured.
+func (st *HelmState) resolveServerSideValue(release *ReleaseSpec, serverSide 
string) string {
+       switch {
+       case release.ServerSide != nil && *release.ServerSide != "":
+               return *release.ServerSide
+       case serverSide != "":
+               return serverSide
+       case st.HelmDefaults.ServerSide != nil && *st.HelmDefaults.ServerSide 
!= "":
+               return *st.HelmDefaults.ServerSide
+       default:
+               return ""
+       }
+}
+
 // appendServerSideFlagsForUpgrade appends the helm 4 --server-side flag when 
appropriate.
 // Precedence: release-level > CLI flag > helmDefaults.
 func (st *HelmState) appendServerSideFlagsForUpgrade(flags []string, helm 
helmexec.Interface, release *ReleaseSpec, serverSide string) ([]string, error) {
@@ -372,15 +387,8 @@
                return flags, nil
        }
 
-       var value string
-       switch {
-       case release.ServerSide != nil && *release.ServerSide != "":
-               value = *release.ServerSide
-       case serverSide != "":
-               value = serverSide
-       case st.HelmDefaults.ServerSide != nil && *st.HelmDefaults.ServerSide 
!= "":
-               value = *st.HelmDefaults.ServerSide
-       default:
+       value := st.resolveServerSideValue(release, serverSide)
+       if value == "" {
                return flags, nil
        }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/pkg/state/state.go 
new/helmfile-1.5.5/pkg/state/state.go
--- old/helmfile-1.5.4/pkg/state/state.go       2026-06-16 02:24:47.000000000 
+0200
+++ new/helmfile-1.5.5/pkg/state/state.go       2026-06-17 03:07:44.000000000 
+0200
@@ -4019,7 +4019,7 @@
        }
 
        // append server-side flag
-       flags, err = st.appendServerSideFlagsForUpgrade(flags, helm, release, 
serverSide)
+       flags, err = st.appendServerSideFlagsForDiff(flags, helm, release, 
serverSide, pluginsDir)
        if err != nil {
                return nil, nil, err
        }
@@ -4058,6 +4058,27 @@
        return flags, nil
 }
 
+// appendServerSideFlagsForDiff appends the helm 4 --server-side flag for 
helm-diff.
+// It requires helm-diff plugin version v3.15.10 or later.
+func (st *HelmState) appendServerSideFlagsForDiff(flags []string, helm 
helmexec.Interface, release *ReleaseSpec, serverSide string, pluginsDir string) 
([]string, error) {
+       if helm.IsHelm4() {
+               value := st.resolveServerSideValue(release, serverSide)
+               if value != "" {
+                       diffVersion, err := helmexec.GetPluginVersion("diff", 
pluginsDir)
+                       if err != nil {
+                               return flags, err
+                       }
+                       minVersion, _ := semver.NewVersion("v3.15.10")
+
+                       if diffVersion.LessThan(minVersion) {
+                               return flags, fmt.Errorf("server-side is not 
supported by helm-diff plugin version %s, please use at least v3.15.10", 
diffVersion)
+                       }
+               }
+       }
+
+       return st.appendServerSideFlagsForUpgrade(flags, helm, release, 
serverSide)
+}
+
 func (st *HelmState) appendChartVersionFlags(flags []string, release 
*ReleaseSpec) []string {
        version := release.Version
        // Strip OCI digest from version (digest is handled in chart URL, not 
--version flag)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/helmfile-1.5.4/test/integration/run.sh 
new/helmfile-1.5.5/test/integration/run.sh
--- old/helmfile-1.5.4/test/integration/run.sh  2026-06-16 02:24:47.000000000 
+0200
+++ new/helmfile-1.5.5/test/integration/run.sh  2026-06-17 03:07:44.000000000 
+0200
@@ -27,7 +27,7 @@
 export HELM_HOME="${HELM_DATA_HOME}"
 export HELM_PLUGINS="${HELM_DATA_HOME}/plugins"
 export HELM_CONFIG_HOME="${helm_dir}/config"
-HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.9}"
+HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.10}"
 HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.4.1}"
 HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-4.7.4}"
 export GNUPGHOME="${PWD}/${dir}/.gnupg"

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/helmfile/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.helmfile.new.1981/vendor.tar.gz differ: char 121, 
line 4

Reply via email to