From bbc2008f587f3bf65c3ce667824718fac10502d5 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Tue, 23 Jul 2024 22:02:18 -0400 Subject: [PATCH 1/2] generate release notes for delta since latest release of each chart Signed-off-by: Kenneth Bingham --- .github/workflows/ci.yaml | 6 +-- .github/workflows/release.yaml | 8 ++-- .goreleaser.yml | 10 ++-- 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 +++++++++++ 9 files changed, 146 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 497631d9..1ef1865a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: '1.22' + go-version-file: ./go.mod check-latest: true - name: Set up QEMU @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: '1.22' + go-version-file: ./go.mod check-latest: true - name: generate docs @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version-file: './go.mod' + go-version-file: ./go.mod check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aef0d0c4..1b6fb06b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,7 +23,7 @@ jobs: - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: '1.22' + go-version-file: ./go.mod check-latest: true - name: Set up QEMU @@ -56,9 +56,9 @@ jobs: - name: Login to registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: - registry: quay.io - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + registry: ${{ vars.OCI_REGISTRY || 'quay.io' }} + username: ${{ secrets.DOCKER_USERNAME || secrets.DOCKER_HUB_API_USER }} + password: ${{ secrets.DOCKER_PASSWORD || secrets.DOCKER_HUB_API_TOKEN }} - name: Run Mage uses: magefile/mage-action@6a5dcb5fe61f43d7c08a98bc3cf9bc63c308c08e # v3.0.0 diff --git a/.goreleaser.yml b/.goreleaser.yml index 148e778f..209c6d15 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -58,7 +58,7 @@ snapshot: dockers: - goos: linux goarch: amd64 - skip_push: "{{ ne .GitURL 'https://github.com/helm/chart-releaser' | toYaml }}" + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" dockerfile: Dockerfile use: buildx image_templates: @@ -77,7 +77,7 @@ dockers: - goos: linux goarch: arm64 - skip_push: "{{ ne .GitURL 'https://github.com/helm/chart-releaser' | toYaml }}" + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" dockerfile: Dockerfile use: buildx image_templates: @@ -97,7 +97,7 @@ dockers: - goos: linux goarch: arm goarm: 7 - skip_push: "{{ ne .GitURL 'https://github.com/helm/chart-releaser' | toYaml }}" + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" dockerfile: Dockerfile use: buildx image_templates: @@ -116,7 +116,7 @@ dockers: - goos: linux goarch: s390x - skip_push: "{{ ne .GitURL 'https://github.com/helm/chart-releaser' | toYaml }}" + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" dockerfile: Dockerfile use: buildx image_templates: @@ -135,7 +135,7 @@ dockers: - goos: linux goarch: ppc64le - skip_push: "{{ ne .GitURL 'https://github.com/helm/chart-releaser' | toYaml }}" + skip_push: "{{ if ne .GitURL \"https://github.com/helm/chart-releaser\" }}true{{ else }}false{{ end }}" dockerfile: Dockerfile use: buildx image_templates: 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) From d9a2daa2651f534082af43b85a4c37c8398af171 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Thu, 25 Jul 2024 19:03:05 -0400 Subject: [PATCH 2/2] unless repo owner is upstream: goreleaser release --skip=docker,homebrew Signed-off-by: Kenneth Bingham --- .github/workflows/release.yaml | 7 ++++--- magefile.go | 9 ++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1b6fb06b..3e35adb7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -54,11 +54,12 @@ jobs: - uses: chainguard-dev/actions/goimports@main - name: Login to registry + if: github.repository_owner == 'helm' uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: - registry: ${{ vars.OCI_REGISTRY || 'quay.io' }} - username: ${{ secrets.DOCKER_USERNAME || secrets.DOCKER_HUB_API_USER }} - password: ${{ secrets.DOCKER_PASSWORD || secrets.DOCKER_HUB_API_TOKEN }} + registry: 'quay.io' + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - name: Run Mage uses: magefile/mage-action@6a5dcb5fe61f43d7c08a98bc3cf9bc63c308c08e # v3.0.0 diff --git a/magefile.go b/magefile.go index 721f8416..ce3a6abb 100644 --- a/magefile.go +++ b/magefile.go @@ -137,5 +137,12 @@ func Release() error { return err } - return sh.RunV("goreleaser", "release", "--clean") + var args []string + args = append(args, "release", "--clean") + + if os.Getenv("GITHUB_REPOSITORY_OWNER") != "helm" { + args = append(args, "--skip=docker,homebrew") + } + + return sh.RunV("goreleaser", args...) }