code/go/internal/validator/semantic/validate_version_integrity.go (132 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package semantic
import (
"errors"
"fmt"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/elastic/package-spec/v3/code/go/internal/fspath"
"github.com/elastic/package-spec/v3/code/go/internal/pkgpath"
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
)
// ValidateVersionIntegrity returns validation errors if the version defined in manifest isn't referenced in the latest
// entry of the changelog file.
func ValidateVersionIntegrity(fsys fspath.FS) specerrors.ValidationErrors {
manifestVersion, err := readManifestVersion(fsys)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
changelogVersions, err := readChangelogVersions(fsys)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
err = ensureUniqueVersions(changelogVersions)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
err = ensureManifestVersionHasChangelogEntry(manifestVersion, changelogVersions)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
err = ensureChangelogLatestVersionIsGreaterThanOthers(changelogVersions)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
return nil
}
func readManifestVersion(fsys fspath.FS) (string, error) {
manifestPath := "manifest.yml"
f, err := pkgpath.Files(fsys, manifestPath)
if err != nil {
return "", fmt.Errorf("can't locate manifest file: %w", err)
}
if len(f) != 1 {
return "", errors.New("single manifest file expected")
}
val, err := f[0].Values("$.version")
if err != nil {
return "", fmt.Errorf("can't read manifest version: %w", err)
}
sVal, ok := val.(string)
if !ok {
return "", errors.New("version is undefined")
}
return sVal, nil
}
func readChangelogVersions(fsys fspath.FS) ([]string, error) {
return readChangelog(fsys, "$[*].version")
}
func readChangelog(fsys fspath.FS, jsonpath string) ([]string, error) {
changelogPath := "changelog.yml"
f, err := pkgpath.Files(fsys, changelogPath)
if err != nil {
return nil, fmt.Errorf("can't locate changelog file: %w", err)
}
if len(f) != 1 {
return nil, errors.New("single changelog file expected")
}
vals, err := f[0].Values(jsonpath)
if err != nil {
return nil, fmt.Errorf("can't read changelog entries: %w", err)
}
versions, err := toStringSlice(vals)
if err != nil {
return nil, fmt.Errorf("can't convert slice entries: %w", err)
}
return versions, nil
}
func toStringSlice(val interface{}) ([]string, error) {
vals, ok := val.([]interface{})
if !ok {
return nil, errors.New("conversion error")
}
var s []string
for _, v := range vals {
str, ok := v.(string)
if !ok {
return nil, errors.New("conversion error")
}
s = append(s, str)
}
return s, nil
}
func ensureUniqueVersions(versions []string) error {
m := map[string]struct{}{}
for _, v := range versions {
if _, ok := m[v]; ok {
return fmt.Errorf("versions in changelog must be unique, found at least two same versions (%s)", v)
}
m[v] = struct{}{}
}
return nil
}
func ensureManifestVersionHasChangelogEntry(manifestVersion string, versions []string) error {
if len(versions) == 0 {
return errors.New("no versions found in changelog")
}
if manifestVersion == versions[0] {
return nil
}
for _, v := range versions {
// It's allowed to keep additional record with "-next" suffix for changes that will be released in the future.
if v == manifestVersion && strings.HasSuffix(versions[0], "-next") {
return nil
}
}
return errors.New("current manifest version doesn't have changelog entry")
}
func ensureChangelogLatestVersionIsGreaterThanOthers(versions []string) error {
if len(versions) == 0 {
return errors.New("no versions found in changelog")
}
latestVersion, err := semver.NewVersion(versions[0])
if err != nil {
return fmt.Errorf("could not read package manifest version [%s]: %w", versions[0], err)
}
for _, v := range versions[1:] {
changelogVersion, err := semver.NewVersion(v)
if err != nil {
return fmt.Errorf("could not read package manifest version [%s]: %w", changelogVersion, err)
}
if changelogVersion.GreaterThanEqual(latestVersion) {
return fmt.Errorf("changelog entry %s is greater than or equal to first changelog entry: %s", changelogVersion, latestVersion)
}
}
return nil
}