diff --git a/pkg/releaser/releaser.go b/pkg/releaser/releaser.go index 03e28ae2..85df9a96 100644 --- a/pkg/releaser/releaser.go +++ b/pkg/releaser/releaser.go @@ -164,6 +164,11 @@ func (r *Releaser) UpdateIndexFile() (bool, error) { for _, asset := range release.Assets { downloadURL, _ := url.Parse(asset.URL) name := filepath.Base(downloadURL.Path) + // Decode URL-encoded characters in the filename (e.g., %2B -> +) + name, err = url.PathUnescape(name) + if err != nil { + return false, errors.Wrapf(err, "error decoding filename from URL: %s", asset.URL) + } // Ignore any other files added in the release by the users. if filepath.Ext(name) != chartAssetFileExtension { continue @@ -255,8 +260,14 @@ func (r *Releaser) splitPackageNameAndVersion(pkg string) []string { return []string{pkg[0:delimIndex], pkg[delimIndex+1:]} } -func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error { - arch := filepath.Join(r.config.PackagePath, filepath.Base(url)) +func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, urlStr string) error { + // Decode URL-encoded characters in the filename (e.g., %2B -> +) + filename := filepath.Base(urlStr) + decodedFilename, err := url.PathUnescape(filename) + if err != nil { + return errors.Wrapf(err, "error decoding filename from URL: %s", urlStr) + } + arch := filepath.Join(r.config.PackagePath, decodedFilename) // extract chart metadata fmt.Printf("Extracting chart metadata from %s\n", arch) @@ -274,7 +285,7 @@ func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error { // remove url name from url as helm's index library // adds it in during .Add // there should be a better way to handle this :( - s := strings.Split(url, "/") + s := strings.Split(urlStr, "/") s = s[:len(s)-1] if r.config.PackagesWithIndex { @@ -283,8 +294,8 @@ func (r *Releaser) addToIndexFile(indexFile *repo.IndexFile, url string) error { s = s[:0] } - // Add to index - return indexFile.MustAdd(c.Metadata, filepath.Base(arch), strings.Join(s, "/"), hash) + // Add to index (using the decoded filename) + return indexFile.MustAdd(c.Metadata, decodedFilename, strings.Join(s, "/"), hash) } // CreateReleases finds and uploads Helm chart packages to GitHub diff --git a/pkg/releaser/releaser_test.go b/pkg/releaser/releaser_test.go index dde79340..6f87c781 100644 --- a/pkg/releaser/releaser_test.go +++ b/pkg/releaser/releaser_test.go @@ -116,9 +116,33 @@ func (f *FakeGitHub) CreatePullRequest(owner string, repo string, message string } func TestReleaser_UpdateIndexFile(t *testing.T) { - indexDir, _ := os.MkdirTemp(".", "index") + indexDir, err := os.MkdirTemp(".", "index") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } defer os.RemoveAll(indexDir) + // Create a separate index directory for the URL encoding test + indexDirWithPlus, err := os.MkdirTemp(".", "index-plus") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(indexDirWithPlus) + + // FakeGitHub that returns URL-encoded asset URLs (e.g., + encoded as %2B) + fakeGitHubURLEncoded := &FakeGitHubWithURLEncoding{ + release: &github.Release{ + Name: "test-chart-0.1.0+build.1", + Description: "A Helm chart with build metadata", + Assets: []*github.Asset{ + { + Path: "testdata/release-packages-plus/test-chart-0.1.0+build.1.tgz", + URL: "https://myrepo/charts/test-chart-0.1.0%2Bbuild.1.tgz", + }, + }, + }, + } + fakeGitHub := new(FakeGitHub) tests := []struct { @@ -177,6 +201,18 @@ func TestReleaser_UpdateIndexFile(t *testing.T) { }, indexFile: "", }, + { + name: "index-file-with-url-encoded-plus-sign", + exists: false, + releaser: &Releaser{ + config: &config.Options{ + IndexPath: filepath.Join(indexDirWithPlus, "index.yaml"), + PackagePath: "testdata/release-packages-plus", + }, + github: fakeGitHubURLEncoded, + }, + indexFile: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -198,6 +234,20 @@ func TestReleaser_UpdateIndexFile(t *testing.T) { } else { _, err := os.Stat(tt.releaser.config.IndexPath) assert.NoError(t, err) + + // Additional verification for URL-encoded case + if tt.name == "index-file-with-url-encoded-plus-sign" { + indexFile, err := repo.LoadIndexFile(tt.releaser.config.IndexPath) + assert.NoError(t, err, "should load index file") + assert.True(t, indexFile.Has("test-chart", "0.1.0+build.1"), + "index should contain chart version with + sign (decoded from %2B)") + + // Verify the chart entry has the correct version with build metadata + chartVersion, err := indexFile.Get("test-chart", "0.1.0+build.1") + assert.NoError(t, err, "should retrieve chart version") + assert.NotNil(t, chartVersion, "chart version should not be nil") + assert.Equal(t, "0.1.0+build.1", chartVersion.Version, "version should preserve + sign") + } } }) } @@ -535,3 +585,19 @@ func TestReleaser_ReleaseNotes(t *testing.T) { }) } } + +type FakeGitHubWithURLEncoding struct { + release *github.Release +} + +func (f *FakeGitHubWithURLEncoding) CreateRelease(ctx context.Context, input *github.Release) error { + return nil +} + +func (f *FakeGitHubWithURLEncoding) GetRelease(ctx context.Context, tag string) (*github.Release, error) { + return f.release, nil +} + +func (f *FakeGitHubWithURLEncoding) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) { + return "https://github.com/owner/repo/pull/42", nil +} diff --git a/pkg/releaser/testdata/release-packages-plus/test-chart-0.1.0+build.1.tgz b/pkg/releaser/testdata/release-packages-plus/test-chart-0.1.0+build.1.tgz new file mode 100644 index 00000000..db1466b1 Binary files /dev/null and b/pkg/releaser/testdata/release-packages-plus/test-chart-0.1.0+build.1.tgz differ