Skip to content

Commit 9fa0ea7

Browse files
authored
Implement changelog builder (#15)
1 parent 1f4f695 commit 9fa0ea7

24 files changed

+577
-1
lines changed

cmd/build.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package cmd
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/elastic/elastic-agent-changelog-tool/internal/changelog"
11+
"github.com/spf13/afero"
12+
"github.com/spf13/cobra"
13+
"github.com/spf13/viper"
14+
)
15+
16+
func BuildCmd(fs afero.Fs) *cobra.Command {
17+
18+
buildCmd := &cobra.Command{
19+
Use: "build",
20+
Long: "Create changelog from fragments",
21+
Args: func(cmd *cobra.Command, args []string) error {
22+
return nil
23+
},
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
filename := viper.GetString("changelog_filename")
26+
src := viper.GetString("fragment_location")
27+
dest := viper.GetString("changelog_destination")
28+
29+
b := changelog.NewBuilder(fs, filename, "8.2.1", src, dest)
30+
31+
if err := b.Build(); err != nil {
32+
return fmt.Errorf("cannot build changelog: %w", err)
33+
}
34+
35+
return nil
36+
},
37+
}
38+
39+
return buildCmd
40+
}

cmd/build_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package cmd_test
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"testing"
11+
"time"
12+
13+
"github.com/elastic/elastic-agent-changelog-tool/cmd"
14+
"github.com/elastic/elastic-agent-changelog-tool/internal/changelog"
15+
"github.com/elastic/elastic-agent-changelog-tool/internal/changelog/fragment"
16+
"github.com/spf13/afero"
17+
"github.com/spf13/viper"
18+
"github.com/stretchr/testify/require"
19+
"gopkg.in/yaml.v3"
20+
)
21+
22+
func TestBuildCmd_default(t *testing.T) {
23+
testFs := afero.NewMemMapFs()
24+
c := fragment.NewCreator(testFs, viper.GetString("fragment_location"))
25+
err := c.Create("foo")
26+
require.Nil(t, err)
27+
// NOTE: sleeping to produce different fragment's timestamps
28+
time.Sleep(1 * time.Second)
29+
err = c.Create("bar")
30+
require.Nil(t, err)
31+
32+
cmd := cmd.BuildCmd(testFs)
33+
34+
b := new(bytes.Buffer)
35+
cmd.SetOut(b)
36+
37+
err = cmd.Execute()
38+
require.Nil(t, err)
39+
40+
content, err := afero.ReadFile(testFs, viper.GetString("changelog_destination"))
41+
require.Nil(t, err)
42+
43+
ch := changelog.Changelog{}
44+
err = yaml.Unmarshal(content, &ch)
45+
require.Nil(t, err)
46+
47+
fmt.Println(ch)
48+
49+
require.Equal(t, "8.2.1", ch.Version)
50+
require.Len(t, ch.Entries, 2)
51+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/spf13/viper v1.10.1
1111
github.com/stretchr/testify v1.7.0
1212
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
13+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
1314
)
1415

1516
require (
@@ -36,5 +37,4 @@ require (
3637
google.golang.org/protobuf v1.27.1 // indirect
3738
gopkg.in/ini.v1 v1.66.2 // indirect
3839
gopkg.in/yaml.v2 v2.4.0 // indirect
39-
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
4040
)

internal/changelog/builder.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package changelog
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"log"
11+
"os"
12+
"path"
13+
14+
"github.com/elastic/elastic-agent-changelog-tool/internal/changelog/fragment"
15+
"github.com/spf13/afero"
16+
"gopkg.in/yaml.v3"
17+
)
18+
19+
type Builder struct {
20+
changelog Changelog
21+
filename string
22+
fs afero.Fs
23+
// src is the source location to gather changelog fragments
24+
src string
25+
// dest is the destination location where the changelog is written to
26+
dest string
27+
}
28+
29+
func NewBuilder(fs afero.Fs, filename, version, src, dest string) *Builder {
30+
return &Builder{
31+
changelog: Changelog{Version: version},
32+
filename: filename,
33+
fs: fs,
34+
src: src,
35+
dest: dest,
36+
}
37+
}
38+
39+
var changelogFilePerm = os.FileMode(0660)
40+
var errNoFragments = errors.New("no fragments found in the source folder")
41+
42+
func (b Builder) Build() error {
43+
log.Printf("building changelog for version: %s\n", b.changelog.Version)
44+
log.Printf("collecting fragments from %s\n", b.src)
45+
46+
var files []string
47+
err := afero.Walk(b.fs, b.src, func(path string, info os.FileInfo, err error) error {
48+
if info, err := b.fs.Stat(path); err == nil && !info.IsDir() {
49+
files = append(files, path)
50+
} else {
51+
return err
52+
}
53+
54+
return nil
55+
})
56+
if err != nil {
57+
return fmt.Errorf("cannot walk path %s: %w", b.src, err)
58+
}
59+
60+
if len(files) == 0 {
61+
return errNoFragments
62+
}
63+
64+
for _, file := range files {
65+
log.Printf("parsing %s", file)
66+
67+
f, err := fragment.Load(b.fs, file)
68+
if err != nil {
69+
return fmt.Errorf("cannot load fragment from file %s: %w", file, err)
70+
}
71+
72+
b.changelog.Entries = append(b.changelog.Entries, EntryFromFragment(f))
73+
}
74+
75+
data, err := yaml.Marshal(&b.changelog)
76+
if err != nil {
77+
return fmt.Errorf("cannot marshall changelog: %w", err)
78+
}
79+
80+
return afero.WriteFile(b.fs, path.Join(b.dest, b.filename), data, changelogFilePerm)
81+
}

internal/changelog/changelog.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package changelog
6+
7+
type Changelog struct {
8+
Version string `yaml:"version"`
9+
Entries []Entry `yaml:"entries"`
10+
}

internal/changelog/entry.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package changelog
6+
7+
import "github.com/elastic/elastic-agent-changelog-tool/internal/changelog/fragment"
8+
9+
type FragmentFileInfo struct {
10+
Name string `yaml:"name"`
11+
Checksum string `yaml:"checksum"`
12+
}
13+
14+
type Entry struct {
15+
Summary string `yaml:"summary"`
16+
Description string `yaml:"description"`
17+
Kind Kind `yaml:"kind"`
18+
LinkedPR int `yaml:"pr"`
19+
LinkedIssue int `yaml:"issue"`
20+
Timestamp int64 `yaml:"timestamp"`
21+
File FragmentFileInfo `yaml:"file"`
22+
}
23+
24+
// EntriesFromFragment returns one or more entries based on the fragment File.
25+
// A single Fragment can contain multiple Changelog entries.
26+
func EntryFromFragment(f fragment.File) Entry {
27+
e := Entry{
28+
Summary: f.Fragment.Summary,
29+
Description: f.Fragment.Description,
30+
Kind: kind2kind(f),
31+
LinkedPR: f.Fragment.Pr,
32+
LinkedIssue: f.Fragment.Issue,
33+
Timestamp: f.Timestamp,
34+
File: FragmentFileInfo{
35+
Name: f.Name,
36+
Checksum: f.Checksum(),
37+
},
38+
}
39+
40+
return e
41+
}
42+
43+
func kind2kind(f fragment.File) Kind {
44+
switch f.Fragment.Kind {
45+
case string(BreakingChange):
46+
return BreakingChange
47+
case string(BugFix):
48+
return BugFix
49+
case string(Deprecation):
50+
return Deprecation
51+
case string(Enhancement):
52+
return Enhancement
53+
case string(Feature):
54+
return Feature
55+
case string(KnownIssue):
56+
return KnownIssue
57+
case string(Security):
58+
return Security
59+
case string(Upgrade):
60+
return Upgrade
61+
case string(Other):
62+
return Other
63+
default:
64+
return Unknown
65+
}
66+
}

internal/changelog/entry_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package changelog_test
6+
7+
import (
8+
"path"
9+
"testing"
10+
11+
"github.com/elastic/elastic-agent-changelog-tool/internal/changelog"
12+
"github.com/elastic/elastic-agent-changelog-tool/internal/changelog/fragment"
13+
"github.com/spf13/afero"
14+
"github.com/spf13/viper"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func loadEntries(t *testing.T, fixture string) (fragment.File, changelog.Entry) {
19+
fs := afero.NewCopyOnWriteFs(afero.NewOsFs(), afero.NewMemMapFs())
20+
21+
viper.Set("fragment_location", "testdata")
22+
23+
f1, err := fragment.Load(fs, path.Join(viper.GetString("fragment_location"), fixture))
24+
require.Nil(t, err)
25+
26+
return f1, changelog.EntryFromFragment(f1)
27+
}
28+
29+
func TestEntriesFromFragment_breaking(t *testing.T) {
30+
fixture := "1648040928-breaking-change.yaml"
31+
f, e := loadEntries(t, fixture)
32+
require.Equal(t, e.File.Name, fixture)
33+
require.Equal(t, e.File.Checksum, f.Checksum())
34+
require.Equal(t, e.Timestamp, f.Timestamp)
35+
require.Equal(t, e.Kind, changelog.BreakingChange)
36+
require.Equal(t, e.Summary, f.Fragment.Summary)
37+
}
38+
39+
func TestEntriesFromFragment_deprecation(t *testing.T) {
40+
fixture := "1648040928-bug-fix.yaml"
41+
f, e := loadEntries(t, fixture)
42+
require.Equal(t, e.File.Name, fixture)
43+
require.Equal(t, e.File.Checksum, f.Checksum())
44+
require.Equal(t, e.Timestamp, f.Timestamp)
45+
require.Equal(t, e.Kind, changelog.BugFix)
46+
require.Equal(t, e.Summary, f.Fragment.Summary)
47+
}
48+
49+
func TestEntriesFromFragment_bugfix(t *testing.T) {
50+
fixture := "1648040928-deprecation.yaml"
51+
f, e := loadEntries(t, fixture)
52+
require.Equal(t, e.File.Name, fixture)
53+
require.Equal(t, e.File.Checksum, f.Checksum())
54+
require.Equal(t, e.Timestamp, f.Timestamp)
55+
require.Equal(t, e.Kind, changelog.Deprecation)
56+
require.Equal(t, e.Summary, f.Fragment.Summary)
57+
}
58+
59+
func TestEntriesFromFragment_enhancement(t *testing.T) {
60+
fixture := "1648040928-enhancement.yaml"
61+
f, e := loadEntries(t, fixture)
62+
require.Equal(t, e.File.Name, fixture)
63+
require.Equal(t, e.File.Checksum, f.Checksum())
64+
require.Equal(t, e.Timestamp, f.Timestamp)
65+
require.Equal(t, e.Kind, changelog.Enhancement)
66+
require.Equal(t, e.Summary, f.Fragment.Summary)
67+
}
68+
69+
func TestEntriesFromFragment_feature(t *testing.T) {
70+
fixture := "1648040928-feature.yaml"
71+
f, e := loadEntries(t, fixture)
72+
require.Equal(t, e.File.Name, fixture)
73+
require.Equal(t, e.File.Checksum, f.Checksum())
74+
require.Equal(t, e.Timestamp, f.Timestamp)
75+
require.Equal(t, e.Kind, changelog.Feature)
76+
require.Equal(t, e.Summary, f.Fragment.Summary)
77+
}
78+
79+
func TestEntriesFromFragment_knownissue(t *testing.T) {
80+
fixture := "1648040928-known-issue.yaml"
81+
f, e := loadEntries(t, fixture)
82+
require.Equal(t, e.File.Name, fixture)
83+
require.Equal(t, e.File.Checksum, f.Checksum())
84+
require.Equal(t, e.Timestamp, f.Timestamp)
85+
require.Equal(t, e.Kind, changelog.KnownIssue)
86+
require.Equal(t, e.Summary, f.Fragment.Summary)
87+
88+
}
89+
func TestEntriesFromFragment_security(t *testing.T) {
90+
fixture := "1648040928-security.yaml"
91+
f, e := loadEntries(t, fixture)
92+
require.Equal(t, e.File.Name, fixture)
93+
require.Equal(t, e.File.Checksum, f.Checksum())
94+
require.Equal(t, e.Timestamp, f.Timestamp)
95+
require.Equal(t, e.Kind, changelog.Security)
96+
require.Equal(t, e.Summary, f.Fragment.Summary)
97+
98+
}
99+
func TestEntriesFromFragment_upgrade(t *testing.T) {
100+
fixture := "1648040928-upgrade.yaml"
101+
f, e := loadEntries(t, fixture)
102+
require.Equal(t, e.File.Name, fixture)
103+
require.Equal(t, e.File.Checksum, f.Checksum())
104+
require.Equal(t, e.Timestamp, f.Timestamp)
105+
require.Equal(t, e.Kind, changelog.Upgrade)
106+
require.Equal(t, e.Summary, f.Fragment.Summary)
107+
108+
}
109+
func TestEntriesFromFragment_other(t *testing.T) {
110+
fixture := "1648040928-other.yaml"
111+
f, e := loadEntries(t, fixture)
112+
require.Equal(t, e.File.Name, fixture)
113+
require.Equal(t, e.File.Checksum, f.Checksum())
114+
require.Equal(t, e.Timestamp, f.Timestamp)
115+
require.Equal(t, e.Kind, changelog.Other)
116+
require.Equal(t, e.Summary, f.Fragment.Summary)
117+
}
118+
119+
func TestEntriesFromFragment_unknown(t *testing.T) {
120+
fixture := "1648040928-unknown.yaml"
121+
f, e := loadEntries(t, fixture)
122+
require.Equal(t, e.File.Name, fixture)
123+
require.Equal(t, e.File.Checksum, f.Checksum())
124+
require.Equal(t, e.Timestamp, f.Timestamp)
125+
require.Equal(t, e.Kind, changelog.Unknown)
126+
require.Equal(t, e.Summary, f.Fragment.Summary)
127+
}

0 commit comments

Comments
 (0)