191 lines
4.9 KiB
Go
191 lines
4.9 KiB
Go
package releaseprep
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var versionPattern = regexp.MustCompile(`const String = "[^"]+"`)
|
|
|
|
type semver struct {
|
|
major int
|
|
minor int
|
|
patch int
|
|
}
|
|
|
|
func Prepare(rootDir, version, releaseDate string) error {
|
|
normalizedVersion := strings.TrimPrefix(strings.TrimSpace(version), "v")
|
|
if normalizedVersion == "" {
|
|
return fmt.Errorf("version must not be empty")
|
|
}
|
|
|
|
releaseDate = strings.TrimSpace(releaseDate)
|
|
if releaseDate == "" {
|
|
return fmt.Errorf("release date must not be empty")
|
|
}
|
|
|
|
if err := updateVersionFile(rootDir, normalizedVersion); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := updateChangelog(rootDir, normalizedVersion, releaseDate); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func RecommendedTag(rootDir string) (string, error) {
|
|
currentVersion, err := readCurrentVersion(rootDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
unreleasedBody, err := readUnreleasedBody(rootDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
parsed, err := parseSemver(currentVersion)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(unreleasedBody, "### Breaking"), strings.Contains(unreleasedBody, "### Removed"):
|
|
parsed.major++
|
|
parsed.minor = 0
|
|
parsed.patch = 0
|
|
case strings.Contains(unreleasedBody, "### Added"):
|
|
parsed.minor++
|
|
parsed.patch = 0
|
|
default:
|
|
parsed.patch++
|
|
}
|
|
|
|
return fmt.Sprintf("v%d.%d.%d", parsed.major, parsed.minor, parsed.patch), nil
|
|
}
|
|
|
|
func updateVersionFile(rootDir, version string) error {
|
|
path := filepath.Join(rootDir, "internal", "homesick", "version", "version.go")
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read version file: %w", err)
|
|
}
|
|
|
|
updated := versionPattern.ReplaceAllString(string(contents), fmt.Sprintf(`const String = %q`, version))
|
|
if updated == string(contents) {
|
|
return fmt.Errorf("version constant not found in %s", path)
|
|
}
|
|
|
|
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
|
return fmt.Errorf("write version file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateChangelog(rootDir, version, releaseDate string) error {
|
|
unreleasedBody, text, afterHeader, nextSectionStart, err := readChangelogState(rootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.TrimSpace(unreleasedBody) == "" {
|
|
return fmt.Errorf("unreleased section is empty")
|
|
}
|
|
|
|
path := filepath.Join(rootDir, "changelog.md")
|
|
newSection := fmt.Sprintf("## [%s] - %s\n", version, releaseDate)
|
|
newSection += "\n" + unreleasedBody
|
|
if !strings.HasSuffix(newSection, "\n") {
|
|
newSection += "\n"
|
|
}
|
|
|
|
updated := text[:afterHeader] + "\n" + newSection + text[nextSectionStart:]
|
|
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
|
return fmt.Errorf("write changelog: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readCurrentVersion(rootDir string) (string, error) {
|
|
path := filepath.Join(rootDir, "internal", "homesick", "version", "version.go")
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read version file: %w", err)
|
|
}
|
|
|
|
match := versionPattern.FindString(string(contents))
|
|
if match == "" {
|
|
return "", fmt.Errorf("version constant not found in %s", path)
|
|
}
|
|
|
|
return strings.TrimSuffix(strings.TrimPrefix(match, `const String = "`), `"`), nil
|
|
}
|
|
|
|
func readUnreleasedBody(rootDir string) (string, error) {
|
|
unreleasedBody, _, _, _, err := readChangelogState(rootDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if strings.TrimSpace(unreleasedBody) == "" {
|
|
return "", fmt.Errorf("unreleased section is empty")
|
|
}
|
|
|
|
return unreleasedBody, nil
|
|
}
|
|
|
|
func readChangelogState(rootDir string) (string, string, int, int, error) {
|
|
path := filepath.Join(rootDir, "changelog.md")
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", "", 0, 0, fmt.Errorf("read changelog: %w", err)
|
|
}
|
|
|
|
text := string(contents)
|
|
unreleasedHeader := "## [Unreleased]\n"
|
|
start := strings.Index(text, unreleasedHeader)
|
|
if start == -1 {
|
|
return "", "", 0, 0, fmt.Errorf("unreleased section not found in changelog")
|
|
}
|
|
|
|
afterHeader := start + len(unreleasedHeader)
|
|
nextSectionRelative := strings.Index(text[afterHeader:], "\n## [")
|
|
if nextSectionRelative == -1 {
|
|
nextSectionRelative = len(text[afterHeader:])
|
|
}
|
|
nextSectionStart := afterHeader + nextSectionRelative
|
|
unreleasedBody := strings.TrimLeft(text[afterHeader:nextSectionStart], "\n")
|
|
|
|
return unreleasedBody, text, afterHeader, nextSectionStart, nil
|
|
}
|
|
|
|
func parseSemver(version string) (semver, error) {
|
|
parts := strings.Split(strings.TrimSpace(version), ".")
|
|
if len(parts) != 3 {
|
|
return semver{}, fmt.Errorf("version %q is not semantic major.minor.patch", version)
|
|
}
|
|
|
|
major, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return semver{}, fmt.Errorf("parse major version: %w", err)
|
|
}
|
|
minor, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return semver{}, fmt.Errorf("parse minor version: %w", err)
|
|
}
|
|
patch, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return semver{}, fmt.Errorf("parse patch version: %w", err)
|
|
}
|
|
|
|
return semver{major: major, minor: minor, patch: patch}, nil
|
|
}
|