cmd/changelogger/main.go (176 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"text/template"
"github.com/ghodss/yaml"
"github.com/elastic/cloud-sdk-go/internal/pkg/changelogger"
"github.com/elastic/cloud-sdk-go/pkg/multierror"
)
var description = `
Generates a complete changelog aggregating individual changelog files from a specific folder.
The -changelog-dir flag's value is joined with -version, resulting: ${changelog-dir}/${version}.
`[1:]
type config struct {
// flags
dir string
version string
template string
baseURL string
// device where the result will be written.
out io.Writer
}
type appError struct {
err error
code int
}
func (e *appError) Error() string {
if e == nil || e.err == nil {
return ""
}
return e.err.Error()
}
func main() {
flagSet := flag.NewFlagSet("changelogger", flag.ContinueOnError)
ogUsage := flagSet.Usage
flagSet.Usage = func() {
fmt.Fprintln(flagSet.Output(), description)
ogUsage()
}
// Flag Definition
cfg := config{
out: os.Stdout,
}
flagSet.StringVar(&cfg.dir, "changelog-dir", ".changelog", "path to the changelog directory")
flagSet.StringVar(&cfg.version, "version", "", "version for the changelog being generated. Any 'v' prefix will be stripped")
flagSet.StringVar(&cfg.template, "template", "", "template to generate the resulting changelog")
flagSet.StringVar(&cfg.baseURL, "base-url", "", "base URL to use for each of the changes")
// Flag parsing
if err := flagSet.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// Application wrapper
if err := run(cfg); err != nil {
code := 255
fmt.Fprintln(os.Stderr, err)
if e, ok := err.(*appError); ok {
code = e.code
}
os.Exit(code)
}
}
func run(cfg config) error {
// config Validation
if err := validateConfig(cfg); err != nil {
return &appError{err: err, code: 1}
}
// Template parsing
tplBytes, err := os.ReadFile(cfg.template)
if err != nil {
return &appError{
err: fmt.Errorf("failed opening template file: %w", err),
code: 2,
}
}
funcMap := template.FuncMap{
"GitHubTracker": func(id string) string {
baseURL := strings.TrimSuffix(cfg.baseURL, "/")
return fmt.Sprintf("%s/issues/%s", baseURL, id)
},
"Version": func() string { return cfg.version },
"Env": os.Getenv,
}
tpl, err := template.New("changelog").Funcs(funcMap).Parse(string(tplBytes))
if err != nil {
return &appError{
err: fmt.Errorf("failed parsing template file contents: %w", err),
code: 3,
}
}
// Trim version prefix and walk the path.
cleanVersion := cfg.version
if strings.HasPrefix(cleanVersion, "v") {
cleanVersion = strings.Replace(cleanVersion, "v", "", 1)
}
var changes changelogger.Changes
changeValErr := multierror.NewPrefixed("invalid changelog entries")
dir := filepath.Join(cfg.dir, cleanVersion)
if err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed opening file %s: %w", path, err)
}
var change changelogger.Change
if err := yaml.Unmarshal(b, &change); err != nil {
return fmt.Errorf("failed decoding yaml file %s: %w", path, err)
}
// If there's no reference set in the file, use the file name.
if change.Ref == "" {
change.Ref = strings.Replace(info.Name(),
filepath.Ext(info.Name()), "", 1,
)
}
if !reflect.DeepEqual(change, changelogger.Change{}) {
changes = append(changes, change)
}
if validateErr := change.Validate(info.Name()); validateErr != nil {
changeValErr = changeValErr.Append(validateErr)
}
return nil
}); err != nil {
return &appError{
err: fmt.Errorf("failed walking the specified path: %w", err),
code: 4,
}
}
sort.Sort(changes)
if len(changes) == 0 {
return &appError{
err: fmt.Errorf("folder %s has no changelog files", dir),
code: 5,
}
}
if err := changeValErr.ErrorOrNil(); err != nil {
return &appError{
err: err,
code: 7,
}
}
buf := new(bytes.Buffer)
if err := tpl.Execute(buf, changes); err != nil {
return &appError{
err: fmt.Errorf("failed executing the changelog template: %w", err),
code: 8,
}
}
if _, err := io.Copy(cfg.out, buf); err != nil {
return &appError{
err: fmt.Errorf("failed copying the template output: %w", err),
code: 9,
}
}
return nil
}
func validateConfig(cfg config) error {
merr := multierror.NewPrefixed("invalid flags")
if cfg.version == "" {
merr = merr.Append(errors.New("version cannot be empty"))
}
if cfg.baseURL == "" {
merr = merr.Append(errors.New("base-url cannot be empty"))
}
if cfg.template == "" {
merr = merr.Append(errors.New("template cannot be empty"))
}
if cfg.out == nil {
merr = merr.Append(errors.New("out io.writer cannot be nil"))
}
return merr.ErrorOrNil()
}