Skip to content

Commit 9c5207a

Browse files
authored
Merge pull request #1 from netfoundry/generate-release-notes-since-latest-chart
generate release notes for delta since latest release of each chart
2 parents 61a6b61 + 6fd1fc4 commit 9c5207a

File tree

7 files changed

+137
-26
lines changed

7 files changed

+137
-26
lines changed

.github/workflows/release.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ jobs:
5656
- name: Login to registry
5757
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
5858
with:
59-
registry: quay.io
60-
username: ${{ secrets.DOCKER_USERNAME }}
61-
password: ${{ secrets.DOCKER_PASSWORD }}
59+
registry: ${{ vars.OCI_REGISTRY || 'quay.io' }}
60+
username: ${{ secrets.DOCKER_USERNAME || secrets.DOCKER_HUB_API_USER }}
61+
password: ${{ secrets.DOCKER_PASSWORD || secrets.DOCKER_HUB_API_TOKEN }}
6262

6363
- name: Run Mage
6464
uses: magefile/mage-action@6a5dcb5fe61f43d7c08a98bc3cf9bc63c308c08e # v3.0.0

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ git-upload-url: https://uploads.github.com/
258258
cr upload --config config.yaml
259259
```
260260

261-
`cr` supports any format [Viper](https://github.com/spf13/viper) can read, i. e. JSON, TOML, YAML, HCL, and Java properties files.
261+
`cr` supports any format [Viper](https://github.com/spf13/viper) can read, i.e. JSON, TOML, YAML, HCL, and Java properties files.
262262

263263
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.
264264

@@ -279,7 +279,7 @@ and then look for `upload_url`. You need the part of the URL that appears before
279279

280280
## Common Error Messages
281281

282-
During the upload, you can get the follwing error :
282+
During the upload, you can get the following error :
283283

284284
```bash
285285
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
289289

290290
## Known Bug
291291

292-
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.
292+
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.
293293

294-
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.
294+
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.
295295

296296
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.

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.22.4
77
require (
88
github.com/MakeNowJust/heredoc v1.0.0
99
github.com/Songmu/retry v0.1.0
10+
github.com/blang/semver v3.5.1+incompatible
1011
github.com/google/go-github/v56 v56.0.0
1112
github.com/magefile/mage v1.15.0
1213
github.com/mitchellh/go-homedir v1.1.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
3737
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
3838
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
3939
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
40+
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
41+
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
4042
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
4143
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
4244
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=

pkg/github/github.go

+71-12
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@ import (
2525
"github.com/Songmu/retry"
2626
"github.com/pkg/errors"
2727

28+
"github.com/blang/semver"
2829
"github.com/google/go-github/v56/github"
2930
"golang.org/x/oauth2"
3031
)
3132

3233
type Release struct {
33-
Name string
34-
Description string
35-
Assets []*Asset
36-
Commit string
37-
GenerateReleaseNotes bool
38-
MakeLatest string
34+
Name string
35+
Description string
36+
Assets []*Asset
37+
Commit string
38+
MakeLatest string
39+
SemVer semver.Version
3940
}
4041

4142
type Asset struct {
@@ -102,15 +103,73 @@ func (c *Client) GetRelease(_ context.Context, tag string) (*Release, error) {
102103
return result, nil
103104
}
104105

106+
// GetLatestChartRelease queries the GitHub API for the previous release of a chart
107+
func (c *Client) GetLatestChartRelease(_ context.Context, prefix string) (*Release, error) {
108+
// Append hyphen to prefix unless already present
109+
prefix = strings.TrimSuffix(prefix, "-") + "-"
110+
111+
// Find all versions with tags matching prefix
112+
opt := &github.ListOptions{
113+
PerPage: 100,
114+
}
115+
var versions []semver.Version
116+
for {
117+
rels, resp, err := c.Repositories.ListReleases(context.TODO(), c.owner, c.repo, opt)
118+
if err != nil {
119+
return nil, err
120+
} else if len(rels) == 0 {
121+
return nil, errors.New("no releases found")
122+
}
123+
for _, rel := range rels {
124+
if strings.HasPrefix(*rel.TagName, prefix) {
125+
version := semver.MustParse(strings.TrimPrefix(*rel.TagName, prefix))
126+
versions = append(versions, version)
127+
}
128+
}
129+
if resp.NextPage == 0 {
130+
break
131+
}
132+
opt.Page = resp.NextPage
133+
}
134+
135+
// Sort versions ascending
136+
semver.Sort(versions)
137+
138+
// Find highest version
139+
latestVersion := versions[len(versions)-1]
140+
var release *github.RepositoryRelease
141+
if rel, _, err := c.Repositories.GetReleaseByTag(context.TODO(), c.owner, c.repo, prefix+latestVersion.String()); err == nil {
142+
release = rel
143+
}
144+
145+
result := &Release{
146+
Name: *release.TagName,
147+
Commit: *release.TargetCommitish,
148+
SemVer: latestVersion,
149+
}
150+
return result, nil
151+
}
152+
153+
// GenerateReleaseNotes generates the release notes for a release
154+
func (c *Client) GenerateReleaseNotes(_ context.Context, latestRelease *Release, nextRelease string) (string, error) {
155+
notes, _, err := c.Repositories.GenerateReleaseNotes(context.TODO(), c.owner, c.repo, &github.GenerateNotesOptions{
156+
TagName: nextRelease,
157+
PreviousTagName: &latestRelease.Name,
158+
})
159+
if err != nil {
160+
return "", err
161+
}
162+
return notes.Body, err
163+
}
164+
105165
// CreateRelease creates a new release object in the GitHub API
106166
func (c *Client) CreateRelease(_ context.Context, input *Release) error {
107167
req := &github.RepositoryRelease{
108-
Name: &input.Name,
109-
Body: &input.Description,
110-
TagName: &input.Name,
111-
TargetCommitish: &input.Commit,
112-
GenerateReleaseNotes: &input.GenerateReleaseNotes,
113-
MakeLatest: &input.MakeLatest,
168+
Name: &input.Name,
169+
Body: &input.Description,
170+
TagName: &input.Name,
171+
TargetCommitish: &input.Commit,
172+
MakeLatest: &input.MakeLatest,
114173
}
115174

116175
release, _, err := c.Repositories.CreateRelease(context.TODO(), c.owner, c.repo, req)

pkg/releaser/releaser.go

+30-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"time"
2929

3030
"github.com/Songmu/retry"
31+
"github.com/blang/semver"
3132

3233
"text/template"
3334

@@ -50,6 +51,8 @@ type GitHub interface {
5051
CreateRelease(ctx context.Context, input *github.Release) error
5152
GetRelease(ctx context.Context, tag string) (*github.Release, error)
5253
CreatePullRequest(owner string, repo string, message string, head string, base string) (string, error)
54+
GetLatestChartRelease(ctx context.Context, prefix string) (*github.Release, error)
55+
GenerateReleaseNotes(ctx context.Context, latestRelease *github.Release, nextRelease string) (string, error)
5356
}
5457

5558
type Git interface {
@@ -238,16 +241,33 @@ func (r *Releaser) computeReleaseName(chart *chart.Chart) (string, error) {
238241
return releaseName, nil
239242
}
240243

241-
func (r *Releaser) getReleaseNotes(chart *chart.Chart) string {
244+
func (r *Releaser) getReleaseNotes(chart *chart.Chart) (string, error) {
242245
if r.config.ReleaseNotesFile != "" {
243246
for _, f := range chart.Files {
244247
if f.Name == r.config.ReleaseNotesFile {
245-
return string(f.Data)
248+
return string(f.Data), nil
246249
}
247250
}
248251
fmt.Printf("The release note file %q, is not present in the chart package\n", r.config.ReleaseNotesFile)
252+
} else if r.config.GenerateReleaseNotes {
253+
latestRelease, err := r.github.GetLatestChartRelease(context.TODO(), chart.Metadata.Name)
254+
if err != nil {
255+
return "", errors.Wrapf(err, "failed to get latest release for chart %s", chart.Metadata.Name)
256+
}
257+
nextVersion := semver.MustParse(chart.Metadata.Version)
258+
versions := []semver.Version{nextVersion, latestRelease.SemVer}
259+
semver.Sort(versions)
260+
highest := versions[len(versions)-1]
261+
// skip generating notes if there's already a higher version in GitHub
262+
if nextVersion.String() == highest.String() {
263+
notes, err := r.github.GenerateReleaseNotes(context.TODO(), latestRelease, chart.Metadata.Version)
264+
if err != nil {
265+
return "", errors.Wrapf(err, "failed to generate release notes for chart %s", chart.Metadata.Name)
266+
}
267+
return notes, nil
268+
}
249269
}
250-
return chart.Metadata.Description
270+
return chart.Metadata.Description, nil
251271
}
252272

253273
func (r *Releaser) splitPackageNameAndVersion(pkg string) []string {
@@ -307,16 +327,19 @@ func (r *Releaser) CreateReleases() error {
307327
if err != nil {
308328
return err
309329
}
330+
notes, err := r.getReleaseNotes(ch)
331+
if err != nil {
332+
return err
333+
}
310334

311335
release := &github.Release{
312336
Name: releaseName,
313-
Description: r.getReleaseNotes(ch),
337+
Description: notes,
314338
Assets: []*github.Asset{
315339
{Path: p},
316340
},
317-
Commit: r.config.Commit,
318-
GenerateReleaseNotes: r.config.GenerateReleaseNotes,
319-
MakeLatest: strconv.FormatBool(r.config.MakeReleaseLatest),
341+
Commit: r.config.Commit,
342+
MakeLatest: strconv.FormatBool(r.config.MakeReleaseLatest),
320343
}
321344
provFile := fmt.Sprintf("%s.prov", p)
322345
if _, err := os.Stat(provFile); err == nil {

pkg/releaser/releaser_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"path/filepath"
2222
"testing"
2323

24+
"github.com/blang/semver"
2425
"github.com/helm/chart-releaser/pkg/github"
2526
"github.com/stretchr/testify/assert"
2627
"github.com/stretchr/testify/mock"
@@ -115,6 +116,31 @@ func (f *FakeGitHub) CreatePullRequest(owner string, repo string, message string
115116
return "https://github.com/owner/repo/pull/42", nil
116117
}
117118

119+
// GetLatestChartRelease queries the GitHub API for the previous release of a chart
120+
func (f *FakeGitHub) GetLatestChartRelease(_ context.Context, prefix string) (*github.Release, error) {
121+
f.Called(prefix)
122+
123+
result := &github.Release{
124+
Name: prefix + "-1.2.3",
125+
Commit: "c11eea26f51782a8063ded1085384acb2928fd91",
126+
SemVer: semver.Version{
127+
Major: 1,
128+
Minor: 2,
129+
Patch: 3,
130+
},
131+
}
132+
return result, nil
133+
}
134+
135+
// GenerateReleaseNotes generates the release notes for a release
136+
func (f *FakeGitHub) GenerateReleaseNotes(_ context.Context, latestRelease *github.Release, nextRelease string) (string, error) {
137+
f.Called(latestRelease, nextRelease)
138+
139+
notes := "# Noted."
140+
141+
return notes, nil
142+
}
143+
118144
func TestReleaser_UpdateIndexFile(t *testing.T) {
119145
indexDir, _ := os.MkdirTemp(".", "index")
120146
defer os.RemoveAll(indexDir)

0 commit comments

Comments
 (0)