From cdaefca169dc2020d2540f9c6efee0bf6398c356 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Tue, 23 Jul 2024 22:02:18 -0400 Subject: [PATCH] generate release notes for delta since latest release of each chart Signed-off-by: Kenneth Bingham --- README.md | 8 ++-- go.mod | 1 + go.sum | 2 + pkg/github/github.go | 83 ++++++++++++++++++++++++++++++----- pkg/releaser/releaser.go | 37 +++++++++++++--- pkg/releaser/releaser_test.go | 26 +++++++++++ 6 files changed, 134 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 39af753f..4c0d0249 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ git-upload-url: https://uploads.github.com/ cr upload --config config.yaml ``` -`cr` supports any format [Viper](https://github.com/spf13/viper) can read, i. e. JSON, TOML, YAML, HCL, and Java properties files. +`cr` supports any format [Viper](https://github.com/spf13/viper) can read, i.e. JSON, TOML, YAML, HCL, and Java properties files. Notice that if no config file is specified, `cr.yaml` (or any of the supported formats) is loaded from the current directory, `$HOME/.cr`, or `/etc/cr`, in that order, if found. @@ -279,7 +279,7 @@ and then look for `upload_url`. You need the part of the URL that appears before ## Common Error Messages -During the upload, you can get the follwing error : +During the upload, you can get the following error : ```bash 422 Validation Failed [{Resource:Release Field:tag_name Code:already_exists Message:}] @@ -289,8 +289,8 @@ You can solve it by adding the `--skip-existing` flag to your command. More deta ## Known Bug -Currently, if you set the upload URL incorrectly, let's say to something like `https://example.com/uploads/`, then `cr upload` will appear to work, but the release will not be complete. When everything is working there should be 3 assets in each release, but instead there will only be the 2 source code assets. The third asset, which is what helm actually uses, is missing. This issue will become apparent when you run `cr index` and it always claims that nothing has changed, because it can't find the asset it expects for the release. +Currently, if you set the upload URL incorrectly, let's say to something like `https://example.com/uploads/`, then `cr upload` will appear to work, but the release will not be complete. When everything is working there should be three assets in each release, but instead, there will only be two source code assets. The third asset is missing and is needed by Helm. This issue will become apparent when you run `cr index` and it always claims that nothing has changed, because it can't find the asset it expects for the release. -It appears like the [go-github Do call](https://github.com/google/go-github/blob/master/github/github.go#L520) does not catch the fact that the upload URL is incorrect and pass back the expected error. If the asset upload fails, it would be better if the release was rolled back (deleted) and an appropriate log message is be displayed to the user. +It appears like the [go-github Do call](https://github.com/google/go-github/blob/master/github/github.go#L520) does not catch the fact that the upload URL is incorrect and passes back the expected error. If the asset upload fails, it would be better if the release was rolled back (deleted) and an appropriate log message is displayed to the user. The `cr index` command should also generate a warning when a release has no assets attached to it, to help people detect and troubleshoot this type of problem. diff --git a/go.mod b/go.mod index 7e1de161..48202780 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.22.4 require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/Songmu/retry v0.1.0 + github.com/blang/semver v3.5.1+incompatible github.com/google/go-github/v56 v56.0.0 github.com/magefile/mage v1.15.0 github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index 0dfd0642..ccd13bcc 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= diff --git a/pkg/github/github.go b/pkg/github/github.go index 4143c50d..ff8bbc33 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -25,17 +25,18 @@ import ( "github.com/Songmu/retry" "github.com/pkg/errors" + "github.com/blang/semver" "github.com/google/go-github/v56/github" "golang.org/x/oauth2" ) type Release struct { - Name string - Description string - Assets []*Asset - Commit string - GenerateReleaseNotes bool - MakeLatest string + Name string + Description string + Assets []*Asset + Commit string + MakeLatest string + SemVer semver.Version } type Asset struct { @@ -102,15 +103,73 @@ func (c *Client) GetRelease(_ context.Context, tag string) (*Release, error) { return result, nil } +// GetLatestChartRelease queries the GitHub API for the previous release of a chart +func (c *Client) GetLatestChartRelease(_ context.Context, prefix string) (*Release, error) { + // Append hyphen to prefix unless already present + prefix = strings.TrimSuffix(prefix, "-") + "-" + + // Find all versions with tags matching prefix + opt := &github.ListOptions{ + PerPage: 100, + } + var versions []semver.Version + for { + rels, resp, err := c.Repositories.ListReleases(context.TODO(), c.owner, c.repo, opt) + if err != nil { + return nil, err + } else if len(rels) == 0 { + return nil, errors.New("no releases found") + } + for _, rel := range rels { + if strings.HasPrefix(*rel.TagName, prefix) { + version := semver.MustParse(strings.TrimPrefix(*rel.TagName, prefix)) + versions = append(versions, version) + } + } + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + // Sort versions ascending + semver.Sort(versions) + + // Find highest version + latestVersion := versions[len(versions)-1] + var release *github.RepositoryRelease + if rel, _, err := c.Repositories.GetReleaseByTag(context.TODO(), c.owner, c.repo, prefix+latestVersion.String()); err == nil { + release = rel + } + + result := &Release{ + Name: *release.TagName, + Commit: *release.TargetCommitish, + SemVer: latestVersion, + } + return result, nil +} + +// GenerateReleaseNotes generates the release notes for a release +func (c *Client) GenerateReleaseNotes(_ context.Context, latestRelease *Release, nextRelease string) (string, error) { + notes, _, err := c.Repositories.GenerateReleaseNotes(context.TODO(), c.owner, c.repo, &github.GenerateNotesOptions{ + TagName: nextRelease, + PreviousTagName: &latestRelease.Name, + }) + if err != nil { + return "", err + } + return notes.Body, err +} + // CreateRelease creates a new release object in the GitHub API func (c *Client) CreateRelease(_ context.Context, input *Release) error { req := &github.RepositoryRelease{ - Name: &input.Name, - Body: &input.Description, - TagName: &input.Name, - TargetCommitish: &input.Commit, - GenerateReleaseNotes: &input.GenerateReleaseNotes, - MakeLatest: &input.MakeLatest, + Name: &input.Name, + Body: &input.Description, + TagName: &input.Name, + TargetCommitish: &input.Commit, + MakeLatest: &input.MakeLatest, } release, _, err := c.Repositories.CreateRelease(context.TODO(), c.owner, c.repo, req) diff --git a/pkg/releaser/releaser.go b/pkg/releaser/releaser.go index 439ae818..6297c03c 100644 --- a/pkg/releaser/releaser.go +++ b/pkg/releaser/releaser.go @@ -28,6 +28,7 @@ import ( "time" "github.com/Songmu/retry" + "github.com/blang/semver" "text/template" @@ -50,6 +51,8 @@ type GitHub interface { CreateRelease(ctx context.Context, input *github.Release) error GetRelease(ctx context.Context, tag string) (*github.Release, error) CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error) + GetLatestChartRelease(ctx context.Context, prefix string) (*github.Release, error) + GenerateReleaseNotes(ctx context.Context, latestRelease *github.Release, nextRelease string) (string, error) } type Git interface { @@ -238,16 +241,33 @@ func (r *Releaser) computeReleaseName(chart *chart.Chart) (string, error) { return releaseName, nil } -func (r *Releaser) getReleaseNotes(chart *chart.Chart) string { +func (r *Releaser) getReleaseNotes(chart *chart.Chart) (string, error) { if r.config.ReleaseNotesFile != "" { for _, f := range chart.Files { if f.Name == r.config.ReleaseNotesFile { - return string(f.Data) + return string(f.Data), nil } } fmt.Printf("The release note file %q, is not present in the chart package\n", r.config.ReleaseNotesFile) + } else if r.config.GenerateReleaseNotes { + latestRelease, err := r.github.GetLatestChartRelease(context.TODO(), chart.Metadata.Name) + if err != nil { + return "", errors.Wrapf(err, "failed to get latest release for chart %s", chart.Metadata.Name) + } + nextVersion := semver.MustParse(chart.Metadata.Version) + versions := []semver.Version{nextVersion, latestRelease.SemVer} + semver.Sort(versions) + highest := versions[len(versions)-1] + // skip generating notes if there's already a higher version in GitHub + if nextVersion.String() == highest.String() { + notes, err := r.github.GenerateReleaseNotes(context.TODO(), latestRelease, chart.Metadata.Version) + if err != nil { + return "", errors.Wrapf(err, "failed to generate release notes for chart %s", chart.Metadata.Name) + } + return notes, nil + } } - return chart.Metadata.Description + return chart.Metadata.Description, nil } func (r *Releaser) splitPackageNameAndVersion(pkg string) []string { @@ -307,16 +327,19 @@ func (r *Releaser) CreateReleases() error { if err != nil { return err } + notes, err := r.getReleaseNotes(ch) + if err != nil { + return err + } release := &github.Release{ Name: releaseName, - Description: r.getReleaseNotes(ch), + Description: notes, Assets: []*github.Asset{ {Path: p}, }, - Commit: r.config.Commit, - GenerateReleaseNotes: r.config.GenerateReleaseNotes, - MakeLatest: strconv.FormatBool(r.config.MakeReleaseLatest), + Commit: r.config.Commit, + MakeLatest: strconv.FormatBool(r.config.MakeReleaseLatest), } provFile := fmt.Sprintf("%s.prov", p) if _, err := os.Stat(provFile); err == nil { diff --git a/pkg/releaser/releaser_test.go b/pkg/releaser/releaser_test.go index 55c7841f..56b2cd77 100644 --- a/pkg/releaser/releaser_test.go +++ b/pkg/releaser/releaser_test.go @@ -21,6 +21,7 @@ import ( "path/filepath" "testing" + "github.com/blang/semver" "github.com/helm/chart-releaser/pkg/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -115,6 +116,31 @@ func (f *FakeGitHub) CreatePullRequest(owner string, repo string, message string return "https://github.com/owner/repo/pull/42", nil } +// GetLatestChartRelease queries the GitHub API for the previous release of a chart +func (f *FakeGitHub) GetLatestChartRelease(_ context.Context, prefix string) (*github.Release, error) { + f.Called(prefix) + + result := &github.Release{ + Name: prefix + "-1.2.3", + Commit: "c11eea26f51782a8063ded1085384acb2928fd91", + SemVer: semver.Version{ + Major: 1, + Minor: 2, + Patch: 3, + }, + } + return result, nil +} + +// GenerateReleaseNotes generates the release notes for a release +func (f *FakeGitHub) GenerateReleaseNotes(_ context.Context, latestRelease *github.Release, nextRelease string) (string, error) { + f.Called(latestRelease, nextRelease) + + notes := "# Noted." + + return notes, nil +} + func TestReleaser_UpdateIndexFile(t *testing.T) { indexDir, _ := os.MkdirTemp(".", "index") defer os.RemoveAll(indexDir)