From d3cab1c4ca687afb595c925e29bb23e75f8b30b5 Mon Sep 17 00:00:00 2001 From: s0ders <39492740+s0ders@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:38:08 +0200 Subject: [PATCH 1/2] ci: updated .semver.yaml and .justfile This commit adds the following: - updated `git-email` in `.semver.yaml`to the one mapped to the GPG key used for signing tags so they appear as "verified" in the GitHub UI - fixed `golangci-lint` command --- .justfile | 2 +- .semver.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.justfile b/.justfile index f22108c..c7b2b57 100644 --- a/.justfile +++ b/.justfile @@ -38,7 +38,7 @@ clean: rm -rf ./bin/* lint: - golangci-lint run + golangci-lint run ./... gocyclo -over 15 . vuln: diff --git a/.semver.yaml b/.semver.yaml index 1affff1..04e1153 100644 --- a/.semver.yaml +++ b/.semver.yaml @@ -1,7 +1,7 @@ remote: true remote-name: origin git-name: Go Semver Release -git-email: go-semver@release.ci +git-email: 39492740+s0ders@users.noreply.github.com tag-prefix: v branches: - name: main From 7aef5e0a5ec1c96c59994d1ae0c01c1bea03fff3 Mon Sep 17 00:00:00 2001 From: s0ders <39492740+s0ders@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:43:13 +0200 Subject: [PATCH 2/2] perf!: various refactor to maximize performances This commits adds the following: - repository, local and remote, are now cloned (faster for local repository instead of using `PlainOpen`) - commits are now parsed one by one and not in bulk (around 1.5% faster) - branch, monorepo and rules flags types are updated to "JSON string" - AppContext has been moved to its own internal package - parser now only uses AppContext for configuration - added back support for configuration via environment variables (prefix "GO_SEMVER_RELEASE") BREAKING CHANGE --- .github/slsa-goreleaser/darwin-amd64.yml | 6 +- .github/slsa-goreleaser/darwin-arm64.yml | 6 +- .github/slsa-goreleaser/linux-amd64.yml | 6 +- .github/slsa-goreleaser/linux-arm64.yml | 6 +- .github/slsa-goreleaser/windows-amd64.yml | 6 +- .github/slsa-goreleaser/windows-arm64.yml | 6 +- .github/workflows/main.yaml | 4 - .justfile | 2 +- build/Dockerfile | 2 +- cmd/release.go | 138 ++++----- cmd/release_test.go | 86 +++--- cmd/root.go | 54 ++-- docs/README.md | 10 +- docs/SUMMARY.md | 6 +- docs/examples/github-actions-local-mode.yml | 34 +-- docs/examples/github-actions-remote-mode.yml | 28 +- docs/examples/gitlab-ci.yml | 28 ++ docs/miscellaneous/benchmark.md | 15 + docs/{usage => miscellaneous}/how-it-works.md | 0 docs/recipes/workflow-examples.md | 1 + docs/usage/configuration.md | 48 ++-- docs/usage/install.md | 7 +- docs/usage/output.md | 11 +- docs/usage/quickstart.md | 3 +- go.mod | 2 +- internal/appcontext/app_context.go | 35 +++ internal/branch/flag.go | 2 +- internal/ci/github.go | 2 +- internal/ci/github_test.go | 2 +- internal/gittest/gittest.go | 27 +- internal/monorepo/flag.go | 2 +- internal/monorepo/monorepo.go | 1 + internal/parser/parser.go | 264 ++++++++---------- internal/parser/parser_test.go | 237 +++++++++------- internal/remote/remote.go | 2 +- internal/remote/remote_test.go | 4 +- internal/rule/flag.go | 2 +- internal/tag/tag.go | 16 +- internal/tag/tag_test.go | 4 +- main.go | 2 +- 40 files changed, 602 insertions(+), 515 deletions(-) create mode 100644 docs/miscellaneous/benchmark.md rename docs/{usage => miscellaneous}/how-it-works.md (100%) create mode 100644 internal/appcontext/app_context.go diff --git a/.github/slsa-goreleaser/darwin-amd64.yml b/.github/slsa-goreleaser/darwin-amd64.yml index 363578d..16d48f7 100644 --- a/.github/slsa-goreleaser/darwin-amd64.yml +++ b/.github/slsa-goreleaser/darwin-amd64.yml @@ -11,8 +11,8 @@ main: ./main.go binary: go-semver-release-{{ .Os }}-{{ .Arch }} ldflags: - - "-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion={{ .Env.VERSION }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion={{ .Env.VERSION }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" - "-w" - "-s" diff --git a/.github/slsa-goreleaser/darwin-arm64.yml b/.github/slsa-goreleaser/darwin-arm64.yml index 78058f7..862eddf 100644 --- a/.github/slsa-goreleaser/darwin-arm64.yml +++ b/.github/slsa-goreleaser/darwin-arm64.yml @@ -11,8 +11,8 @@ main: ./main.go binary: go-semver-release-{{ .Os }}-{{ .Arch }} ldflags: - - "-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion={{ .Env.VERSION }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion={{ .Env.VERSION }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" - "-w" - "-s" diff --git a/.github/slsa-goreleaser/linux-amd64.yml b/.github/slsa-goreleaser/linux-amd64.yml index eab0586..c0d33de 100644 --- a/.github/slsa-goreleaser/linux-amd64.yml +++ b/.github/slsa-goreleaser/linux-amd64.yml @@ -11,8 +11,8 @@ main: ./main.go binary: go-semver-release-{{ .Os }}-{{ .Arch }} ldflags: - - "-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion={{ .Env.VERSION }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion={{ .Env.VERSION }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" - "-w" - "-s" diff --git a/.github/slsa-goreleaser/linux-arm64.yml b/.github/slsa-goreleaser/linux-arm64.yml index 4db508f..c63f185 100644 --- a/.github/slsa-goreleaser/linux-arm64.yml +++ b/.github/slsa-goreleaser/linux-arm64.yml @@ -11,8 +11,8 @@ main: ./main.go binary: go-semver-release-{{ .Os }}-{{ .Arch }} ldflags: - - "-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion={{ .Env.VERSION }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion={{ .Env.VERSION }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" - "-w" - "-s" diff --git a/.github/slsa-goreleaser/windows-amd64.yml b/.github/slsa-goreleaser/windows-amd64.yml index 8c7c474..74b0851 100644 --- a/.github/slsa-goreleaser/windows-amd64.yml +++ b/.github/slsa-goreleaser/windows-amd64.yml @@ -11,8 +11,8 @@ main: ./main.go binary: go-semver-release-{{ .Os }}-{{ .Arch }}.exe ldflags: - - "-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion={{ .Env.VERSION }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion={{ .Env.VERSION }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" - "-w" - "-s" \ No newline at end of file diff --git a/.github/slsa-goreleaser/windows-arm64.yml b/.github/slsa-goreleaser/windows-arm64.yml index f83a561..3ba3feb 100644 --- a/.github/slsa-goreleaser/windows-arm64.yml +++ b/.github/slsa-goreleaser/windows-arm64.yml @@ -11,8 +11,8 @@ main: ./main.go binary: go-semver-release-{{ .Os }}-{{ .Arch }}.exe ldflags: - - "-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion={{ .Env.VERSION }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" - - "-X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion={{ .Env.VERSION }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildNumber={{ .Env.BUILD_NUMBER }}" + - "-X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash={{ .Env.COMMIT_HASH }}" - "-w" - "-s" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4bc30f9..e2986b0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -5,10 +5,6 @@ on: branches: [ "main" ] env: - DOCKER_REPO: docker.io/s0ders - DOCKER_IMAGE: docker.io/s0ders/go-semver-release - GIT_CI_USERNAME: go-semver-ci - GIT_CI_EMAIL: go-semver@release.ci GO_VERSION: 1.23.1 permissions: read-all diff --git a/.justfile b/.justfile index c7b2b57..ce89751 100644 --- a/.justfile +++ b/.justfile @@ -8,7 +8,7 @@ appVersion := "v0.0.0+local" buildNumber := "local" commitHash := "local" -importPath := "github.com/s0ders/go-semver-release/v5/" +importPath := "github.com/s0ders/go-semver-release/v6/" ldFlags := "-X " + importPath + "cmd.cmdVersion=" + appVersion + " -X " + importPath + "cmd.buildNumber=" + buildNumber + " -X " + importPath + "cmd.buildCommitHash=" + commitHash + " -w -s" tests: diff --git a/build/Dockerfile b/build/Dockerfile index 5c336fa..1de6a4a 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /app COPY .. /app RUN go mod download -RUN CGO_ENABLED=0 go build -ldflags="-X github.com/s0ders/go-semver-release/v5/cmd.cmdVersion=$APP_VERSION -X github.com/s0ders/go-semver-release/v5/cmd.buildNumber=$APP_BUILD_NUMBER -X github.com/s0ders/go-semver-release/v5/cmd.buildCommitHash=$APP_COMMIT_HASH -w -s" -v -o app . +RUN CGO_ENABLED=0 go build -ldflags="-X github.com/s0ders/go-semver-release/v6/cmd.cmdVersion=$APP_VERSION -X github.com/s0ders/go-semver-release/v6/cmd.buildNumber=$APP_BUILD_NUMBER -X github.com/s0ders/go-semver-release/v6/cmd.buildCommitHash=$APP_COMMIT_HASH -w -s" -v -o app . # alpine:3.20.3 FROM alpine@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d AS vulnscan diff --git a/cmd/release.go b/cmd/release.go index 38d5419..0b04b8a 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -10,17 +10,18 @@ import ( "github.com/go-git/go-git/v5" "github.com/spf13/cobra" - "github.com/s0ders/go-semver-release/v5/internal/branch" - "github.com/s0ders/go-semver-release/v5/internal/ci" - "github.com/s0ders/go-semver-release/v5/internal/gpg" - "github.com/s0ders/go-semver-release/v5/internal/monorepo" - "github.com/s0ders/go-semver-release/v5/internal/parser" - "github.com/s0ders/go-semver-release/v5/internal/remote" - "github.com/s0ders/go-semver-release/v5/internal/rule" - "github.com/s0ders/go-semver-release/v5/internal/tag" + "github.com/s0ders/go-semver-release/v6/internal/appcontext" + "github.com/s0ders/go-semver-release/v6/internal/branch" + "github.com/s0ders/go-semver-release/v6/internal/ci" + "github.com/s0ders/go-semver-release/v6/internal/gpg" + "github.com/s0ders/go-semver-release/v6/internal/monorepo" + "github.com/s0ders/go-semver-release/v6/internal/parser" + "github.com/s0ders/go-semver-release/v6/internal/remote" + "github.com/s0ders/go-semver-release/v6/internal/rule" + "github.com/s0ders/go-semver-release/v6/internal/tag" ) -func NewReleaseCmd(ctx *AppContext) *cobra.Command { +func NewReleaseCmd(ctx *appcontext.AppContext) *cobra.Command { releaseCmd := &cobra.Command{ Use: "release ", Short: "Version a Git repository according the the given configuration", @@ -32,97 +33,82 @@ func NewReleaseCmd(ctx *AppContext) *cobra.Command { origin *remote.Remote ) - if ctx.RemoteModeFlag { - origin = remote.New(ctx.RemoteNameFlag, ctx.AccessTokenFlag) - repository, err = origin.Clone(args[0]) - if err != nil { - return fmt.Errorf("cloning Git repository: %w", err) - } - } else { - repository, err = git.PlainOpen(args[0]) - if err != nil { - return fmt.Errorf("opening local Git repository: %w", err) - } - } - entity, err := configureGPGKey(ctx) if err != nil { return fmt.Errorf("configuring GPG key: %w", err) } - rules, err := configureRules(ctx) + ctx.Rules, err = configureRules(ctx) if err != nil { return fmt.Errorf("loading rules configuration: %w", err) } - branches, err := configureBranches(ctx) + ctx.Branches, err = configureBranches(ctx) if err != nil { return fmt.Errorf("loading branches configuration: %w", err) } - projects, err := configureProjects(ctx) + ctx.Projects, err = configureProjects(ctx) if err != nil { return fmt.Errorf("loading projects configuration: %w", err) } + origin = remote.New(ctx.RemoteNameFlag, ctx.AccessTokenFlag) + + repository, err = origin.Clone(args[0]) + if err != nil { + return fmt.Errorf("cloning Git repository: %w", err) + } + + outputs, err := parser.New(ctx).Run(context.Background(), repository) + if err != nil { + return fmt.Errorf("computing new semver: %w", err) + } + tagger := tag.NewTagger(ctx.GitNameFlag, ctx.GitEmailFlag, tag.WithTagPrefix(ctx.TagPrefixFlag), tag.WithSignKey(entity)) - semverParser := parser.New(ctx.Logger, tagger, rules, parser.WithBuildMetadata(ctx.BuildMetadataFlag), parser.WithProjects(projects)) - for _, branch := range branches { - semverParser.SetBranch(branch.Name) - semverParser.SetPrerelease(branch.Prerelease) - semverParser.SetPrereleaseIdentifier(branch.Name) + for _, output := range outputs { + semver := output.Semver + release := output.NewRelease + commitHash := output.CommitHash + project := output.Project.Name - outputs, err := semverParser.Run(context.Background(), repository) + err = ci.GenerateGitHubOutput(semver, output.Branch, ci.WithNewRelease(release), ci.WithTagPrefix(ctx.TagPrefixFlag), ci.WithProject(project)) if err != nil { - return fmt.Errorf("computing new semver: %w", err) + return fmt.Errorf("generating github output: %w", err) } - for _, output := range outputs { - semver := output.Semver - release := output.NewRelease - commitHash := output.CommitHash - project := output.Project.Name - - err = ci.GenerateGitHubOutput(semver, branch.Name, ci.WithNewRelease(release), ci.WithTagPrefix(ctx.TagPrefixFlag), ci.WithProject(project)) - if err != nil { - return fmt.Errorf("generating github output: %w", err) - } + logEvent := ctx.Logger.Info() + logEvent.Bool("new-release", release) + logEvent.Str("version", semver.String()) + logEvent.Str("branch", output.Branch) - logEvent := ctx.Logger.Info() - logEvent.Bool("new-release", release) - logEvent.Str("version", semver.String()) - logEvent.Str("branch", branch.Name) + if project != "" { + logEvent.Str("project", project) - if project != "" { - logEvent.Str("project", project) + tagger.SetProjectName(project) + } - tagger.SetProjectName(project) + switch { + case !release: + logEvent.Msg("no new release") + return nil + case release && ctx.DryRunFlag: + logEvent.Msg("dry-run enabled, next release found") + return nil + default: + logEvent.Msg("new release found") + + err = tagger.TagRepository(repository, semver, commitHash) + if err != nil { + return fmt.Errorf("tagging repository: %w", err) } - switch { - case !release: - logEvent.Msg("no new release") - return nil - case release && ctx.DryRunFlag: - logEvent.Msg("dry-run enabled, next release found") - return nil - default: - logEvent.Msg("new release found") - - err = tagger.TagRepository(repository, semver, commitHash) - if err != nil { - return fmt.Errorf("tagging repository: %w", err) - } - - ctx.Logger.Debug().Str("tag", tagger.Format(semver)).Msg("new tag added to repository") - - if ctx.RemoteModeFlag { - err = origin.PushTag(tagger.Format(semver)) - if err != nil { - return fmt.Errorf("pushing tag to remote: %w", err) - } - } + ctx.Logger.Debug().Str("tag", tagger.Format(semver)).Msg("new tag added to repository") + + err = origin.PushTag(tagger.Format(semver)) + if err != nil { + return fmt.Errorf("pushing tag to remote: %w", err) } } } @@ -134,7 +120,7 @@ func NewReleaseCmd(ctx *AppContext) *cobra.Command { return releaseCmd } -func configureRules(ctx *AppContext) (rule.Rules, error) { +func configureRules(ctx *appcontext.AppContext) (rule.Rules, error) { flag := ctx.RulesFlag if flag.String() == "{}" { @@ -151,7 +137,7 @@ func configureRules(ctx *AppContext) (rule.Rules, error) { return unmarshalledRules, nil } -func configureBranches(ctx *AppContext) ([]branch.Branch, error) { +func configureBranches(ctx *appcontext.AppContext) ([]branch.Branch, error) { branchesJSON := []map[string]any(ctx.BranchesFlag) unmarshalledBranches, err := branch.Unmarshall(branchesJSON) @@ -162,7 +148,7 @@ func configureBranches(ctx *AppContext) ([]branch.Branch, error) { return unmarshalledBranches, nil } -func configureProjects(ctx *AppContext) ([]monorepo.Project, error) { +func configureProjects(ctx *appcontext.AppContext) ([]monorepo.Project, error) { flag := ctx.MonorepositoryFlag if flag.String() == "[]" { @@ -179,7 +165,7 @@ func configureProjects(ctx *AppContext) ([]monorepo.Project, error) { return projects, nil } -func configureGPGKey(ctx *AppContext) (*openpgp.Entity, error) { +func configureGPGKey(ctx *appcontext.AppContext) (*openpgp.Entity, error) { flag := ctx.GPGKeyPathFlag if flag == "" { diff --git a/cmd/release_test.go b/cmd/release_test.go index 8595a61..f6d5639 100644 --- a/cmd/release_test.go +++ b/cmd/release_test.go @@ -4,21 +4,22 @@ import ( "bufio" "bytes" "encoding/json" - "github.com/s0ders/go-semver-release/v5/internal/branch" - "github.com/s0ders/go-semver-release/v5/internal/monorepo" - "github.com/s0ders/go-semver-release/v5/internal/rule" - "github.com/spf13/cobra" - "github.com/spf13/viper" "os" "path/filepath" "testing" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/spf13/cobra" + "github.com/spf13/viper" assertion "github.com/stretchr/testify/assert" - "github.com/s0ders/go-semver-release/v5/internal/gittest" - "github.com/s0ders/go-semver-release/v5/internal/tag" + "github.com/s0ders/go-semver-release/v6/internal/appcontext" + "github.com/s0ders/go-semver-release/v6/internal/branch" + "github.com/s0ders/go-semver-release/v6/internal/gittest" + "github.com/s0ders/go-semver-release/v6/internal/monorepo" + "github.com/s0ders/go-semver-release/v6/internal/rule" + "github.com/s0ders/go-semver-release/v6/internal/tag" ) type cmdOutput struct { @@ -29,6 +30,25 @@ type cmdOutput struct { NewRelease bool `json:"new-release"` } +func TestReleaseCmd_ConfigurationAsEnvironmentVariable(t *testing.T) { + assert := assertion.New(t) + th := NewTestHelper(t) + + err := th.SetFlag(BranchesConfiguration, `[{"name": "master"}]`) + checkErr(t, err, "setting branches configuration") + + testRepository := NewTestRepository(t, []string{}) + + accessToken := "secret" + err = os.Setenv("GO_SEMVER_RELEASE_ACCESS_TOKEN", accessToken) + checkErr(t, err, "setting environment variable") + + _, err = th.ExecuteCommand("release", testRepository.Path) + checkErr(t, err, "executing command") + + assert.Equal(accessToken, th.Ctx.AccessTokenFlag, "access token flag value should be equal to environment variable value") +} + func TestReleaseCmd_ConfigurationAsFile(t *testing.T) { assert := assertion.New(t) @@ -39,7 +59,7 @@ func TestReleaseCmd_ConfigurationAsFile(t *testing.T) { cfgContent := []byte(` git-name: ` + taggerName + ` git-email: ` + taggerEmail + ` -tag-prefix: version +tag-prefix: v branches: - name: master - name: alpha @@ -118,24 +138,25 @@ rules: checkErr(t, err, "running release command") expectedMasterVersion := "1.2.2" - expectedMasterTag := "version" + expectedMasterVersion - expectedMasterOut := cmdOutput{ - Message: "new release found", - Version: expectedMasterVersion, - NewRelease: true, - Branch: "master", - } - actualMasterOut := cmdOutput{} - + expectedMasterTag := "v" + expectedMasterVersion expectedAlphaVersion := "1.3.0-alpha" - expectedAlphaTag := "version" + expectedAlphaVersion - expectedAlphaOut := cmdOutput{ - Message: "new release found", - Version: expectedAlphaVersion, - NewRelease: true, - Branch: "alpha", + expectedAlphaTag := "v" + expectedAlphaVersion + + expectedOutputs := []cmdOutput{ + { + Message: "new release found", + Version: expectedAlphaVersion, + NewRelease: true, + Branch: "alpha", + }, + { + Message: "new release found", + Version: expectedMasterVersion, + NewRelease: true, + Branch: "master", + }, } - actualAlphaOut := cmdOutput{} + actualOutput := cmdOutput{} outputs := make([]string, 0, 2) @@ -145,10 +166,10 @@ rules: } // Checking master - err = json.Unmarshal([]byte(outputs[0]), &actualMasterOut) + err = json.Unmarshal([]byte(outputs[0]), &actualOutput) checkErr(t, err, "unmarshalling master output") - assert.Equal(expectedMasterOut, actualMasterOut, "releaseCmd output should be equal") + assert.Contains(expectedOutputs, actualOutput, "releaseCmd output should be equal") exists, err := tag.Exists(testRepository.Repository, expectedMasterTag) checkErr(t, err, "checking if master tag exists") @@ -165,10 +186,10 @@ rules: assert.Equal(taggerEmail, expectedTagObj.Tagger.Email) // Checking alpha - err = json.Unmarshal([]byte(outputs[1]), &actualAlphaOut) + err = json.Unmarshal([]byte(outputs[1]), &actualOutput) checkErr(t, err, "unmarshalling alpha output") - assert.Equal(expectedAlphaOut, actualAlphaOut, "releaseCmd output should be equal") + assert.Contains(expectedOutputs, actualOutput, "releaseCmd output should be equal") exists, err = tag.Exists(testRepository.Repository, expectedAlphaTag) checkErr(t, err, "checking if alpha tag exists") @@ -297,7 +318,6 @@ func TestReleaseCmd_RemoteRelease(t *testing.T) { th := NewTestHelper(t) err := th.SetFlags(map[string]string{ BranchesConfiguration: `[{"name": "master"}]`, - RemoteConfiguration: "true", RemoteNameConfiguration: "origin", AccessTokenConfiguration: "", }) @@ -421,7 +441,7 @@ func TestReleaseCmd_MultiBranchRelease(t *testing.T) { err = json.Unmarshal(rawOutput, &actualOutput) checkErr(t, err, "unmarshalling output") - assert.Equal(expectedOutputs[i], actualOutput) + assert.Contains(expectedOutputs, actualOutput) i++ } @@ -626,7 +646,7 @@ func TestReleaseCmd_InvalidRepositoryPath(t *testing.T) { _ = th.SetFlag(BranchesConfiguration, `[{"name": "master"}]`) _, err := th.ExecuteCommand("release", "./does/not/exist") - assert.ErrorContains(err, "opening local Git repository", "should have failed trying to open inexisting Git repository") + assert.ErrorContains(err, "cloning Git repository", "should have failed trying to open inexisting Git repository") } func TestReleaseCmd_RepositoryWithNoHead(t *testing.T) { @@ -897,13 +917,13 @@ func NewTestRepository(t *testing.T, commits []string) *gittest.TestRepository { } type TestHelper struct { - Ctx *AppContext + Ctx *appcontext.AppContext Cmd *cobra.Command } // NewTestHelper creates a new TestHelper with a fresh AppContext and Command func NewTestHelper(t *testing.T) *TestHelper { - ctx := &AppContext{ + ctx := &appcontext.AppContext{ Viper: viper.New(), } cmd := NewRootCommand(ctx) diff --git a/cmd/root.go b/cmd/root.go index bfad94a..9bef3a8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,15 +4,18 @@ import ( "encoding/json" "errors" "fmt" + "path/filepath" + "strings" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/s0ders/go-semver-release/v5/internal/branch" - "github.com/s0ders/go-semver-release/v5/internal/monorepo" - "github.com/s0ders/go-semver-release/v5/internal/rule" + "github.com/s0ders/go-semver-release/v6/internal/appcontext" + "github.com/s0ders/go-semver-release/v6/internal/branch" + "github.com/s0ders/go-semver-release/v6/internal/monorepo" + "github.com/s0ders/go-semver-release/v6/internal/rule" ) const ( @@ -29,41 +32,21 @@ const ( GitNameConfiguration = "git-name" GPGPathConfiguration = "gpg-key-path" MonorepoConfiguration = "monorepo" - RemoteConfiguration = "remote" RemoteNameConfiguration = "remote-name" RulesConfiguration = "rules" TagPrefixConfiguration = "tag-prefix" ) -type AppContext struct { - Viper *viper.Viper - Logger zerolog.Logger - CfgFileFlag string - GitNameFlag string - GitEmailFlag string - TagPrefixFlag string - AccessTokenFlag string - RemoteNameFlag string - GPGKeyPathFlag string - RemoteModeFlag bool - BuildMetadataFlag string - DryRunFlag bool - VerboseFlag bool - BranchesFlag branch.Flag - MonorepositoryFlag monorepo.Flag - RulesFlag rule.Flag -} - -func NewAppContext() *AppContext { - return &AppContext{ +func NewAppContext() *appcontext.AppContext { + return &appcontext.AppContext{ Viper: viper.New(), } } -func NewRootCommand(ctx *AppContext) *cobra.Command { +func NewRootCommand(ctx *appcontext.AppContext) *cobra.Command { rootCmd := &cobra.Command{ Use: "go-semver-release", - Short: "go-semver-release - CLI to automate semantic versioning of Git repositories", + Short: "go-semver-release - Automate semantic versioning of Git repositories", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { ctx.Logger = zerolog.New(cmd.OutOrStdout()).Level(zerolog.InfoLevel) @@ -77,16 +60,15 @@ func NewRootCommand(ctx *AppContext) *cobra.Command { } rootCmd.PersistentFlags().StringVar(&ctx.AccessTokenFlag, AccessTokenConfiguration, "", "Access token used to push tag to Git remote") - rootCmd.PersistentFlags().Var(&ctx.BranchesFlag, BranchesConfiguration, "An array of branches such as [{\"name\": \"main\"}, {\"name\": \"rc\", \"prerelease\": true}]") + rootCmd.PersistentFlags().VarP(&ctx.BranchesFlag, BranchesConfiguration, "b", "An array of branches such as [{\"name\": \"main\"}, {\"name\": \"rc\", \"prerelease\": true}]") rootCmd.PersistentFlags().StringVar(&ctx.BuildMetadataFlag, BuildMetadataConfiguration, "", "Build metadata (e.g. build number) that will be appended to the SemVer") - rootCmd.PersistentFlags().StringVar(&ctx.CfgFileFlag, "config", "", "Configuration file path (default is ./"+defaultConfigFile+""+configFileFormat+")") + rootCmd.PersistentFlags().StringVar(&ctx.CfgFileFlag, "config", "", "Configuration file path (default \"./"+defaultConfigFile+"."+configFileFormat+"\")") rootCmd.PersistentFlags().BoolVarP(&ctx.DryRunFlag, DryRunConfiguration, "d", false, "Only compute the next SemVer, do not push any tag") rootCmd.PersistentFlags().StringVar(&ctx.GitEmailFlag, GitEmailConfiguration, "go-semver@release.ci", "Email used in semantic version tags") rootCmd.PersistentFlags().StringVar(&ctx.GitNameFlag, GitNameConfiguration, "Go Semver Release", "Name used in semantic version tags") rootCmd.PersistentFlags().StringVar(&ctx.GPGKeyPathFlag, GPGPathConfiguration, "", "Path to an armored GPG key used to sign produced tags") rootCmd.PersistentFlags().Var(&ctx.MonorepositoryFlag, MonorepoConfiguration, "An array of branches such as [{\"name\": \"foo\", \"path\": \"./foo/\"}]") rootCmd.PersistentFlags().StringVar(&ctx.RemoteNameFlag, RemoteNameConfiguration, "origin", "Name of the Git repository remote") - rootCmd.PersistentFlags().BoolVar(&ctx.RemoteModeFlag, RemoteConfiguration, false, "Version a remote repository, a token is required") rootCmd.PersistentFlags().Var(&ctx.RulesFlag, RulesConfiguration, "An hashmap of array such as {\"minor\": [\"feat\"], \"patch\": [\"fix\", \"perf\"]} ]") rootCmd.PersistentFlags().StringVar(&ctx.TagPrefixFlag, TagPrefixConfiguration, "v", "Prefix added to the version tag name") rootCmd.PersistentFlags().BoolVarP(&ctx.VerboseFlag, "verbose", "v", false, "Verbose output") @@ -100,7 +82,7 @@ func NewRootCommand(ctx *AppContext) *cobra.Command { return rootCmd } -func initializeConfig(cmd *cobra.Command, ctx *AppContext) error { +func initializeConfig(cmd *cobra.Command, ctx *appcontext.AppContext) error { if ctx.CfgFileFlag != "" { ctx.Viper.SetConfigFile(ctx.CfgFileFlag) } else { @@ -109,7 +91,15 @@ func initializeConfig(cmd *cobra.Command, ctx *AppContext) error { ctx.Viper.SetConfigName(defaultConfigFile) } - ctx.Logger.Debug().Str("path", ctx.CfgFileFlag).Msg("using the following configuration file") + absCfgPath, err := filepath.Abs(ctx.CfgFileFlag) + if err != nil { + return fmt.Errorf("getting configuration file absolute path: %w", err) + } + ctx.Logger.Debug().Str("path", absCfgPath).Msg("using the following configuration file") + + ctx.Viper.SetEnvPrefix("GO_SEMVER_RELEASE") + ctx.Viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + ctx.Viper.AutomaticEnv() if err := ctx.Viper.ReadInConfig(); err != nil { var configFileNotFoundError viper.ConfigFileNotFoundError diff --git a/docs/README.md b/docs/README.md index 66417a8..e566d2a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,9 +6,9 @@ Mentioned in Awesome Go GitHub Tag GitHub go.mod Go version - Go Reference + Go Reference GitHub Actions Workflow Status - Go Report Card + Go Report Card Codecov GitHub License OpenSSF Best Practices @@ -52,12 +52,16 @@ If you want a simple tool that handle the generation of the next semantic versio * [Quickstart](usage/quickstart.md) * [Configuration](usage/configuration.md) * [Output](usage/output.md) -* [How it works](usage/how-it-works.md) ### Recipes * [Workflow examples](recipes/workflow-examples.md) +### Miscellaneous + +* [Benchmark](miscellaneous/benchmark.md) +* [How it works](miscellaneous/how-it-works.md) +

diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 2c97261..3b386b4 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -8,8 +8,12 @@ * [Quickstart](usage/quickstart.md) * [Configuration](usage/configuration.md) * [Output](usage/output.md) -* [How it works](usage/how-it-works.md) ## Recipes * [Workflow examples](recipes/workflow-examples.md) + +## Miscellaneous + +* [Benchmark](miscelaneous/benchmark.md) +* [How it works](miscelaneous/how-it-works.md) diff --git a/docs/examples/github-actions-local-mode.yml b/docs/examples/github-actions-local-mode.yml index 91e3601..4a4064c 100644 --- a/docs/examples/github-actions-local-mode.yml +++ b/docs/examples/github-actions-local-mode.yml @@ -24,20 +24,18 @@ jobs: fetch-depth: 0 # Fetches tags # Install Go Semver Release - - name: Set up Go - uses: actions/setup-go@v5.0.0 - with: - go-version: 1.22.4 - - name: Install Go Semver Release - run: go install github.com/s0ders/go-semver-release/v5@latest + run: | + curl -SL https://github.com/s0ders/go-semver-release/releases/latest/download/go-semver-release-linux-amd64 -o ./go-semver-release \ + && chmod +x ./go-semver-release - # If build and unit tests are green, check if there is a new release. - # Running in dry-run mode since the repository remote won't be tag anyway because we are in local mode. + # Running in dry-run mode since the repository remote will not be tagged since the program is running + # are in local mode. - name: Go Semver Release id: go-semver - run: go-semver-release release . --config .semver.yaml --dry-run + run: ./go-semver-release release . --config .semver.yaml --dry-run + # Configuring Git username and email that will appear as the tag author - name: Git Configuration run: | git config --global user.email "go-semver-release@ci.com" @@ -59,19 +57,5 @@ jobs: steps: - uses: actions/checkout@v4.1.4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.3.0 - - - name: Login to Docker Hub - uses: docker/login-action@v3.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Tag artifact with pre-release tag - - name: Docker build pre-release - uses: docker/build-push-action@v5.3.0 - with: - push: true - tags: :${{ env.RELEASE_TAG }} - + # Insert your release process here + # ... \ No newline at end of file diff --git a/docs/examples/github-actions-remote-mode.yml b/docs/examples/github-actions-remote-mode.yml index dd0471a..fd8c490 100644 --- a/docs/examples/github-actions-remote-mode.yml +++ b/docs/examples/github-actions-remote-mode.yml @@ -24,20 +24,17 @@ jobs: - uses: actions/checkout@v4.1.4 # Install Go Semver Release - - name: Set up Go - uses: actions/setup-go@v5.0.0 - with: - go-version: 1.22.4 - - name: Install Go Semver Release - run: go install github.com/s0ders/go-semver-release/v5@latest + run: | + curl -SL https://github.com/s0ders/go-semver-release/releases/latest/download/go-semver-release-linux-amd64 -o ./go-semver-release \ + && chmod +x ./go-semver-release # Tests are good, versioning the repository (if any new release is found) - name: Go Semver Release id: go-semver env: GO_SEMVER_RELEASE_ACCESS_TOKEN: ${{ secrets.accessToken }} - run: go-semver-release release https://example.com/my/repo.git --config .semver.yaml + run: ./go-semver-release release https://github.com/my/repo.git --config .semver.yaml release: runs-on: ubuntu-latest @@ -49,18 +46,5 @@ jobs: steps: - uses: actions/checkout@v4.1.4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.3.0 - - - name: Login to Docker Hub - uses: docker/login-action@v3.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Tag artifact with SemVer number - - name: Docker build pre-release - uses: docker/build-push-action@v5.3.0 - with: - push: true - tags: :${{ env.RELEASE_TAG }} + # Insert your release process here + # ... diff --git a/docs/examples/gitlab-ci.yml b/docs/examples/gitlab-ci.yml index e69de29..7fd3faf 100644 --- a/docs/examples/gitlab-ci.yml +++ b/docs/examples/gitlab-ci.yml @@ -0,0 +1,28 @@ +stages: + - build + - test + - deploy + +build-job: + stage: build + script: + - echo "Compiling the code..." + - echo "Compile complete." + +unit-test-job: + stage: test + script: + - echo "Running unit tests... This will take about 60 seconds." + # Insert your testing process here + +versioning: + stage: version + script: + - curl -SL https://github.com/s0ders/go-semver-release/releases/latest/download/go-semver-release-linux-amd64 -o ./go-semver-release && chmod +x ./go-semver-release + - ./go-semver-release https://gitlab.com/my/repo --config .semver.yaml + +deploy-job: + stage: deploy + script: + - echo "Deploying application..." + # Insert your release process here diff --git a/docs/miscellaneous/benchmark.md b/docs/miscellaneous/benchmark.md new file mode 100644 index 0000000..883e13f --- /dev/null +++ b/docs/miscellaneous/benchmark.md @@ -0,0 +1,15 @@ +# Benchmark + +The following showcases performances benchmarks of Go Semver Release against other such tools. Theses benchmarks were realized using [hyperfine ](https://github.com/sharkdp/hyperfine)in GitHub Action [runner ](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)with the following configuration: + +* OS: Ubuntu 24.04 +* Processor (CPU): 4 +* Memory: 16 GB + +Each program was executed 10 times (avoids statistic outliers) computing the latest semantic version of a [sample repository](https://github.com/s0ders/big-sample-repo) of 10,000 commits on a single "main" branch without any prior tag: + +| Program | Time (mean ± σ) | Range (min ... max) | +| ------------------------------------------------------------------------------ | ----------------- | ------------------- | +| [Go Semver Release](https://github.com/s0ders/go-semver-release) | 1.127 s ± 0.244 s | 0.986 s ... 1.600 s | +| [Semantic Release](https://github.com/semantic-release/semantic-release) | 5.150 s ± 0.046 s | 5.041 s ... 5.207 s | + diff --git a/docs/usage/how-it-works.md b/docs/miscellaneous/how-it-works.md similarity index 100% rename from docs/usage/how-it-works.md rename to docs/miscellaneous/how-it-works.md diff --git a/docs/recipes/workflow-examples.md b/docs/recipes/workflow-examples.md index f529299..234e02a 100644 --- a/docs/recipes/workflow-examples.md +++ b/docs/recipes/workflow-examples.md @@ -29,3 +29,4 @@ Below are simple pipeline examples for various CI providers: * [GitHub Actions](https://github.com/s0ders/go-semver-release/blob/main/docs/examples/github-actions-remote-mode.yml) using remote mode * [GitHub Actions](https://github.com/s0ders/go-semver-release/blob/main/docs/examples/github-actions-local-mode.yml) using local mode +* [GitLab CI/CD](https://github.com/s0ders/go-semver-release/blob/main/docs/examples/gitlab-ci.yml) using remote mode \ No newline at end of file diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md index 94e41f3..4523925 100644 --- a/docs/usage/configuration.md +++ b/docs/usage/configuration.md @@ -12,11 +12,12 @@ $ go-semver-release release --help ### Configuration precedence -The order of precedence for the configuration is: +The order of precedence for the configuration, from highest to lowest, is: -* Explicitly set flag values have the highest precedence -* Then values set in the configuration file -* Finally, flag default values have the lowest precedence, each flag default value is given in the help message of the command +1. Flag values +2. Environment variable (prefixed by `GO_SEMVER_RELEASE`) values +3. Configuration file values +4. Flag default value ### Configuration file @@ -91,20 +92,25 @@ branches: ### Remote and access token -CLI flags: `--remote`, `--remote-name` +CLI flags: `--remote-name`, `--access-token` -By default, Go Semver Release operate in local mode and expect the repository to exist on the local file system. This has the advantage of avoiding the use of access token. However, it can be easier to simply let Go Semver Release clone a repository, parse it and push the newly found SemVer tag, if any. +If the path to the Git repository supplied to Go Semver Release is a local path, it will operate in local mode which offers the benefits of avoiding the use of access token. However, it can be easier to simply let Go Semver Release clone a repository, parse it and push the newly found SemVer tag, if any. -To enable the remote mode, you to set the following in your configuration file: +To enable the remote mode, simply provide a URL to the Git repository when invoking the `release`command. The name of the remote can be set if it's not the default `origin`. +An access token is required so that Go Semver Release can clone the Git repository and push tags to it. All modern Git remote providers offer this feature (e.g., [GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens), [GitLab](https://docs.gitlab.com/ee/user/project/settings/project\_access\_tokens.html), [Bitbucket](https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/)). + +Please do not set the access token directly in the configuration file. A much safer alternative it to set the access token as a secret on the remote repository and, in your CI workflow, pass it to Go Semver Release either via the `--access-token` flag or via the `GO_SEMVER_RELEASE_ACCESS_TOKEN` environment variable. + +Examples: +```bash +$ export GO_SEMVER_RELEASE_ACCESS_TOKEN="secret" +$ go-semver-release release --remote-name "origin" +``` ```yaml -remote: true remote-name: "origin" ``` -An access token is required so that Go Semver Release can clone the Git repository and push tags to it. All modern Git remote providers offer this feature (e.g., [GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens), [GitLab](https://docs.gitlab.com/ee/user/project/settings/project\_access\_tokens.html), [Bitbucket](https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/)). - -Please do not set the access token directly in the configuration file. A much safer alternative it to set the access token as a secret on the remote repository and, in your CI workflow, pass it to Go Semver Release either via the `--access-token` flag or via the `GO_SEMVER_RELEASE_ACCESS_TOKEN` environment variable. ### Monorepo @@ -112,14 +118,6 @@ CLI flag: `--monorepo` The program can also version separately multiple projects stored in a single repository also called "monorepo" or "mono repository". To do so, the configuration file must include a `monorepo` section stating the name and path of the various projects inside that repository. -```yaml -monorepo: - - name: foo - path: ./foo/ - - name: bar - path: ./xyz/bar/ -``` - Each project will then be versioned separately meaning that each project will have its SemVer tag in the form `-` for instance `foo-1.2.3` or `bar-v0.0.1` **How does it work?** @@ -128,6 +126,18 @@ The program will first fetch the latest, if any, SemVer tag for each project con This means that if a commit has changes belonging to multiple projects of a monorepo, all projects concerned will have their SemVer bumped according to the commit type. +Examples: +```bash +$ go-semver-release release --monorepo='[{"name": "foo", "path": "./foo/"}, {"name": "bar", "path": "./bar/"}]' +``` +```yaml +monorepo: + - name: foo + path: ./foo/ + - name: bar + path: ./xyz/bar/ +``` + ### Tag prefix CLI flag: `--tag-prefix` diff --git a/docs/usage/install.md b/docs/usage/install.md index 7b65811..7743ca5 100644 --- a/docs/usage/install.md +++ b/docs/usage/install.md @@ -3,7 +3,10 @@ ### GitHub Releases The preferred method of installation is via the releases generated by the CI workflow of the official GitHub repository: -https://github.com/s0ders/go-semver-release/releases/latest + +```bash +$ curl -SL https://github.com/s0ders/go-semver-release/releases/latest/download/go-semver-release-linux-amd64 -o ./go-semver-release && chmod +x ./go-semver-release +``` Releases are available for various OS and architectures and come with generated [provenance](https://slsa.dev/spec/v1.0/provenance) of SLSA level 3 to avoid being tampered with, ensuring a strong layer of protection against supply chain attacks. @@ -12,7 +15,7 @@ Releases are available for various OS and architectures and come with generated If [Go](https://go.dev) is installed on your machine, you can install from source: ```bash -$ go install github.com/s0ders/go-semver-release/v5@latest +$ go install github.com/s0ders/go-semver-release/v6@latest $ go-semver-release --help ``` diff --git a/docs/usage/output.md b/docs/usage/output.md index 3075198..3248f7e 100644 --- a/docs/usage/output.md +++ b/docs/usage/output.md @@ -27,5 +27,12 @@ Here is an example of an output where two branches were parsed, please note that ``` ## GitHub Action output - -🚧 This section is a work in progress 🚧 \ No newline at end of file +Though this tool is CI agnostic, it will try to detect if it is being executed on a GitHub Action runner. +If the program is in [monorepo ](configuration.md#monorepo)mode, three outputs will be generated per branch/project pair: +* `_SEMVER`, the latest semantic version +* `_NEW_RELEASE`, whether a new release was found or not +* `_PROJECT`, the name of the project inside the monorepo + +If not in monorepo mode, two outputs will be generated per branch: +* `_SEMVER`, the latest semantic version +* `_NEW_RELEASE`, whether a new release was found or not \ No newline at end of file diff --git a/docs/usage/quickstart.md b/docs/usage/quickstart.md index 769d563..c7d4728 100644 --- a/docs/usage/quickstart.md +++ b/docs/usage/quickstart.md @@ -4,13 +4,12 @@ All you need to get started is: * A Git repository available locally or remotely * A commit history following the [Conventional Commits](https://www.conventionalcommits.org/en/) convention -* A configuration file inside the Git repository to version +* Optionally, a configuration file inside the Git repository to version The following example configuration file suites most use cases: ```yaml # /.semver.yaml -remote: true remote-name: "origin" git-name: "My Custom Robot Name" git-email: "custom-robot@acme.com" diff --git a/go.mod b/go.mod index 7bdee3c..1d45918 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/s0ders/go-semver-release/v5 +module github.com/s0ders/go-semver-release/v6 go 1.23.1 diff --git a/internal/appcontext/app_context.go b/internal/appcontext/app_context.go new file mode 100644 index 0000000..b967b9a --- /dev/null +++ b/internal/appcontext/app_context.go @@ -0,0 +1,35 @@ +// Package appcontext provides a structure to store the current application execution context. +// +// The use of this structure allows to avoid the use of global variables to share the states of variables across +// structures and functions. +package appcontext + +import ( + "github.com/rs/zerolog" + "github.com/spf13/viper" + + "github.com/s0ders/go-semver-release/v6/internal/branch" + "github.com/s0ders/go-semver-release/v6/internal/monorepo" + "github.com/s0ders/go-semver-release/v6/internal/rule" +) + +type AppContext struct { + Viper *viper.Viper + Branches []branch.Branch + Projects []monorepo.Project + Rules rule.Rules + BranchesFlag branch.Flag + MonorepositoryFlag monorepo.Flag + RulesFlag rule.Flag + Logger zerolog.Logger + CfgFileFlag string + GitNameFlag string + GitEmailFlag string + TagPrefixFlag string + AccessTokenFlag string + RemoteNameFlag string + GPGKeyPathFlag string + BuildMetadataFlag string + DryRunFlag bool + VerboseFlag bool +} diff --git a/internal/branch/flag.go b/internal/branch/flag.go index b70cd0e..db50127 100644 --- a/internal/branch/flag.go +++ b/internal/branch/flag.go @@ -9,7 +9,7 @@ import ( type Flag []map[string]any -const FlagType = "branchesFlag" +const FlagType = "JSON string" func (f *Flag) String() string { if f == nil || len(*f) == 0 { diff --git a/internal/ci/github.go b/internal/ci/github.go index f8485b8..d3536d1 100644 --- a/internal/ci/github.go +++ b/internal/ci/github.go @@ -7,7 +7,7 @@ import ( "os" "strings" - "github.com/s0ders/go-semver-release/v5/internal/semver" + "github.com/s0ders/go-semver-release/v6/internal/semver" ) type GitHubOutput struct { diff --git a/internal/ci/github_test.go b/internal/ci/github_test.go index 5f7ee79..08d2d8e 100644 --- a/internal/ci/github_test.go +++ b/internal/ci/github_test.go @@ -8,7 +8,7 @@ import ( assertion "github.com/stretchr/testify/assert" - "github.com/s0ders/go-semver-release/v5/internal/semver" + "github.com/s0ders/go-semver-release/v6/internal/semver" ) func TestCI_GenerateGitHub_HappyScenario(t *testing.T) { diff --git a/internal/gittest/gittest.go b/internal/gittest/gittest.go index 1a14b29..eeeb911 100644 --- a/internal/gittest/gittest.go +++ b/internal/gittest/gittest.go @@ -3,6 +3,7 @@ package gittest import ( "fmt" + "io" "math/rand/v2" "net/http" "os" @@ -28,8 +29,8 @@ type TestRepository struct { } // NewRepository creates a new TestRepository. -func NewRepository() (testRepository *TestRepository, err error) { - testRepository = &TestRepository{} +func NewRepository() (*TestRepository, error) { + testRepository := &TestRepository{} path, err := os.MkdirTemp("", "gittest-*") if err != nil { @@ -85,6 +86,28 @@ func NewRepository() (testRepository *TestRepository, err error) { return testRepository, err } +// Clone clones the current TestRepository to a temporary directory and returns the clone of that repository. This +// method is useful when testing on repository that are expected to have a configured remote. +func (r *TestRepository) Clone() (*TestRepository, error) { + testRepository := &TestRepository{} + + tempDir, err := os.MkdirTemp("", "*") + if err != nil { + return nil, fmt.Errorf("creating temporary directory: %w", err) + } + + testRepository.Path = tempDir + testRepository.Repository, err = git.PlainClone(tempDir, false, &git.CloneOptions{ + URL: r.Path, + Progress: io.Discard, + }) + if err != nil { + return nil, fmt.Errorf("cloning repository: %w", err) + } + + return testRepository, nil +} + // AddCommit adds a new commit with a given conventional commit type to the underlying Git repository. func (r *TestRepository) AddCommit(commitType string) (plumbing.Hash, error) { var commitHash plumbing.Hash diff --git a/internal/monorepo/flag.go b/internal/monorepo/flag.go index aff1800..f683986 100644 --- a/internal/monorepo/flag.go +++ b/internal/monorepo/flag.go @@ -9,7 +9,7 @@ import ( type Flag []map[string]string -const FlagType = "monorepoFlag" +const FlagType = "JSON string" func (f *Flag) String() string { if f == nil || len(*f) == 0 { diff --git a/internal/monorepo/monorepo.go b/internal/monorepo/monorepo.go index 23f36f6..732efb7 100644 --- a/internal/monorepo/monorepo.go +++ b/internal/monorepo/monorepo.go @@ -1,3 +1,4 @@ +// Package monorepo provides functions to work with monorepository configuration. package monorepo import ( diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 1e65fbd..151ead0 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -18,109 +18,76 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/rs/zerolog" "golang.org/x/sync/errgroup" - "github.com/s0ders/go-semver-release/v5/internal/monorepo" - "github.com/s0ders/go-semver-release/v5/internal/rule" - "github.com/s0ders/go-semver-release/v5/internal/semver" - "github.com/s0ders/go-semver-release/v5/internal/tag" + "github.com/s0ders/go-semver-release/v6/internal/appcontext" + "github.com/s0ders/go-semver-release/v6/internal/branch" + "github.com/s0ders/go-semver-release/v6/internal/monorepo" + "github.com/s0ders/go-semver-release/v6/internal/semver" ) var conventionalCommitRegex = regexp.MustCompile(`^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([\w\-.\\\/]+\))?(!)?: ([\w ]+[\s\S]*)`) type Parser struct { - rules rule.Rules - tagger *tag.Tagger - logger zerolog.Logger - releaseBranch string - buildMetadata string - prereleaseIdentifier string - prereleaseMode bool - projects []monorepo.Project - mu sync.Mutex + ctx *appcontext.AppContext + mu sync.Mutex } -type OptionFunc func(*Parser) - -func WithProjects(projects []monorepo.Project) OptionFunc { - return func(p *Parser) { - p.projects = projects - } -} - -func WithBuildMetadata(metadata string) OptionFunc { - return func(p *Parser) { - p.buildMetadata = metadata - } -} - -func New(logger zerolog.Logger, tagger *tag.Tagger, rules rule.Rules, options ...OptionFunc) *Parser { - parser := &Parser{ - logger: logger, - tagger: tagger, - rules: rules, - } - - for _, option := range options { - option(parser) - } +func New(ctx *appcontext.AppContext) *Parser { + parser := &Parser{ctx: ctx} return parser } type ComputeNewSemverOutput struct { - Project monorepo.Project Semver *semver.Version + Project monorepo.Project + Branch string CommitHash plumbing.Hash NewRelease bool } -func (p *Parser) SetBranch(branch string) { - p.releaseBranch = branch -} - -func (p *Parser) SetPrerelease(b bool) { - p.prereleaseMode = b -} - -func (p *Parser) SetPrereleaseIdentifier(prereleaseID string) { - p.prereleaseIdentifier = prereleaseID -} - +// Run execute a parser on a repository and analyze the given branches and projects contained inside the given +// AppContext. func (p *Parser) Run(ctx context.Context, repository *git.Repository) ([]ComputeNewSemverOutput, error) { - output := make([]ComputeNewSemverOutput, len(p.projects)) - - err := p.checkoutBranch(repository) - if err != nil { - return output, err - } + var output []ComputeNewSemverOutput - if len(p.projects) == 0 { - computerNewSemverOutput, err := p.ComputeNewSemver(repository, monorepo.Project{}) + for _, branch := range p.ctx.Branches { + err := p.checkoutBranch(repository, branch.Name) if err != nil { - return nil, err + return output, fmt.Errorf("checking out to branch %q: %w", branch.Name, err) } - return []ComputeNewSemverOutput{computerNewSemverOutput}, nil - } - - g, _ := errgroup.WithContext(ctx) - - for i, project := range p.projects { - g.Go(func() error { - result, err := p.ComputeNewSemver(repository, project) + if len(p.ctx.Projects) == 0 { + computerNewSemverOutput, err := p.ComputeNewSemver(repository, monorepo.Project{}, branch) if err != nil { - return err + return nil, fmt.Errorf("computing new semver: %w", err) } - output[i] = result - return nil - }) - } + output = append(output, computerNewSemverOutput) + } + + outputBuf := make([]ComputeNewSemverOutput, len(p.ctx.Projects)) + + g, _ := errgroup.WithContext(ctx) - if err := g.Wait(); err != nil { - return nil, err + for i, project := range p.ctx.Projects { + g.Go(func() error { + result, err := p.ComputeNewSemver(repository, project, branch) + if err != nil { + return fmt.Errorf("computing project %q new semver: %w", project.Name, err) + } + + outputBuf[i] = result + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, fmt.Errorf("parsing monorepository projects: %w", err) + } + + output = append(output, outputBuf...) } return output, nil @@ -128,7 +95,7 @@ func (p *Parser) Run(ctx context.Context, repository *git.Repository) ([]Compute // ComputeNewSemver returns the next, if any, semantic version number from a given Git repository by parsing its commit // history. -func (p *Parser) ComputeNewSemver(repository *git.Repository, project monorepo.Project) (ComputeNewSemverOutput, error) { +func (p *Parser) ComputeNewSemver(repository *git.Repository, project monorepo.Project, branch branch.Branch) (ComputeNewSemverOutput, error) { output := ComputeNewSemverOutput{} if project.Name != "" { @@ -147,11 +114,11 @@ func (p *Parser) ComputeNewSemver(repository *git.Repository, project monorepo.P ) if latestSemverTag == nil { - p.logger.Debug().Msg("no previous tag, creating one") + p.ctx.Logger.Debug().Msg("no previous tag, creating one") latestSemver = &semver.Version{Major: 0, Minor: 0, Patch: 0} } else { - p.logger.Debug().Str("tag", latestSemverTag.Name).Msg("latest semver tag found") + p.ctx.Logger.Debug().Str("tag", latestSemverTag.Name).Msg("latest semver tag found") latestSemver, err = semver.NewFromString(latestSemverTag.Name) if err != nil { @@ -171,6 +138,8 @@ func (p *Parser) ComputeNewSemver(repository *git.Repository, project monorepo.P } p.mu.Lock() + defer p.mu.Unlock() + repositoryLogs, err := repository.Log(&logOptions) if err != nil { return output, fmt.Errorf("fetching commit history: %w", err) @@ -186,86 +155,76 @@ func (p *Parser) ComputeNewSemver(repository *git.Repository, project monorepo.P sort.Slice(history, func(i, j int) bool { return history[i].Committer.When.Before(history[j].Committer.When) }) - p.mu.Unlock() - newRelease, commitHash, err := p.ParseHistory(history, latestSemver, project) - if err != nil { - return output, fmt.Errorf("parsing commit history: %w", err) + var newRelease bool + var commitHash plumbing.Hash + + for _, commit := range history { + newReleaseFound, hash, err := p.ProcessCommit(commit, latestSemver, project) + if err != nil { + return output, fmt.Errorf("parsing commit history: %w", err) + } + + if newReleaseFound { + newRelease = true + commitHash = hash + } } - if p.prereleaseMode { - latestSemver.Prerelease = p.prereleaseIdentifier + if branch.Prerelease { + latestSemver.Prerelease = branch.Name } - latestSemver.Metadata = p.buildMetadata + latestSemver.Metadata = p.ctx.BuildMetadataFlag output.Semver = latestSemver + output.Branch = branch.Name output.CommitHash = commitHash output.NewRelease = newRelease return output, nil } -// ParseHistory parses a slice of commits and modifies the given semantic version number according to the release rule -// provided. -func (p *Parser) ParseHistory(commits []*object.Commit, latestSemver *semver.Version, project monorepo.Project) (bool, plumbing.Hash, error) { - newRelease := false - latestReleaseCommitHash := plumbing.Hash{} - rulesMap := p.rules.Map - - for _, commit := range commits { - if !conventionalCommitRegex.MatchString(commit.Message) { - continue - } - - if project.Name != "" { - p.mu.Lock() - containsProjectFiles, err := commitContainsProjectFiles(commit, project.Path) - if err != nil { - return false, latestReleaseCommitHash, fmt.Errorf("checking if commit contains project files: %w", err) - } - p.mu.Unlock() +// ProcessCommit parse a commit message and bump the latest semantic version accordingly. +func (p *Parser) ProcessCommit(commit *object.Commit, latestSemver *semver.Version, project monorepo.Project) (bool, plumbing.Hash, error) { + if !conventionalCommitRegex.MatchString(commit.Message) { + return false, plumbing.ZeroHash, nil + } - if !containsProjectFiles { - continue - } + if project.Name != "" { + containsProjectFiles, err := commitContainsProjectFiles(commit, project.Path) + if err != nil { + return false, plumbing.ZeroHash, fmt.Errorf("checking if commit contains project files: %w", err) } - - match := conventionalCommitRegex.FindStringSubmatch(commit.Message) - breakingChange := match[3] == "!" || strings.Contains(match[0], "BREAKING CHANGE") - commitType := match[1] - shortHash := commit.Hash.String()[0:7] - shortMessage := shortenMessage(commit.Message) - - if breakingChange { - p.logger.Debug().Str("commit-hash", shortHash).Str("commit-message", shortMessage).Msg("breaking change found") - latestSemver.BumpMajor() - latestReleaseCommitHash = commit.Hash - newRelease = true - continue + if !containsProjectFiles { + return false, plumbing.ZeroHash, nil } + } - releaseType, ok := rulesMap[commitType] - if !ok { - continue - } + match := conventionalCommitRegex.FindStringSubmatch(commit.Message) + breakingChange := match[3] == "!" || strings.HasPrefix(commit.Message, "BREAKING CHANGE") + commitType := match[1] - switch releaseType { - case "patch": - latestSemver.BumpPatch() - case "minor": - latestSemver.BumpMinor() - default: - return false, latestReleaseCommitHash, fmt.Errorf("unknown release type %q", releaseType) - } + if breakingChange { + latestSemver.BumpMajor() + return true, commit.Hash, nil + } - latestReleaseCommitHash = commit.Hash - newRelease = true + releaseType, ok := p.ctx.Rules.Map[commitType] + if !ok { + return false, plumbing.ZeroHash, nil + } - p.logger.Debug().Str("commit-hash", shortHash).Str("commit-message", shortMessage).Str("release-type", releaseType).Msg("new release found") + switch releaseType { + case "patch": + latestSemver.BumpPatch() + case "minor": + latestSemver.BumpMinor() + default: + return false, plumbing.ZeroHash, fmt.Errorf("unknown release type %q", releaseType) } - return newRelease, latestReleaseCommitHash, nil + return true, commit.Hash, nil } // FetchLatestSemverTag parses a Git repository to fetch the tag corresponding to the highest semantic version number @@ -312,27 +271,36 @@ func (p *Parser) FetchLatestSemverTag(repository *git.Repository, project monore return latestTag, nil } -func (p *Parser) checkoutBranch(repository *git.Repository) error { - worktree, err := repository.Worktree() +// checkoutBranch moves the HEAD pointer of the given repository to the given branch. This function expects the +// repository to be a clone and have a remote to which it will set the branch being checkout to a remote reference to +// the corresponding remote branch. +func (p *Parser) checkoutBranch(repository *git.Repository, branchName string) error { + remoteBranchRef := plumbing.NewRemoteReferenceName(p.ctx.RemoteNameFlag, branchName) + _, err := repository.Reference(remoteBranchRef, true) if err != nil { - return fmt.Errorf("fetching worktree: %w", err) + return fmt.Errorf("remote branch %q not found: %w", remoteBranchRef, err) } - if worktree == nil { - return fmt.Errorf("no worktree, check that repository is initialized") + localBranchRef := plumbing.NewBranchReferenceName(branchName) + ref := plumbing.NewSymbolicReference(localBranchRef, remoteBranchRef) + err = repository.Storer.SetReference(ref) + if err != nil { + return fmt.Errorf("error creating local branch %q: %w", localBranchRef, err) } - // Checkout to release branch - releaseBranchRef := plumbing.NewBranchReferenceName(p.releaseBranch) - branchCheckOutOpts := git.CheckoutOptions{ - Branch: releaseBranchRef, - Force: true, + // Checkout the new local branch + w, err := repository.Worktree() + if err != nil { + return fmt.Errorf("error getting worktree: %w", err) } - err = worktree.Checkout(&branchCheckOutOpts) + err = w.Checkout(&git.CheckoutOptions{ + Branch: localBranchRef, + Force: true, + }) if err != nil { if errors.Is(err, plumbing.ErrReferenceNotFound) { - return fmt.Errorf("branch %q does not exist: %w", p.releaseBranch, err) + return fmt.Errorf("branch %q does not exist: %w", branchName, err) } return fmt.Errorf("checking out to release branch: %w", err) } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 893da80..400df96 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -3,6 +3,9 @@ package parser import ( "context" "fmt" + "github.com/rs/zerolog" + "github.com/s0ders/go-semver-release/v6/internal/appcontext" + "github.com/s0ders/go-semver-release/v6/internal/branch" "io" "os" "strings" @@ -10,22 +13,12 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/rs/zerolog" assertion "github.com/stretchr/testify/assert" - "github.com/s0ders/go-semver-release/v5/internal/gittest" - "github.com/s0ders/go-semver-release/v5/internal/monorepo" - "github.com/s0ders/go-semver-release/v5/internal/rule" - "github.com/s0ders/go-semver-release/v5/internal/semver" - "github.com/s0ders/go-semver-release/v5/internal/tag" -) - -var ( - logger = zerolog.New(io.Discard) - tagger = tag.NewTagger("foo", "foo") - rules = rule.Default - projects = []monorepo.Project{{Name: "foo", Path: "foo"}, {Name: "bar", Path: "bar"}} - emptyProject = monorepo.Project{} + "github.com/s0ders/go-semver-release/v6/internal/gittest" + "github.com/s0ders/go-semver-release/v6/internal/monorepo" + "github.com/s0ders/go-semver-release/v6/internal/rule" + "github.com/s0ders/go-semver-release/v6/internal/semver" ) func TestParser_CommitTypeRegex(t *testing.T) { @@ -83,9 +76,11 @@ func TestParser_FetchLatestSemverTag_NoTag(t *testing.T) { _ = testRepository.Remove() }) - parser := New(logger, tagger, rules) + th := NewTestHelper(t) - latest, err := parser.FetchLatestSemverTag(testRepository.Repository, emptyProject) + parser := New(th.Ctx) + + latest, err := parser.FetchLatestSemverTag(testRepository.Repository, monorepo.Project{}) checkErr(t, "fetching latest semver tag", err) assert.Nil(latest, "latest semver tag should be nil") @@ -109,9 +104,10 @@ func TestParser_FetchLatestSemverTag_OneTag(t *testing.T) { err = testRepository.AddTag(tagName, head.Hash()) checkErr(t, "creating tag", err) - parser := New(logger, tagger, rules) + th := NewTestHelper(t) + parser := New(th.Ctx) - latest, err := parser.FetchLatestSemverTag(testRepository.Repository, emptyProject) + latest, err := parser.FetchLatestSemverTag(testRepository.Repository, monorepo.Project{}) checkErr(t, "fetching latest semver tag", err) assert.Equal(tagName, latest.Name, "latest semver tagName should be equal") @@ -139,9 +135,10 @@ func TestParser_FetchLatestSemverTag_MultipleTags(t *testing.T) { checkErr(t, "creating tag", err) } - parser := New(logger, tagger, rules) + th := NewTestHelper(t) + parser := New(th.Ctx) - latest, err := parser.FetchLatestSemverTag(testRepository.Repository, emptyProject) + latest, err := parser.FetchLatestSemverTag(testRepository.Repository, monorepo.Project{}) checkErr(t, "fetching latest semver tag", err) want := "3.0.0" @@ -158,10 +155,10 @@ func TestParser_ComputeNewSemver_UntaggedRepository_NoRelease(t *testing.T) { _ = testRepository.Remove() }) - parser := New(logger, tagger, rules) - parser.SetBranch("master") + th := NewTestHelper(t) + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver", err) want := "0.0.0" @@ -182,10 +179,10 @@ func TestParser_ComputeNewSemver_UntaggedRepository_PatchRelease(t *testing.T) { _, err = testRepository.AddCommit("fix") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules) - parser.SetBranch("master") + th := NewTestHelper(t) + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver", err) want := "0.0.1" @@ -212,10 +209,11 @@ func TestParser_ComputeNewSemver_UnknownReleaseType(t *testing.T) { }, } - parser := New(logger, tagger, invalidRules) - parser.SetBranch("master") + th := NewTestHelper(t) + th.Ctx.Rules = invalidRules + parser := New(th.Ctx) - _, err = parser.ComputeNewSemver(testRepository.Repository, emptyProject) + _, err = parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) assert.ErrorContains(err, "unknown release type") } @@ -232,10 +230,10 @@ func TestParser_ComputeNewSemver_UntaggedRepository_MinorRelease(t *testing.T) { _, err = testRepository.AddCommit("feat") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules) - parser.SetBranch("master") + th := NewTestHelper(t) + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver", err) want := "0.1.0" @@ -255,10 +253,10 @@ func TestParser_ComputeNewSemver_UntaggedRepository_MajorRelease(t *testing.T) { _, err = testRepository.AddCommit("feat!") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules) - parser.SetBranch("master") + th := NewTestHelper(t) + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver ", err) want := "1.0.0" @@ -288,10 +286,10 @@ func TestParser_ComputeNewSemver_TaggedRepository(t *testing.T) { _, err = testRepository.AddCommit("fix") // 1.1.1 checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules) - parser.SetBranch("master") + th := NewTestHelper(t) + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver ", err) want := "1.1.1" @@ -313,9 +311,10 @@ func TestParser_ComputeNewSemver_UninitializedRepository(t *testing.T) { repository, err := git.PlainInit(tempPath, false) checkErr(t, "initializing repository", err) - parser := New(logger, tagger, rules) + th := NewTestHelper(t) + parser := New(th.Ctx) - _, err = parser.ComputeNewSemver(repository, emptyProject) + _, err = parser.ComputeNewSemver(repository, monorepo.Project{}, th.Ctx.Branches[0]) assert.ErrorIs(err, plumbing.ErrReferenceNotFound) } @@ -332,10 +331,11 @@ func TestParser_ComputeNewSemver_BuildMetadata(t *testing.T) { _, err = testRepository.AddCommit("feat") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules, WithBuildMetadata("metadata")) - parser.SetBranch("master") + th := NewTestHelper(t) + th.Ctx.BuildMetadataFlag = "metadata" + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver", err) want := semver.Version{ @@ -364,12 +364,11 @@ func TestParser_ComputeNewSemver_Prerelease(t *testing.T) { prereleaseID := "master" - parser := New(logger, tagger, rules) - parser.SetBranch("master") - parser.SetPrerelease(true) - parser.SetPrereleaseIdentifier("master") + th := NewTestHelper(t) + th.Ctx.Branches[0].Prerelease = true + parser := New(th.Ctx) - output, err := parser.ComputeNewSemver(testRepository.Repository, emptyProject) + output, err := parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}, th.Ctx.Branches[0]) checkErr(t, "computing new semver", err) want := semver.Version{ @@ -383,7 +382,8 @@ func TestParser_ComputeNewSemver_Prerelease(t *testing.T) { assert.Equal(true, output.NewRelease, "boolean should be equal") } -func TestParser_Run_NoMonorepo(t *testing.T) { +// FIXME: the "origin" name is not set when calling parser.checkoutBranch leaving remoteRef like "ref/remote// +func TestParser_Run_NoMonorepoOutputLength(t *testing.T) { assert := assertion.New(t) testRepository, err := gittest.NewRepository() @@ -396,10 +396,13 @@ func TestParser_Run_NoMonorepo(t *testing.T) { _, err = testRepository.AddCommit("feat") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules) - parser.SetBranch("master") + clonedTestRepository, err := testRepository.Clone() + checkErr(t, "cloning test repository", err) + + th := NewTestHelper(t) + parser := New(th.Ctx) - output, err := parser.Run(context.Background(), testRepository.Repository) + output, err := parser.Run(context.Background(), clonedTestRepository.Repository) checkErr(t, "computing new semver", err) want := semver.Version{ @@ -412,46 +415,6 @@ func TestParser_Run_NoMonorepo(t *testing.T) { assert.Equal(want.String(), output[0].Semver.String(), "version should be equal") } -func TestParser_Run_Monorepo(t *testing.T) { - assert := assertion.New(t) - - testRepository, err := gittest.NewRepository() - checkErr(t, "creating repository", err) - - t.Cleanup(func() { - _ = testRepository.Remove() - }) - - // Add commit for "foo" project - _, err = testRepository.AddCommitWithSpecificFile("feat!", "./foo/foo.txt") - checkErr(t, "adding commit", err) - - _, err = testRepository.AddCommitWithSpecificFile("feat", "./foo/foo2.txt") - checkErr(t, "adding commit", err) - - _, err = testRepository.AddCommitWithSpecificFile("fix", "./foo/foo3.txt") - checkErr(t, "adding commit", err) - - // Add commit for "bar" project - _, err = testRepository.AddCommitWithSpecificFile("feat", "./bar/xyz/bar.txt") - checkErr(t, "adding commit", err) - - _, err = testRepository.AddCommitWithSpecificFile("fix", "./bar/bar2.txt") - checkErr(t, "adding commit", err) - - // Add commit for unrelated project - _, err = testRepository.AddCommitWithSpecificFile("feat", "./unrelated/temp.txt") - checkErr(t, "adding commit", err) - - parser := New(logger, tagger, rules, WithProjects(projects)) - parser.SetBranch("master") - - output, err := parser.Run(context.Background(), testRepository.Repository) - checkErr(t, "computing new semver", err) - - assert.Len(output, 2, "parser run output should contain one element") -} - func TestParser_ShortMessage(t *testing.T) { assert := assertion.New(t) @@ -481,9 +444,14 @@ func TestMonorepoParser_FetchLatestSemverTagPerProjects(t *testing.T) { err = testRepository.AddTag(wantTag, head.Hash()) checkErr(t, fmt.Sprintf("creating tag %q", wantTag), err) - parser := New(logger, tagger, rules, WithProjects(projects)) + th := NewTestHelper(t) + th.Ctx.Projects = []monorepo.Project{ + {Name: "foo", Path: "foo"}, + {Name: "bar", Path: "bar"}, + } + parser := New(th.Ctx) - gotTag, err := parser.FetchLatestSemverTag(testRepository.Repository, projects[0]) + gotTag, err := parser.FetchLatestSemverTag(testRepository.Repository, th.Ctx.Projects[0]) checkErr(t, "fetching latest semver tag", err) assert.Equal(gotTag.Name, wantTag, "should have found tag") @@ -533,7 +501,7 @@ func TestMonorepoParser_CommitContainsProjectFiles_False(t *testing.T) { assert.False(contains, "commit does not contain project files") } -func TestMonorepoParser_Run(t *testing.T) { +func TestParser_Run_Monorepo(t *testing.T) { assert := assertion.New(t) testRepository, err := gittest.NewRepository() @@ -563,10 +531,17 @@ func TestMonorepoParser_Run(t *testing.T) { _, err = testRepository.AddCommitWithSpecificFile("fix", "./temp/abc/b.txt") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules, WithProjects(projects)) - parser.SetBranch("master") + th := NewTestHelper(t) + th.Ctx.Projects = []monorepo.Project{ + {Name: "foo", Path: "foo"}, + {Name: "bar", Path: "bar"}, + } + parser := New(th.Ctx) + + clonedTestRepository, err := testRepository.Clone() + checkErr(t, "cloning test repository", err) - output, err := parser.Run(context.Background(), testRepository.Repository) + output, err := parser.Run(context.Background(), clonedTestRepository.Repository) checkErr(t, "computing projects new semver", err) assert.Len(output, 2, "parser run output should contain two elements") @@ -577,7 +552,7 @@ func TestMonorepoParser_Run(t *testing.T) { assert.Contains(gotSemver, "0.1.2") } -func TestMonorepoParser_Run_WithPreexistingTags(t *testing.T) { +func TestParser_Run_MonorepoWithPreexistingTags(t *testing.T) { assert := assertion.New(t) testRepository, err := gittest.NewRepository() @@ -621,10 +596,17 @@ func TestMonorepoParser_Run_WithPreexistingTags(t *testing.T) { _, err = testRepository.AddCommitWithSpecificFile("fix", "./temp/abc/b.txt") checkErr(t, "adding commit", err) - parser := New(logger, tagger, rules, WithProjects(projects)) - parser.SetBranch("master") + th := NewTestHelper(t) + th.Ctx.Projects = []monorepo.Project{ + {Name: "foo", Path: "foo"}, + {Name: "bar", Path: "bar"}, + } + parser := New(th.Ctx) + + clonedTestRepository, err := testRepository.Clone() + checkErr(t, "cloning test repository", err) - output, err := parser.Run(context.Background(), testRepository.Repository) + output, err := parser.Run(context.Background(), clonedTestRepository.Repository) checkErr(t, "computing projects new semver", err) assert.Len(output, 2, "parser run output should contain two elements") @@ -645,11 +627,13 @@ func TestParser_Run_InvalidBranch(t *testing.T) { _ = testRepository.Remove() }) - parser := New(logger, tagger, rules) - parser.SetBranch("branch_that_does_not_exist") + th := NewTestHelper(t) + th.Ctx.Branches = []branch.Branch{{Name: "does_not_exist"}} + + parser := New(th.Ctx) _, err = parser.Run(context.Background(), testRepository.Repository) - assert.ErrorContains(err, "does not exist", "parser run should have failed since branch does not exist") + assert.ErrorIs(err, plumbing.ErrReferenceNotFound, "parser run should have failed since branch does not exist") } func checkErr(t *testing.T, msg string, err error) { @@ -658,3 +642,46 @@ func checkErr(t *testing.T, msg string, err error) { t.Fatalf("%s: %s", msg, err.Error()) } } + +/* +func BenchmarkParser_ComputeNewSemver(b *testing.B) { + + parser := New(logger, rules) + testRepository, err := gittest.NewRepository() + if err != nil { + b.Fatalf("creating test repository: %s", err) + } + + b.Cleanup(func() { + os.RemoveAll(testRepository.Path) + }) + + commitTypes := []string{"feat", "fix", "chore"} + + for i := 1; i <= 10000; i++ { + commitType := commitTypes[rand.Intn(len(commitTypes))] + testRepository.AddCommit(commitType) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + parser.ComputeNewSemver(testRepository.Repository, monorepo.Project{}) + } + } +*/ +type TestHelper struct { + Ctx *appcontext.AppContext +} + +func NewTestHelper(t *testing.T) *TestHelper { + ctx := &appcontext.AppContext{ + Rules: rule.Default, + RemoteNameFlag: "origin", + Branches: []branch.Branch{{Name: "master"}}, + Logger: zerolog.New(io.Discard), + } + + return &TestHelper{ + Ctx: ctx, + } +} diff --git a/internal/remote/remote.go b/internal/remote/remote.go index 91a08c6..c895f6f 100644 --- a/internal/remote/remote.go +++ b/internal/remote/remote.go @@ -12,9 +12,9 @@ import ( ) type Remote struct { - name string auth *http.BasicAuth repository *git.Repository + name string } func New(name string, token string) *Remote { diff --git a/internal/remote/remote_test.go b/internal/remote/remote_test.go index 2bc6cc1..b38f281 100644 --- a/internal/remote/remote_test.go +++ b/internal/remote/remote_test.go @@ -3,13 +3,13 @@ package remote import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/s0ders/go-semver-release/v5/internal/tag" + "github.com/s0ders/go-semver-release/v6/internal/tag" "testing" "time" assertion "github.com/stretchr/testify/assert" - "github.com/s0ders/go-semver-release/v5/internal/gittest" + "github.com/s0ders/go-semver-release/v6/internal/gittest" ) func TestRemote_Clone_HappyScenario(t *testing.T) { diff --git a/internal/rule/flag.go b/internal/rule/flag.go index f87cb45..6385c68 100644 --- a/internal/rule/flag.go +++ b/internal/rule/flag.go @@ -9,7 +9,7 @@ import ( type Flag map[string][]string -const FlagType = "ruleFlag" +const FlagType = "JSON string" func (f *Flag) String() string { if f == nil || len(*f) == 0 { diff --git a/internal/tag/tag.go b/internal/tag/tag.go index 403b970..e5787f8 100644 --- a/internal/tag/tag.go +++ b/internal/tag/tag.go @@ -11,7 +11,7 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/s0ders/go-semver-release/v5/internal/semver" + "github.com/s0ders/go-semver-release/v6/internal/semver" ) var ErrTagAlreadyExists = errors.New("tag already exists") @@ -90,11 +90,7 @@ func (t *Tagger) TagRepository(repository *git.Repository, semver *semver.Versio return fmt.Errorf("semver is nil") } - tagMessage := t.TagPrefix + semver.String() - - if t.ProjectName != "" { - tagMessage = t.ProjectName + "-" + tagMessage - } + tagMessage := t.Format(semver) tagOpts := &git.CreateTagOptions{ Message: tagMessage, @@ -116,5 +112,11 @@ func (t *Tagger) TagRepository(repository *git.Repository, semver *semver.Versio } func (t *Tagger) Format(semver *semver.Version) string { - return t.TagPrefix + semver.String() + tag := t.TagPrefix + semver.String() + + if t.ProjectName != "" { + tag = t.ProjectName + "-" + tag + } + + return tag } diff --git a/internal/tag/tag_test.go b/internal/tag/tag_test.go index 55ac747..3807a4a 100644 --- a/internal/tag/tag_test.go +++ b/internal/tag/tag_test.go @@ -11,8 +11,8 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" assertion "github.com/stretchr/testify/assert" - "github.com/s0ders/go-semver-release/v5/internal/gittest" - "github.com/s0ders/go-semver-release/v5/internal/semver" + "github.com/s0ders/go-semver-release/v6/internal/gittest" + "github.com/s0ders/go-semver-release/v6/internal/semver" ) var ( diff --git a/main.go b/main.go index 63c9efb..cac610c 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/s0ders/go-semver-release/v5/cmd" + "github.com/s0ders/go-semver-release/v6/cmd" ) func main() {