chore(go): inject release service dependencies and mirror local validation
This commit is contained in:
@@ -39,8 +39,31 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: go.sum
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
|
- name: Validate formatting
|
||||||
|
run: test -z "$(gofmt -l .)"
|
||||||
|
|
||||||
|
- name: Module hygiene
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
go mod tidy
|
||||||
|
go mod verify
|
||||||
|
|
||||||
|
- name: Run gosec security analysis
|
||||||
|
uses: securego/gosec@v2
|
||||||
|
with:
|
||||||
|
args: ./...
|
||||||
|
|
||||||
|
- name: Run govulncheck
|
||||||
|
uses: golang/govulncheck-action@v1
|
||||||
|
with:
|
||||||
|
go-package: ./...
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test ./...
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
go test ./...
|
||||||
|
|
||||||
- name: Resolve cache token
|
- name: Resolve cache token
|
||||||
id: cache-token
|
id: cache-token
|
||||||
|
|||||||
@@ -33,6 +33,102 @@ var (
|
|||||||
refLinkLineRe = regexp.MustCompile(`^\[[^\]]+\]: \S`)
|
refLinkLineRe = regexp.MustCompile(`^\[[^\]]+\]: \S`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type fileSystem interface {
|
||||||
|
ReadFile(path string) ([]byte, error)
|
||||||
|
WriteFile(path string, data []byte, perm os.FileMode) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type environment interface {
|
||||||
|
Getenv(key string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitRunner interface {
|
||||||
|
FirstCommitShortHash(rootDir string) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type osFileSystem struct{}
|
||||||
|
|
||||||
|
func (osFileSystem) ReadFile(path string) ([]byte, error) {
|
||||||
|
// #nosec G304 -- This adapter intentionally accepts caller-provided paths so the service can work against repository-relative files.
|
||||||
|
return os.ReadFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (osFileSystem) WriteFile(path string, data []byte, perm os.FileMode) error {
|
||||||
|
return os.WriteFile(path, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
type osEnvironment struct{}
|
||||||
|
|
||||||
|
func (osEnvironment) Getenv(key string) string {
|
||||||
|
return os.Getenv(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandGitRunner struct{}
|
||||||
|
|
||||||
|
func (commandGitRunner) FirstCommitShortHash(rootDir string) (string, bool) {
|
||||||
|
command := exec.Command("git", "-C", rootDir, "rev-list", "--max-parents=0", "--abbrev-commit", "HEAD")
|
||||||
|
output, err := command.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
commit := strings.TrimSpace(string(output))
|
||||||
|
if commit == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(commit, "\n") {
|
||||||
|
commit = strings.SplitN(commit, "\n", 2)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependencies defines the injected collaborators required by Service.
|
||||||
|
type Dependencies struct {
|
||||||
|
FileSystem fileSystem
|
||||||
|
Environment environment
|
||||||
|
Git gitRunner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service coordinates changelog and version file operations using injected dependencies.
|
||||||
|
type Service struct {
|
||||||
|
fileSystem fileSystem
|
||||||
|
environment environment
|
||||||
|
git gitRunner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService validates and wires the dependencies required by the release service.
|
||||||
|
func NewService(deps Dependencies) (*Service, error) {
|
||||||
|
if deps.FileSystem == nil {
|
||||||
|
return nil, fmt.Errorf("file system dependency must not be nil")
|
||||||
|
}
|
||||||
|
if deps.Environment == nil {
|
||||||
|
return nil, fmt.Errorf("environment dependency must not be nil")
|
||||||
|
}
|
||||||
|
if deps.Git == nil {
|
||||||
|
return nil, fmt.Errorf("git runner dependency must not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
fileSystem: deps.FileSystem,
|
||||||
|
environment: deps.Environment,
|
||||||
|
git: deps.Git,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultService() *Service {
|
||||||
|
service, err := NewService(Dependencies{
|
||||||
|
FileSystem: osFileSystem{},
|
||||||
|
Environment: osEnvironment{},
|
||||||
|
Git: commandGitRunner{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// VersionFile is the path to the file that stores the current version,
|
// VersionFile is the path to the file that stores the current version,
|
||||||
// relative to the repository root. When empty, release-version is used.
|
// relative to the repository root. When empty, release-version is used.
|
||||||
@@ -67,6 +163,12 @@ type resolvedOptions struct {
|
|||||||
// when repository metadata can be derived from CI environment variables or the
|
// when repository metadata can be derived from CI environment variables or the
|
||||||
// origin git remote.
|
// origin git remote.
|
||||||
func Prepare(rootDir, version, releaseDate string, options Options) error {
|
func Prepare(rootDir, version, releaseDate string, options Options) error {
|
||||||
|
return defaultService().Prepare(rootDir, version, releaseDate, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare updates version state and promotes the Unreleased changelog notes
|
||||||
|
// into a new release section.
|
||||||
|
func (s *Service) Prepare(rootDir, version, releaseDate string, options Options) error {
|
||||||
normalizedVersion := strings.TrimPrefix(strings.TrimSpace(version), "v")
|
normalizedVersion := strings.TrimPrefix(strings.TrimSpace(version), "v")
|
||||||
if normalizedVersion == "" {
|
if normalizedVersion == "" {
|
||||||
return fmt.Errorf("version must not be empty")
|
return fmt.Errorf("version must not be empty")
|
||||||
@@ -82,11 +184,11 @@ func Prepare(rootDir, version, releaseDate string, options Options) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updateVersionFile(rootDir, normalizedVersion, resolved); err != nil {
|
if err := s.updateVersionFile(rootDir, normalizedVersion, resolved); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updateChangelog(rootDir, normalizedVersion, releaseDate, resolved.Changelog); err != nil {
|
if err := s.updateChangelog(rootDir, normalizedVersion, releaseDate, resolved.Changelog); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +206,11 @@ func Prepare(rootDir, version, releaseDate string, options Options) error {
|
|||||||
// When no previous release is present in the changelog, the first
|
// When no previous release is present in the changelog, the first
|
||||||
// recommendation is always v1.0.0.
|
// recommendation is always v1.0.0.
|
||||||
func RecommendedTag(rootDir string, options Options) (string, error) {
|
func RecommendedTag(rootDir string, options Options) (string, error) {
|
||||||
|
return defaultService().RecommendedTag(rootDir, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendedTag returns the next semantic release tag based on current changelog state.
|
||||||
|
func (s *Service) RecommendedTag(rootDir string, options Options) (string, error) {
|
||||||
resolved, err := resolveOptions(options)
|
resolved, err := resolveOptions(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -112,12 +219,12 @@ func RecommendedTag(rootDir string, options Options) (string, error) {
|
|||||||
var currentVersion string
|
var currentVersion string
|
||||||
isFirstRelease := false
|
isFirstRelease := false
|
||||||
if options.VersionFile != "" {
|
if options.VersionFile != "" {
|
||||||
currentVersion, err = readCurrentVersion(rootDir, resolved)
|
currentVersion, err = s.readCurrentVersion(rootDir, resolved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
version, found, err := readLatestChangelogVersion(rootDir, resolved.Changelog)
|
version, found, err := s.readLatestChangelogVersion(rootDir, resolved.Changelog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -129,7 +236,7 @@ func RecommendedTag(rootDir string, options Options) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unreleasedBody, err := readUnreleasedBody(rootDir, resolved.Changelog)
|
unreleasedBody, err := s.readUnreleasedBody(rootDir, resolved.Changelog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -205,11 +312,15 @@ func resolveOptions(options Options) (resolvedOptions, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateVersionFile(rootDir, version string, options resolvedOptions) error {
|
func updateVersionFile(rootDir, version string, options resolvedOptions) error {
|
||||||
|
return defaultService().updateVersionFile(rootDir, version, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateVersionFile(rootDir, version string, options resolvedOptions) error {
|
||||||
path := filepath.Join(rootDir, options.VersionFile)
|
path := filepath.Join(rootDir, options.VersionFile)
|
||||||
contents, err := os.ReadFile(path)
|
contents, err := s.fileSystem.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return os.WriteFile(path, []byte(version+"\n"), 0o644)
|
return s.fileSystem.WriteFile(path, []byte(version+"\n"), 0o644)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("read version file: %w", err)
|
return fmt.Errorf("read version file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -225,7 +336,7 @@ func updateVersionFile(rootDir, version string, options resolvedOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
if err := s.fileSystem.WriteFile(path, []byte(updated), 0o644); err != nil {
|
||||||
return fmt.Errorf("write version file: %w", err)
|
return fmt.Errorf("write version file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +344,11 @@ func updateVersionFile(rootDir, version string, options resolvedOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateChangelog(rootDir, version, releaseDate, changelogPath string) error {
|
func updateChangelog(rootDir, version, releaseDate, changelogPath string) error {
|
||||||
unreleasedBody, text, afterHeader, nextSectionStart, path, err := readChangelogState(rootDir, changelogPath)
|
return defaultService().updateChangelog(rootDir, version, releaseDate, changelogPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateChangelog(rootDir, version, releaseDate, changelogPath string) error {
|
||||||
|
unreleasedBody, text, afterHeader, nextSectionStart, path, err := s.readChangelogState(rootDir, changelogPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -249,11 +364,11 @@ func updateChangelog(rootDir, version, releaseDate, changelogPath string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
updated := text[:afterHeader] + "\n" + defaultUnreleasedTemplate + "\n" + newSection + text[nextSectionStart:]
|
updated := text[:afterHeader] + "\n" + defaultUnreleasedTemplate + "\n" + newSection + text[nextSectionStart:]
|
||||||
repoURL, ok := deriveRepositoryURL(rootDir)
|
repoURL, ok := s.deriveRepositoryURL(rootDir)
|
||||||
if ok {
|
if ok {
|
||||||
updated = addChangelogLinks(updated, repoURL, rootDir)
|
updated = s.addChangelogLinks(updated, repoURL, rootDir)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(updated), 0o644); err != nil {
|
if err := s.fileSystem.WriteFile(path, []byte(updated), 0o644); err != nil {
|
||||||
return fmt.Errorf("write changelog: %w", err)
|
return fmt.Errorf("write changelog: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,8 +376,12 @@ func updateChangelog(rootDir, version, releaseDate, changelogPath string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readCurrentVersion(rootDir string, options resolvedOptions) (string, error) {
|
func readCurrentVersion(rootDir string, options resolvedOptions) (string, error) {
|
||||||
|
return defaultService().readCurrentVersion(rootDir, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) readCurrentVersion(rootDir string, options resolvedOptions) (string, error) {
|
||||||
path := filepath.Join(rootDir, options.VersionFile)
|
path := filepath.Join(rootDir, options.VersionFile)
|
||||||
contents, err := os.ReadFile(path)
|
contents, err := s.fileSystem.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("read version file: %w", err)
|
return "", fmt.Errorf("read version file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -276,7 +395,11 @@ func readCurrentVersion(rootDir string, options resolvedOptions) (string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readUnreleasedBody(rootDir, changelogPath string) (string, error) {
|
func readUnreleasedBody(rootDir, changelogPath string) (string, error) {
|
||||||
unreleasedBody, _, _, _, _, err := readChangelogState(rootDir, changelogPath)
|
return defaultService().readUnreleasedBody(rootDir, changelogPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) readUnreleasedBody(rootDir, changelogPath string) (string, error) {
|
||||||
|
unreleasedBody, _, _, _, _, err := s.readChangelogState(rootDir, changelogPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -301,8 +424,12 @@ func unreleasedHasEntries(unreleasedBody string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readChangelogState(rootDir, changelogPath string) (string, string, int, int, string, error) {
|
func readChangelogState(rootDir, changelogPath string) (string, string, int, int, string, error) {
|
||||||
|
return defaultService().readChangelogState(rootDir, changelogPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) readChangelogState(rootDir, changelogPath string) (string, string, int, int, string, error) {
|
||||||
path := filepath.Join(rootDir, changelogPath)
|
path := filepath.Join(rootDir, changelogPath)
|
||||||
contents, err := os.ReadFile(path)
|
contents, err := s.fileSystem.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", 0, 0, "", fmt.Errorf("read changelog: %w", err)
|
return "", "", 0, 0, "", fmt.Errorf("read changelog: %w", err)
|
||||||
}
|
}
|
||||||
@@ -325,8 +452,12 @@ func readChangelogState(rootDir, changelogPath string) (string, string, int, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readLatestChangelogVersion(rootDir, changelogPath string) (string, bool, error) {
|
func readLatestChangelogVersion(rootDir, changelogPath string) (string, bool, error) {
|
||||||
|
return defaultService().readLatestChangelogVersion(rootDir, changelogPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) readLatestChangelogVersion(rootDir, changelogPath string) (string, bool, error) {
|
||||||
path := filepath.Join(rootDir, changelogPath)
|
path := filepath.Join(rootDir, changelogPath)
|
||||||
contents, err := os.ReadFile(path)
|
contents, err := s.fileSystem.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, fmt.Errorf("read changelog: %w", err)
|
return "", false, fmt.Errorf("read changelog: %w", err)
|
||||||
}
|
}
|
||||||
@@ -339,9 +470,13 @@ func readLatestChangelogVersion(rootDir, changelogPath string) (string, bool, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deriveRepositoryURL(rootDir string) (string, bool) {
|
func deriveRepositoryURL(rootDir string) (string, bool) {
|
||||||
override := strings.TrimSpace(os.Getenv("VOCIFERATE_REPOSITORY_URL"))
|
return defaultService().deriveRepositoryURL(rootDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) deriveRepositoryURL(rootDir string) (string, bool) {
|
||||||
|
override := strings.TrimSpace(s.environment.Getenv("VOCIFERATE_REPOSITORY_URL"))
|
||||||
if override != "" {
|
if override != "" {
|
||||||
repositoryPath, ok := deriveRepositoryPath(rootDir)
|
repositoryPath, ok := s.deriveRepositoryPath(rootDir)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -350,14 +485,14 @@ func deriveRepositoryURL(rootDir string) (string, bool) {
|
|||||||
return baseURL + "/" + repositoryPath, true
|
return baseURL + "/" + repositoryPath, true
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := strings.TrimSpace(os.Getenv("GITHUB_SERVER_URL"))
|
serverURL := strings.TrimSpace(s.environment.Getenv("GITHUB_SERVER_URL"))
|
||||||
repository := strings.TrimSpace(os.Getenv("GITHUB_REPOSITORY"))
|
repository := strings.TrimSpace(s.environment.Getenv("GITHUB_REPOSITORY"))
|
||||||
if serverURL != "" && repository != "" {
|
if serverURL != "" && repository != "" {
|
||||||
return strings.TrimSuffix(serverURL, "/") + "/" + strings.TrimPrefix(repository, "/"), true
|
return strings.TrimSuffix(serverURL, "/") + "/" + strings.TrimPrefix(repository, "/"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
gitConfigPath := filepath.Join(rootDir, ".git", "config")
|
gitConfigPath := filepath.Join(rootDir, ".git", "config")
|
||||||
contents, err := os.ReadFile(gitConfigPath)
|
contents, err := s.fileSystem.ReadFile(gitConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -376,13 +511,17 @@ func deriveRepositoryURL(rootDir string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deriveRepositoryPath(rootDir string) (string, bool) {
|
func deriveRepositoryPath(rootDir string) (string, bool) {
|
||||||
repository := strings.TrimSpace(os.Getenv("GITHUB_REPOSITORY"))
|
return defaultService().deriveRepositoryPath(rootDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) deriveRepositoryPath(rootDir string) (string, bool) {
|
||||||
|
repository := strings.TrimSpace(s.environment.Getenv("GITHUB_REPOSITORY"))
|
||||||
if repository != "" {
|
if repository != "" {
|
||||||
return strings.TrimPrefix(repository, "/"), true
|
return strings.TrimPrefix(repository, "/"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
gitConfigPath := filepath.Join(rootDir, ".git", "config")
|
gitConfigPath := filepath.Join(rootDir, ".git", "config")
|
||||||
contents, err := os.ReadFile(gitConfigPath)
|
contents, err := s.fileSystem.ReadFile(gitConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -476,6 +615,10 @@ func normalizeRepoURL(remoteURL string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addChangelogLinks(text, repoURL, rootDir string) string {
|
func addChangelogLinks(text, repoURL, rootDir string) string {
|
||||||
|
return defaultService().addChangelogLinks(text, repoURL, rootDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) addChangelogLinks(text, repoURL, rootDir string) string {
|
||||||
repoURL = strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
|
repoURL = strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
|
||||||
if repoURL == "" {
|
if repoURL == "" {
|
||||||
return text
|
return text
|
||||||
@@ -523,7 +666,7 @@ func addChangelogLinks(text, repoURL, rootDir string) string {
|
|||||||
linkDefs = append(linkDefs, fmt.Sprintf("[Unreleased]: %s/src/branch/main", displayRepoURL))
|
linkDefs = append(linkDefs, fmt.Sprintf("[Unreleased]: %s/src/branch/main", displayRepoURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
firstCommitShort, hasFirstCommit := firstCommitShortHash(rootDir)
|
firstCommitShort, hasFirstCommit := s.firstCommitShortHash(rootDir)
|
||||||
for i, version := range releasedVersions {
|
for i, version := range releasedVersions {
|
||||||
if i+1 < len(releasedVersions) {
|
if i+1 < len(releasedVersions) {
|
||||||
previousVersion := releasedVersions[i+1]
|
previousVersion := releasedVersions[i+1]
|
||||||
@@ -554,22 +697,11 @@ func displayURL(url string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func firstCommitShortHash(rootDir string) (string, bool) {
|
func firstCommitShortHash(rootDir string) (string, bool) {
|
||||||
command := exec.Command("git", "-C", rootDir, "rev-list", "--max-parents=0", "--abbrev-commit", "HEAD")
|
return defaultService().firstCommitShortHash(rootDir)
|
||||||
output, err := command.Output()
|
}
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
commit := strings.TrimSpace(string(output))
|
func (s *Service) firstCommitShortHash(rootDir string) (string, bool) {
|
||||||
if commit == "" {
|
return s.git.FirstCommitShortHash(rootDir)
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(commit, "\n") {
|
|
||||||
commit = strings.SplitN(commit, "\n", 2)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return commit, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareURL(repoURL, baseRef, headRef string) string {
|
func compareURL(repoURL, baseRef, headRef string) string {
|
||||||
|
|||||||
@@ -3,9 +3,54 @@ package vociferate
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type stubFileSystem struct {
|
||||||
|
files map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStubFileSystem(files map[string]string) *stubFileSystem {
|
||||||
|
backing := make(map[string][]byte, len(files))
|
||||||
|
for path, contents := range files {
|
||||||
|
backing[path] = []byte(contents)
|
||||||
|
}
|
||||||
|
return &stubFileSystem{files: backing}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *stubFileSystem) ReadFile(path string) ([]byte, error) {
|
||||||
|
contents, ok := fs.files[path]
|
||||||
|
if !ok {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
clone := make([]byte, len(contents))
|
||||||
|
copy(clone, contents)
|
||||||
|
return clone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *stubFileSystem) WriteFile(path string, data []byte, _ os.FileMode) error {
|
||||||
|
clone := make([]byte, len(data))
|
||||||
|
copy(clone, data)
|
||||||
|
fs.files[path] = clone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubEnvironment map[string]string
|
||||||
|
|
||||||
|
func (env stubEnvironment) Getenv(key string) string {
|
||||||
|
return env[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubGitRunner struct {
|
||||||
|
commit string
|
||||||
|
ok bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner stubGitRunner) FirstCommitShortHash(_ string) (string, bool) {
|
||||||
|
return runner.commit, runner.ok
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeRepoURL(t *testing.T) {
|
func TestNormalizeRepoURL(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -159,3 +204,110 @@ func TestResolveOptions_UsesUppercaseChangelogDefault(t *testing.T) {
|
|||||||
t.Fatalf("resolved changelog = %q, want %q", resolved.Changelog, "CHANGELOG.md")
|
t.Fatalf("resolved changelog = %q, want %q", resolved.Changelog, "CHANGELOG.md")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewService_ValidatesDependencies(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
validFS := newStubFileSystem(nil)
|
||||||
|
validEnv := stubEnvironment{}
|
||||||
|
validGit := stubGitRunner{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
deps Dependencies
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing file system",
|
||||||
|
deps: Dependencies{Environment: validEnv, Git: validGit},
|
||||||
|
wantErr: "file system dependency must not be nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing environment",
|
||||||
|
deps: Dependencies{FileSystem: validFS, Git: validGit},
|
||||||
|
wantErr: "environment dependency must not be nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing git runner",
|
||||||
|
deps: Dependencies{FileSystem: validFS, Environment: validEnv},
|
||||||
|
wantErr: "git runner dependency must not be nil",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := NewService(tt.deps)
|
||||||
|
if err == nil || err.Error() != tt.wantErr {
|
||||||
|
t.Fatalf("NewService() error = %v, want %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServicePrepare_UsesInjectedEnvironmentForRepositoryLinks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
rootDir := "/repo"
|
||||||
|
fs := newStubFileSystem(map[string]string{
|
||||||
|
filepath.Join(rootDir, "release-version"): "1.1.6\n",
|
||||||
|
filepath.Join(rootDir, "CHANGELOG.md"): "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- New thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n",
|
||||||
|
})
|
||||||
|
|
||||||
|
svc, err := NewService(Dependencies{
|
||||||
|
FileSystem: fs,
|
||||||
|
Environment: stubEnvironment{"GITHUB_SERVER_URL": "https://git.hrafn.xyz", "GITHUB_REPOSITORY": "aether/vociferate"},
|
||||||
|
Git: stubGitRunner{commit: "deadbee", ok: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewService() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.Prepare(rootDir, "1.1.7", "2026-03-21", Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Prepare() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := string(fs.files[filepath.Join(rootDir, "CHANGELOG.md")])
|
||||||
|
if !contains(updated, "[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main") {
|
||||||
|
t.Fatalf("Prepare() changelog missing injected environment link:\n%s", updated)
|
||||||
|
}
|
||||||
|
if !contains(updated, "[1.1.7]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7") {
|
||||||
|
t.Fatalf("Prepare() changelog missing injected release link:\n%s", updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServicePrepare_UsesInjectedGitRunnerForFirstCommitLink(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
rootDir := "/repo"
|
||||||
|
fs := newStubFileSystem(map[string]string{
|
||||||
|
filepath.Join(rootDir, "release-version"): "1.1.6\n",
|
||||||
|
filepath.Join(rootDir, "CHANGELOG.md"): "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- New thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n",
|
||||||
|
filepath.Join(rootDir, ".git", "config"): "[remote \"origin\"]\n\turl = git@git.hrafn.xyz:aether/vociferate.git\n",
|
||||||
|
})
|
||||||
|
|
||||||
|
svc, err := NewService(Dependencies{
|
||||||
|
FileSystem: fs,
|
||||||
|
Environment: stubEnvironment{},
|
||||||
|
Git: stubGitRunner{commit: "abc1234", ok: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewService() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.Prepare(rootDir, "1.1.7", "2026-03-21", Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Prepare() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := string(fs.files[filepath.Join(rootDir, "CHANGELOG.md")])
|
||||||
|
if !contains(updated, "[1.1.6]: https://git.hrafn.xyz/aether/vociferate/compare/abc1234...v1.1.6") {
|
||||||
|
t.Fatalf("Prepare() changelog missing injected git link:\n%s", updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(text, fragment string) bool {
|
||||||
|
return len(fragment) > 0 && strings.Contains(text, fragment)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user