diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8fbca23..38a1f4c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.59.1 # renovate: datasource=github-releases depName=golangci/golangci-lint + version: v1.60.1 # renovate: datasource=github-releases depName=golangci/golangci-lint args: --timeout 5m @@ -37,8 +37,12 @@ jobs: go-version-file: go.mod - name: Run tests - run: go test -v -race -coverpkg=./... ./... + run: go test -v -race -coverpkg=./... -coverprofile=coverage.txt ./... + - name: Upload results to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} go-mod-tidy: runs-on: ubuntu-latest diff --git a/.github/workflows/releaser-pleaser.yaml b/.github/workflows/releaser-pleaser.yaml index 44690a0..e0ce818 100644 --- a/.github/workflows/releaser-pleaser.yaml +++ b/.github/workflows/releaser-pleaser.yaml @@ -9,10 +9,7 @@ on: - labeled - unlabeled -permissions: - contents: write - issues: write - pull-requests: write +permissions: {} jobs: releaser-pleaser: @@ -21,5 +18,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - run: env - - uses: ./ + - name: releaser-pleaser + uses: ./ + with: + token: ${{ secrets.RELEASER_PLEASER_TOKEN }} diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..b3e717d --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,27 @@ +linters: + presets: + - bugs + - error + - import + - metalinter + - module + - unused + + enable: + - testifylint + + disable: + # preset error + # These should probably be cleaned up at some point if we want to publish part of this as a library. + - err113 # Very annoying to define static errors everywhere + - wrapcheck # Very annoying to wrap errors everywhere + # preset import + - depguard + +linters-settings: + gci: + sections: + - standard + - default + - localmodule + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19df7c2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## [v0.2.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.2.0) +### Features + +- update version references in any files (#14) + +## [v0.1.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.1.0) +### This is the first release ever, so it also includes a lot of other functionality. + + +### Features + +- add github action (#1) diff --git a/action.yml b/action.yml index c84aa28..9d68042 100644 --- a/action.yml +++ b/action.yml @@ -12,14 +12,19 @@ inputs: description: 'GitHub token for creating and grooming release PRs, defaults to using secrets.GITHUB_TOKEN' required: false default: ${{ github.token }} + extra-files: + description: 'List of files that are scanned for version references.' + required: false + default: "" outputs: {} runs: using: 'docker' - image: ghcr.io/apricote/releaser-pleaser:v0.1.0 + image: ghcr.io/apricote/releaser-pleaser:v0.2.0 # x-releaser-pleaser-version args: - run - --forge=github - --branch=${{ inputs.branch }} + - --extra-files="${{ inputs.extra-files }}" env: GITHUB_TOKEN: ${{ inputs.token }} GITHUB_USER: "oauth2" diff --git a/changelog.go b/changelog.go index 40c65d4..286faf4 100644 --- a/changelog.go +++ b/changelog.go @@ -3,26 +3,17 @@ package rp import ( "bytes" _ "embed" - "fmt" "html/template" - "io" "log" - "os" - "regexp" - - "github.com/go-git/go-git/v5" ) const ( - ChangelogFile = "CHANGELOG.md" - ChangelogFileBuffer = "CHANGELOG.md.tmp" - ChangelogHeader = "# Changelog" + ChangelogFile = "CHANGELOG.md" + ChangelogHeader = "# Changelog" ) var ( changelogTemplate *template.Template - - headerRegex = regexp.MustCompile(`^# Changelog\n`) ) //go:embed changelog.md.tpl @@ -36,72 +27,16 @@ func init() { } } -func UpdateChangelogFile(wt *git.Worktree, newEntry string) error { - file, err := wt.Filesystem.OpenFile(ChangelogFile, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - return err - } - defer file.Close() - - content, err := io.ReadAll(file) - if err != nil { - return err - } - - headerIndex := headerRegex.FindIndex(content) - if headerIndex == nil && len(content) != 0 { - return fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") - } - if headerIndex != nil { - // Remove the header from the content - content = content[headerIndex[1]:] - } - - err = file.Truncate(0) - if err != nil { - return err - } - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return err - } - - _, err = file.Write([]byte(ChangelogHeader + "\n\n" + newEntry)) - if err != nil { - return err - } - - _, err = file.Write(content) - if err != nil { - return err - } - - // Close file to make sure it is written to disk. - err = file.Close() - if err != nil { - return err - } - - _, err = wt.Add(ChangelogFile) - if err != nil { - return err - } - - return nil -} - -func NewChangelogEntry(changesets []Changeset, version, link, prefix, suffix string) (string, error) { +func NewChangelogEntry(commits []AnalyzedCommit, version, link, prefix, suffix string) (string, error) { features := make([]AnalyzedCommit, 0) fixes := make([]AnalyzedCommit, 0) - for _, changeset := range changesets { - for _, commit := range changeset.ChangelogEntries { - switch commit.Type { - case "feat": - features = append(features, commit) - case "fix": - fixes = append(fixes, commit) - } + for _, commit := range commits { + switch commit.Type { + case "feat": + features = append(features, commit) + case "fix": + fixes = append(fixes, commit) } } diff --git a/changelog_test.go b/changelog_test.go index 91ffc85..3fabff8 100644 --- a/changelog_test.go +++ b/changelog_test.go @@ -1,106 +1,22 @@ package rp import ( - "io" "testing" - "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/apricote/releaser-pleaser/internal/testutils" ) func ptr[T any](input T) *T { return &input } -func TestUpdateChangelogFile(t *testing.T) { - tests := []struct { - name string - repoFn testutils.Repo - entry string - expectedContent string - wantErr assert.ErrorAssertionFunc - }{ - { - name: "empty repo", - repoFn: testutils.WithTestRepo(), - entry: "## v1.0.0\n", - expectedContent: "# Changelog\n\n## v1.0.0\n", - wantErr: assert.NoError, - }, - { - name: "repo with well-formatted changelog", - repoFn: testutils.WithTestRepo(testutils.WithCommit("feat: add changelog", testutils.WithFile(ChangelogFile, `# Changelog - -## v0.0.1 - -- Bazzle - -## v0.1.0 - -### Bazuuum -`))), - entry: "## v1.0.0\n\n- Version 1, juhu.\n", - expectedContent: `# Changelog - -## v1.0.0 - -- Version 1, juhu. - -## v0.0.1 - -- Bazzle - -## v0.1.0 - -### Bazuuum -`, - wantErr: assert.NoError, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - repo := tt.repoFn(t) - wt, err := repo.Worktree() - require.NoError(t, err, "failed to get worktree") - - err = UpdateChangelogFile(wt, tt.entry) - if !tt.wantErr(t, err) { - return - } - - wtStatus, err := wt.Status() - require.NoError(t, err, "failed to get worktree status") - - assert.Len(t, wtStatus, 1, "worktree status does not have the expected entry number") - - changelogFileStatus := wtStatus.File(ChangelogFile) - - assert.Equal(t, git.Unmodified, changelogFileStatus.Worktree, "unexpected file status in worktree") - assert.Equal(t, git.Added, changelogFileStatus.Staging, "unexpected file status in staging") - - changelogFile, err := wt.Filesystem.Open(ChangelogFile) - require.NoError(t, err) - defer changelogFile.Close() - - changelogFileContent, err := io.ReadAll(changelogFile) - require.NoError(t, err) - - assert.Equal(t, tt.expectedContent, string(changelogFileContent)) - }) - } -} - func Test_NewChangelogEntry(t *testing.T) { type args struct { - changesets []Changeset - version string - link string - prefix string - suffix string + analyzedCommits []AnalyzedCommit + version string + link string + prefix string + suffix string } tests := []struct { name string @@ -111,9 +27,9 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "empty", args: args{ - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{}}}, - version: "1.0.0", - link: "https://example.com/1.0.0", + analyzedCommits: []AnalyzedCommit{}, + version: "1.0.0", + link: "https://example.com/1.0.0", }, want: "## [1.0.0](https://example.com/1.0.0)", wantErr: assert.NoError, @@ -121,13 +37,13 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "single feature", args: args{ - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{ + analyzedCommits: []AnalyzedCommit{ { Commit: Commit{}, Type: "feat", Description: "Foobar!", }, - }}}, + }, version: "1.0.0", link: "https://example.com/1.0.0", }, @@ -137,13 +53,13 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "single fix", args: args{ - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{ + analyzedCommits: []AnalyzedCommit{ { Commit: Commit{}, Type: "fix", Description: "Foobar!", }, - }}}, + }, version: "1.0.0", link: "https://example.com/1.0.0", }, @@ -153,7 +69,7 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "multiple commits with scopes", args: args{ - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{ + analyzedCommits: []AnalyzedCommit{ { Commit: Commit{}, Type: "feat", @@ -176,7 +92,7 @@ func Test_NewChangelogEntry(t *testing.T) { Description: "So sad!", Scope: ptr("sad"), }, - }}}, + }, version: "1.0.0", link: "https://example.com/1.0.0", }, @@ -196,13 +112,13 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "prefix", args: args{ - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{ + analyzedCommits: []AnalyzedCommit{ { Commit: Commit{}, Type: "fix", Description: "Foobar!", }, - }}}, + }, version: "1.0.0", link: "https://example.com/1.0.0", prefix: "### Breaking Changes", @@ -219,13 +135,13 @@ func Test_NewChangelogEntry(t *testing.T) { { name: "suffix", args: args{ - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{ + analyzedCommits: []AnalyzedCommit{ { Commit: Commit{}, Type: "fix", Description: "Foobar!", }, - }}}, + }, version: "1.0.0", link: "https://example.com/1.0.0", suffix: "### Compatibility\n\nThis version is compatible with flux-compensator v2.2 - v2.9.", @@ -245,7 +161,7 @@ This version is compatible with flux-compensator v2.2 - v2.9. for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix) + got, err := NewChangelogEntry(tt.args.analyzedCommits, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix) if !tt.wantErr(t, err) { return } diff --git a/cmd/rp/cmd/run.go b/cmd/rp/cmd/run.go index 4b7c9e2..7661af5 100644 --- a/cmd/rp/cmd/run.go +++ b/cmd/rp/cmd/run.go @@ -1,21 +1,13 @@ package cmd import ( - "context" - "fmt" + "strings" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" "github.com/spf13/cobra" rp "github.com/apricote/releaser-pleaser" ) -const ( - RELEASER_PLEASER_BRANCH = "releaser-pleaser--branches--%s" -) - // runCmd represents the run command var runCmd = &cobra.Command{ Use: "run", @@ -23,10 +15,11 @@ var runCmd = &cobra.Command{ } var ( - flagForge string - flagBranch string - flagOwner string - flagRepo string + flagForge string + flagBranch string + flagOwner string + flagRepo string + flagExtraFiles string ) func init() { @@ -38,6 +31,7 @@ func init() { runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "") runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "") runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "") + runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "") } func run(cmd *cobra.Command, _ []string) error { @@ -50,319 +44,50 @@ func run(cmd *cobra.Command, _ []string) error { "repo", flagRepo, ) - var f rp.Forge + var forge rp.Forge forgeOptions := rp.ForgeOptions{ Repository: flagRepo, BaseBranch: flagBranch, } - switch flagForge { - //case "gitlab": - //f = rp.NewGitLab(forgeOptions) + switch flagForge { // nolint:gocritic // Will become a proper switch once gitlab is added + // case "gitlab": + // f = rp.NewGitLab(forgeOptions) case "github": logger.DebugContext(ctx, "using forge GitHub") - f = rp.NewGitHub(logger, &rp.GitHubOptions{ + forge = rp.NewGitHub(logger, &rp.GitHubOptions{ ForgeOptions: forgeOptions, Owner: flagOwner, Repo: flagRepo, }) } - err := ensureLabels(ctx, f) - if err != nil { - return fmt.Errorf("failed to ensure all labels exist: %w", err) - } + extraFiles := parseExtraFiles(flagExtraFiles) - err = createPendingReleases(ctx, f) - if err != nil { - return fmt.Errorf("failed to create pending releases: %w", err) - } + releaserPleaser := rp.New( + forge, + logger, + flagBranch, + rp.NewConventionalCommitsParser(), + rp.SemVerNextVersion, + extraFiles, + []rp.Updater{&rp.GenericUpdater{}}, + ) - changesets, releases, err := getChangesetsFromForge(ctx, f) - if err != nil { - return fmt.Errorf("failed to get changesets: %w", err) - } - - err = reconcileReleasePR(ctx, f, changesets, releases) - if err != nil { - return fmt.Errorf("failed to reconcile release pr: %w", err) - } - - return nil + return releaserPleaser.Run(ctx) } -func ensureLabels(ctx context.Context, forge rp.Forge) error { - return forge.EnsureLabelsExist(ctx, rp.Labels) -} - -func createPendingReleases(ctx context.Context, forge rp.Forge) error { - logger.InfoContext(ctx, "checking for pending releases") - prs, err := forge.PendingReleases(ctx) - if err != nil { - return err - } - - if len(prs) == 0 { - logger.InfoContext(ctx, "No pending releases found") - return nil - } - - logger.InfoContext(ctx, "Found pending releases", "length", len(prs)) - - for _, pr := range prs { - err = createPendingRelease(ctx, forge, pr) - if err != nil { - return err - } - } - - return nil -} - -func createPendingRelease(ctx context.Context, forge rp.Forge, pr *rp.ReleasePullRequest) error { - logger := logger.With("pr.id", pr.ID, "pr.title", pr.Title) - - if pr.ReleaseCommit == nil { - return fmt.Errorf("pull request is missing the merge commit") - } - - logger.Info("Creating release", "commit.hash", pr.ReleaseCommit.Hash) - - version, err := pr.Version() - if err != nil { - return err - } - - changelog, err := pr.ChangelogText() - if err != nil { - return err - } - - // TODO: pre-release & latest - - logger.DebugContext(ctx, "Creating release on forge") - err = forge.CreateRelease(ctx, *pr.ReleaseCommit, version, changelog, false, true) - if err != nil { - return fmt.Errorf("failed to create release on forge: %w", err) - } - logger.DebugContext(ctx, "created release", "release.title", version, "release.url", forge.ReleaseURL(version)) - - logger.DebugContext(ctx, "updating pr labels") - err = forge.SetPullRequestLabels(ctx, pr, []string{rp.LabelReleasePending}, []string{rp.LabelReleaseTagged}) - if err != nil { - return err - } - logger.DebugContext(ctx, "updated pr labels") - - logger.InfoContext(ctx, "Created release", "release.title", version, "release.url", forge.ReleaseURL(version)) - - return nil -} - -func getChangesetsFromForge(ctx context.Context, forge rp.Forge) ([]rp.Changeset, rp.Releases, error) { - releases, err := forge.LatestTags(ctx) - if err != nil { - return nil, rp.Releases{}, err - } - - if releases.Latest != nil { - logger.InfoContext(ctx, "found latest tag", "tag.hash", releases.Latest.Hash, "tag.name", releases.Latest.Name) - if releases.Stable != nil && releases.Latest.Hash != releases.Stable.Hash { - logger.InfoContext(ctx, "found stable tag", "tag.hash", releases.Stable.Hash, "tag.name", releases.Stable.Name) - } - } else { - logger.InfoContext(ctx, "no latest tag found") - } - - releasableCommits, err := forge.CommitsSince(ctx, releases.Stable) - if err != nil { - return nil, rp.Releases{}, err - } - - logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits)) - - changesets, err := forge.Changesets(ctx, releasableCommits) - if err != nil { - return nil, rp.Releases{}, err - } - - logger.InfoContext(ctx, "Found changesets", "length", len(changesets)) - - return changesets, releases, nil -} - -func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Changeset, releases rp.Releases) error { - rpBranch := fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch) - rpBranchRef := plumbing.NewBranchReferenceName(rpBranch) - // Check Forge for open PR - // Get any modifications from open PR - // Clone Repo - // Run Updaters + Changelog - // Upsert PR - pr, err := forge.PullRequestForBranch(ctx, fmt.Sprintf(RELEASER_PLEASER_BRANCH, flagBranch)) - if err != nil { - return err - } - - if pr != nil { - logger.InfoContext(ctx, "found existing release pull request", "pr.id", pr.ID, "pr.title", pr.Title) - } - - if len(changesets) == 0 { - if pr != nil { - logger.InfoContext(ctx, "closing existing pull requests, no changesets available", "pr.id", pr.ID, "pr.title", pr.Title) - err = forge.ClosePullRequest(ctx, pr) - if err != nil { - return err - } - } else { - logger.InfoContext(ctx, "No changesets available for release") - } - - return nil - } - - var releaseOverrides rp.ReleaseOverrides - if pr != nil { - releaseOverrides, err = pr.GetOverrides() - if err != nil { - return err - } - } - - versionBump := rp.VersionBumpFromChangesets(changesets) - nextVersion, err := releases.NextVersion(versionBump, releaseOverrides.NextVersionType) - if err != nil { - return err - } - logger.InfoContext(ctx, "next version", "version", nextVersion) - - logger.DebugContext(ctx, "cloning repository", "clone.url", forge.CloneURL()) - repo, err := rp.CloneRepo(ctx, forge.CloneURL(), flagBranch, forge.GitAuth()) - if err != nil { - return fmt.Errorf("failed to clone repository: %w", err) - } - worktree, err := repo.Worktree() - if err != nil { - return err - } - - if branch, _ := repo.Branch(rpBranch); branch != nil { - logger.DebugContext(ctx, "deleting previous releaser-pleaser branch locally", "branch.name", rpBranch) - if err = repo.DeleteBranch(rpBranch); err != nil { - return err - } - } - - if err = worktree.Checkout(&git.CheckoutOptions{ - Branch: rpBranchRef, - Create: true, - }); err != nil { - return fmt.Errorf("failed to check out branch: %w", err) - } - - err = rp.RunUpdater(ctx, nextVersion, worktree) - if err != nil { - return fmt.Errorf("failed to update files with new version: %w", err) - } - - changelogEntry, err := rp.NewChangelogEntry(changesets, nextVersion, forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix) - if err != nil { - return fmt.Errorf("failed to build changelog entry: %w", err) - } - - err = rp.UpdateChangelogFile(worktree, changelogEntry) - if err != nil { - return fmt.Errorf("failed to update changelog file: %w", err) - } - - releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", flagBranch, nextVersion) - releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{ - Author: rp.GitSignature(), - Committer: rp.GitSignature(), - }) - if err != nil { - return fmt.Errorf("failed to commit changes: %w", err) - } - - logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage) - - newReleasePRChanges := true - - // Check if anything changed in comparison to the remote branch (if exists) - if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(rp.GitRemoteName, rpBranch), false); err != nil { - if err.Error() != "reference not found" { - // "reference not found" is expected and we should always push - return err - } - } else { - remoteCommit, err := repo.CommitObject(remoteRef.Hash()) - if err != nil { - return err - } - - localCommit, err := repo.CommitObject(releaseCommitHash) - if err != nil { - return err - } - - diff, err := localCommit.PatchContext(ctx, remoteCommit) - if err != nil { - return err - } - - newReleasePRChanges = len(diff.FilePatches()) > 0 - } - - if newReleasePRChanges { - pushRefSpec := config.RefSpec(fmt.Sprintf( - "+%s:%s", - rpBranchRef, - // This needs to be the local branch name, not the remotes/origin ref - // See https://stackoverflow.com/a/75727620 - rpBranchRef, - )) - logger.DebugContext(ctx, "pushing branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String()) - if err = repo.PushContext(ctx, &git.PushOptions{ - RemoteName: rp.GitRemoteName, - RefSpecs: []config.RefSpec{pushRefSpec}, - Force: true, - Auth: forge.GitAuth(), - }); err != nil { - return fmt.Errorf("failed to push branch: %w", err) - } - - logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String()) - } else { - logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push") - } - - // Open/Update PR - if pr == nil { - pr, err = rp.NewReleasePullRequest(rpBranch, flagBranch, nextVersion, changelogEntry) - if err != nil { - return err - } - - err = forge.CreatePullRequest(ctx, pr) - if err != nil { - return err - } - logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID) - } else { - pr.SetTitle(flagBranch, nextVersion) - err = pr.SetDescription(changelogEntry) - if err != nil { - return err - } - - err = forge.UpdatePullRequest(ctx, pr) - if err != nil { - return err - } - logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID) - } - - return nil +func parseExtraFiles(input string) []string { + lines := strings.Split(input, "\n") + + extraFiles := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if len(line) > 0 { + extraFiles = append(extraFiles, line) + } + } + + return extraFiles } diff --git a/commits.go b/commits.go index 565deb7..f0c64e9 100644 --- a/commits.go +++ b/commits.go @@ -7,6 +7,19 @@ import ( "github.com/leodido/go-conventionalcommits/parser" ) +type Commit struct { + Hash string + Message string + + PullRequest *PullRequest +} + +type PullRequest struct { + ID int + Title string + Description string +} + type AnalyzedCommit struct { Commit Type string @@ -15,24 +28,36 @@ type AnalyzedCommit struct { BreakingChange bool } -func AnalyzeCommits(commits []Commit) ([]AnalyzedCommit, conventionalcommits.VersionBump, error) { +type CommitParser interface { + Analyze(commits []Commit) ([]AnalyzedCommit, error) +} + +type ConventionalCommitsParser struct { + machine conventionalcommits.Machine +} + +func NewConventionalCommitsParser() *ConventionalCommitsParser { parserMachine := parser.NewMachine( parser.WithBestEffort(), parser.WithTypes(conventionalcommits.TypesConventional), ) + return &ConventionalCommitsParser{ + machine: parserMachine, + } +} + +func (c *ConventionalCommitsParser) Analyze(commits []Commit) ([]AnalyzedCommit, error) { analyzedCommits := make([]AnalyzedCommit, 0, len(commits)) - highestVersionBump := conventionalcommits.UnknownVersion - for _, commit := range commits { - msg, err := parserMachine.Parse([]byte(commit.Message)) + msg, err := c.machine.Parse([]byte(commit.Message)) if err != nil { - return nil, conventionalcommits.UnknownVersion, fmt.Errorf("failed to parse message of commit %q: %w", commit.Hash, err) + return nil, fmt.Errorf("failed to parse message of commit %q: %w", commit.Hash, err) } conventionalCommit, ok := msg.(*conventionalcommits.ConventionalCommit) if !ok { - return nil, conventionalcommits.UnknownVersion, fmt.Errorf("unable to get ConventionalCommit from parser result: %T", msg) + return nil, fmt.Errorf("unable to get ConventionalCommit from parser result: %T", msg) } commitVersionBump := conventionalCommit.VersionBump(conventionalcommits.DefaultStrategy) @@ -47,11 +72,7 @@ func AnalyzeCommits(commits []Commit) ([]AnalyzedCommit, conventionalcommits.Ver }) } - if commitVersionBump > highestVersionBump { - // Get max version bump from all releasable commits - highestVersionBump = commitVersionBump - } } - return analyzedCommits, highestVersionBump, nil + return analyzedCommits, nil } diff --git a/commits_test.go b/commits_test.go index 0e686e6..e58a718 100644 --- a/commits_test.go +++ b/commits_test.go @@ -3,7 +3,6 @@ package rp import ( "testing" - "github.com/leodido/go-conventionalcommits" "github.com/stretchr/testify/assert" ) @@ -12,14 +11,12 @@ func TestAnalyzeCommits(t *testing.T) { name string commits []Commit expectedCommits []AnalyzedCommit - expectedBump conventionalcommits.VersionBump wantErr assert.ErrorAssertionFunc }{ { name: "empty commits", commits: []Commit{}, expectedCommits: []AnalyzedCommit{}, - expectedBump: conventionalcommits.UnknownVersion, wantErr: assert.NoError, }, { @@ -30,7 +27,6 @@ func TestAnalyzeCommits(t *testing.T) { }, }, expectedCommits: nil, - expectedBump: conventionalcommits.UnknownVersion, wantErr: assert.Error, }, { @@ -41,7 +37,6 @@ func TestAnalyzeCommits(t *testing.T) { }, }, expectedCommits: []AnalyzedCommit{}, - expectedBump: conventionalcommits.UnknownVersion, wantErr: assert.NoError, }, { @@ -61,8 +56,7 @@ func TestAnalyzeCommits(t *testing.T) { Description: "blabla", }, }, - expectedBump: conventionalcommits.PatchVersion, - wantErr: assert.NoError, + wantErr: assert.NoError, }, { name: "highest bump (minor)", @@ -86,8 +80,7 @@ func TestAnalyzeCommits(t *testing.T) { Description: "foobar", }, }, - expectedBump: conventionalcommits.MinorVersion, - wantErr: assert.NoError, + wantErr: assert.NoError, }, { @@ -113,19 +106,17 @@ func TestAnalyzeCommits(t *testing.T) { BreakingChange: true, }, }, - expectedBump: conventionalcommits.MajorVersion, - wantErr: assert.NoError, + wantErr: assert.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - analyzedCommits, versionBump, err := AnalyzeCommits(tt.commits) + analyzedCommits, err := NewConventionalCommitsParser().Analyze(tt.commits) if !tt.wantErr(t, err) { return } assert.Equal(t, tt.expectedCommits, analyzedCommits) - assert.Equal(t, tt.expectedBump, versionBump) }) } } diff --git a/forge.go b/forge.go index 8a3c9bb..1086564 100644 --- a/forge.go +++ b/forge.go @@ -19,18 +19,12 @@ const ( GitHubPerPageMax = 100 GitHubPRStateOpen = "open" GitHubPRStateClosed = "closed" - GitHubEnvAPIToken = "GITHUB_TOKEN" + GitHubEnvAPIToken = "GITHUB_TOKEN" // nolint:gosec // Not actually a hardcoded credential GitHubEnvUsername = "GITHUB_USER" GitHubEnvRepository = "GITHUB_REPOSITORY" GitHubLabelColor = "dedede" ) -type Changeset struct { - URL string - Identifier string - ChangelogEntries []AnalyzedCommit -} - type Forge interface { RepoURL() string CloneURL() string @@ -46,23 +40,35 @@ type Forge interface { // function should return all commits. CommitsSince(context.Context, *Tag) ([]Commit, error) - // Changesets looks up the Pull/Merge Requests for each commit, returning its parsed metadata. - Changesets(context.Context, []Commit) ([]Changeset, error) - - EnsureLabelsExist(context.Context, []string) error + // EnsureLabelsExist verifies that all desired labels are available on the repository. If labels are missing, they + // are created them. + EnsureLabelsExist(context.Context, []Label) error // PullRequestForBranch returns the open pull request between the branch and ForgeOptions.BaseBranch. If no open PR // exists, it returns nil. PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error) + // CreatePullRequest opens a new pull/merge request for the ReleasePullRequest. CreatePullRequest(context.Context, *ReleasePullRequest) error + + // UpdatePullRequest updates the pull/merge request identified through the ID of + // the ReleasePullRequest to the current description and title. UpdatePullRequest(context.Context, *ReleasePullRequest) error - SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []string) error + + // SetPullRequestLabels updates the pull/merge request identified through the ID of + // the ReleasePullRequest to the current labels. + SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error + + // ClosePullRequest closes the pull/merge request identified through the ID of + // the ReleasePullRequest, as it is no longer required. ClosePullRequest(context.Context, *ReleasePullRequest) error - PendingReleases(context.Context) ([]*ReleasePullRequest, error) + // PendingReleases returns a list of ReleasePullRequest. The list should contain all pull/merge requests that are + // merged and have the matching label. + PendingReleases(context.Context, Label) ([]*ReleasePullRequest, error) - CreateRelease(ctx context.Context, commit Commit, title, changelog string, prelease, latest bool) error + // CreateRelease creates a release on the Forge, pointing at the commit with the passed in details. + CreateRelease(ctx context.Context, commit Commit, title, changelog string, prerelease, latest bool) error } type ForgeOptions struct { @@ -169,10 +175,16 @@ func (g *GitHub) CommitsSince(ctx context.Context, tag *Tag) ([]Commit, error) { var commits = make([]Commit, 0, len(repositoryCommits)) for _, ghCommit := range repositoryCommits { - commits = append(commits, Commit{ + commit := Commit{ Hash: ghCommit.GetSHA(), Message: ghCommit.GetCommit().GetMessage(), - }) + } + commit.PullRequest, err = g.prForCommit(ctx, commit) + if err != nil { + return nil, fmt.Errorf("failed to check for commit pull request: %w", err) + } + + commits = append(commits, commit) } return commits, nil @@ -257,76 +269,52 @@ func (g *GitHub) commitsSinceInit(ctx context.Context) ([]*github.RepositoryComm return repositoryCommits, nil } -func (g *GitHub) Changesets(ctx context.Context, commits []Commit) ([]Changeset, error) { +func (g *GitHub) prForCommit(ctx context.Context, commit Commit) (*PullRequest, error) { // We naively look up the associated PR for each commit through the "List pull requests associated with a commit" // endpoint. This requires len(commits) requests. // Using the "List pull requests" endpoint might be faster, as it allows us to fetch 100 arbitrary PRs per request, // but worst case we need to look up all PRs made in the repository ever. - changesets := make([]Changeset, 0, len(commits)) + log := g.log.With("commit.hash", commit.Hash) + page := 1 + var associatedPRs []*github.PullRequest - for _, commit := range commits { - log := g.log.With("commit.hash", commit.Hash) - page := 1 - var associatedPRs []*github.PullRequest - - for { - log.Debug("fetching pull requests associated with commit", "page", page) - prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit( - ctx, g.options.Owner, g.options.Repo, - commit.Hash, &github.ListOptions{ - Page: page, - PerPage: GitHubPerPageMax, - }) - if err != nil { - return nil, err - } - - associatedPRs = append(associatedPRs, prs...) - - if page == resp.LastPage || resp.LastPage == 0 { - break - } - page = resp.NextPage - } - - var pullrequest *github.PullRequest - for _, pr := range associatedPRs { - // We only look for the PR that has this commit set as the "merge commit" => The result of squashing this branch onto main - if pr.GetMergeCommitSHA() == commit.Hash { - pullrequest = pr - break - } - } - if pullrequest == nil { - log.Warn("did not find associated pull request, not considering it for changesets") - // No pull request was found for this commit, nothing to do here - // TODO: We could also return the minimal changeset for this commit, so at least it would come up in the changelog. - continue - } - - log = log.With("pullrequest.id", pullrequest.GetID()) - - // TODO: Parse PR description for overrides - changelogEntries, _, err := AnalyzeCommits([]Commit{commit}) - if err != nil { - log.Warn("unable to parse changelog entries", "error", err) - continue - } - - if len(changelogEntries) > 0 { - changesets = append(changesets, Changeset{ - URL: pullrequest.GetHTMLURL(), - Identifier: fmt.Sprintf("#%d", pullrequest.GetNumber()), - ChangelogEntries: changelogEntries, + for { + log.Debug("fetching pull requests associated with commit", "page", page) + prs, resp, err := g.client.PullRequests.ListPullRequestsWithCommit( + ctx, g.options.Owner, g.options.Repo, + commit.Hash, &github.ListOptions{ + Page: page, + PerPage: GitHubPerPageMax, }) + if err != nil { + return nil, err } + + associatedPRs = append(associatedPRs, prs...) + + if page == resp.LastPage || resp.LastPage == 0 { + break + } + page = resp.NextPage } - return changesets, nil + var pullrequest *github.PullRequest + for _, pr := range associatedPRs { + // We only look for the PR that has this commit set as the "merge commit" => The result of squashing this branch onto main + if pr.GetMergeCommitSHA() == commit.Hash { + pullrequest = pr + break + } + } + if pullrequest == nil { + return nil, nil + } + + return gitHubPRToPullRequest(pullrequest), nil } -func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []string) error { +func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []Label) error { existingLabels := make([]string, 0, len(labels)) page := 1 @@ -354,12 +342,12 @@ func (g *GitHub) EnsureLabelsExist(ctx context.Context, labels []string) error { } for _, label := range labels { - if !slices.Contains(existingLabels, label) { + if !slices.Contains(existingLabels, string(label)) { g.log.Info("creating label in repository", "label.name", label) _, _, err := g.client.Issues.CreateLabel( ctx, g.options.Owner, g.options.Repo, &github.Label{ - Name: &label, + Name: Pointer(string(label)), Color: Pointer(GitHubLabelColor), }, ) @@ -422,7 +410,7 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) // TODO: String ID? pr.ID = ghPR.GetNumber() - err = g.SetPullRequestLabels(ctx, pr, []string{}, pr.Labels) + err = g.SetPullRequestLabels(ctx, pr, []Label{}, pr.Labels) if err != nil { return err } @@ -445,20 +433,25 @@ func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest) return nil } -func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []string) error { +func (g *GitHub) SetPullRequestLabels(ctx context.Context, pr *ReleasePullRequest, remove, add []Label) error { for _, label := range remove { _, err := g.client.Issues.RemoveLabelForIssue( ctx, g.options.Owner, g.options.Repo, - pr.ID, label, + pr.ID, string(label), ) if err != nil { return err } } + addString := make([]string, 0, len(add)) + for _, label := range add { + addString = append(addString, string(label)) + } + _, _, err := g.client.Issues.AddLabelsToIssue( ctx, g.options.Owner, g.options.Repo, - pr.ID, add, + pr.ID, addString, ) if err != nil { return err @@ -481,7 +474,7 @@ func (g *GitHub) ClosePullRequest(ctx context.Context, pr *ReleasePullRequest) e return nil } -func (g *GitHub) PendingReleases(ctx context.Context) ([]*ReleasePullRequest, error) { +func (g *GitHub) PendingReleases(ctx context.Context, pendingLabel Label) ([]*ReleasePullRequest, error) { page := 1 var prs []*ReleasePullRequest @@ -509,7 +502,7 @@ func (g *GitHub) PendingReleases(ctx context.Context) ([]*ReleasePullRequest, er for _, pr := range ghPRs { pending := slices.ContainsFunc(pr.Labels, func(l *github.Label) bool { - return l.GetName() == LabelReleasePending + return l.GetName() == string(pendingLabel) }) if !pending { continue @@ -558,10 +551,21 @@ func (g *GitHub) CreateRelease(ctx context.Context, commit Commit, title, change return nil } +func gitHubPRToPullRequest(pr *github.PullRequest) *PullRequest { + return &PullRequest{ + ID: pr.GetNumber(), + Title: pr.GetTitle(), + Description: pr.GetBody(), + } +} + func gitHubPRToReleasePullRequest(pr *github.PullRequest) *ReleasePullRequest { - labels := make([]string, 0, len(pr.Labels)) + labels := make([]Label, 0, len(pr.Labels)) for _, label := range pr.Labels { - labels = append(labels, label.GetName()) + labelName := Label(label.GetName()) + if slices.Contains(KnownLabels, Label(label.GetName())) { + labels = append(labels, labelName) + } } var releaseCommit *Commit diff --git a/git.go b/git.go index 7df742c..9131570 100644 --- a/git.go +++ b/git.go @@ -13,15 +13,9 @@ import ( ) const ( - CommitSearchDepth = 50 // TODO: Increase - GitRemoteName = "origin" + GitRemoteName = "origin" ) -type Commit struct { - Hash string - Message string -} - type Tag struct { Hash string Name string diff --git a/go.mod b/go.mod index e55cfa9..3be3b4e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/apricote/releaser-pleaser -go 1.22.4 +go 1.23.0 require ( github.com/blang/semver/v4 v4.0.0 @@ -14,11 +14,11 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/cyphar/filepath-securejoin v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -31,14 +31,12 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 385a5a2..c43a8f2 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -13,11 +13,11 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= +github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -75,8 +75,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -97,12 +97,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -110,13 +108,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -130,15 +126,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -146,14 +142,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/markdown/extensions/section.go b/internal/markdown/extensions/section.go index e85808a..dcca37d 100644 --- a/internal/markdown/extensions/section.go +++ b/internal/markdown/extensions/section.go @@ -12,8 +12,10 @@ import ( "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast" ) -var sectionStartRegex = regexp.MustCompile(`^`) -var sectionEndRegex = regexp.MustCompile(`^`) +var ( + sectionStartRegex = regexp.MustCompile(`^`) + sectionEndRegex = regexp.MustCompile(`^`) +) const ( sectionTrigger = "" ) -type sectionParser struct { -} +type sectionParser struct{} func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) { line, _ := reader.PeekLine() @@ -75,8 +76,7 @@ func (s *sectionParser) Trigger() []byte { return []byte(sectionTrigger) } -type section struct { -} +type section struct{} // Section is an extension that allow you to use group content under a shared parent ast node. var Section = §ion{} diff --git a/internal/markdown/renderer/markdown/renderer.go b/internal/markdown/renderer/markdown/renderer.go index 7a369e7..69b2883 100644 --- a/internal/markdown/renderer/markdown/renderer.go +++ b/internal/markdown/renderer/markdown/renderer.go @@ -149,7 +149,7 @@ func (r *Renderer) writeByte(w io.Writer, c byte) error { return nil } -// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the +// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefixes are added at the // beginning of each line. func (r *Renderer) writeString(w io.Writer, s string) (int, error) { n, err := r.write(w, []byte(s)) @@ -178,7 +178,7 @@ func (r *Renderer) popPrefix() { // OpenBlock ensures that each block begins on a new line, and that blank lines are inserted before blocks as // indicated by node.HasPreviousBlankLines. -func (r *Renderer) openBlock(w util.BufWriter, source []byte, node ast.Node) error { +func (r *Renderer) openBlock(w util.BufWriter, _ []byte, node ast.Node) error { r.openBlocks = append(r.openBlocks, blockState{ node: node, fresh: true, @@ -222,7 +222,7 @@ func (r *Renderer) closeBlock(w io.Writer) error { } // RenderDocument renders an *ast.Document node to the given BufWriter. -func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderDocument(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) { r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false return ast.WalkContinue, nil } @@ -331,7 +331,7 @@ func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node a return ast.WalkStop, fmt.Errorf(": %w", err) } if err := r.writeByte(w, '\n'); err != nil { - return ast.WalkStop, nil + return ast.WalkStop, fmt.Errorf(": %w", err) } // Write the contents of the fenced code block. @@ -594,7 +594,7 @@ func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node } // RenderEmphasis renders an *ast.Emphasis node to the given BufWriter. -func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderEmphasis(w util.BufWriter, _ []byte, node ast.Node, _ bool) (ast.WalkStatus, error) { em := node.(*ast.Emphasis) if _, err := r.writeString(w, strings.Repeat("*", em.Level)); err != nil { return ast.WalkStop, fmt.Errorf(": %w", err) @@ -663,7 +663,7 @@ func (r *Renderer) renderLinkOrImage(w util.BufWriter, open string, dest, title } // RenderImage renders an *ast.Image node to the given BufWriter. -func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderImage(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { img := node.(*ast.Image) if err := r.renderLinkOrImage(w, "![", img.Destination, img.Title, enter); err != nil { return ast.WalkStop, fmt.Errorf(": %w", err) @@ -672,7 +672,7 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e } // RenderLink renders an *ast.Link node to the given BufWriter. -func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderLink(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { link := node.(*ast.Link) if err := r.renderLinkOrImage(w, "[", link.Destination, link.Title, enter); err != nil { return ast.WalkStop, fmt.Errorf(": %w", err) @@ -724,7 +724,7 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en } // RenderString renders an *ast.String node to the given BufWriter. -func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderString(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { if !enter { return ast.WalkContinue, nil } @@ -801,7 +801,7 @@ func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node return ast.WalkContinue, nil } -func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderTableCell(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { if !enter { if node.NextSibling() != nil { if _, err := r.writeString(w, " | "); err != nil { @@ -813,14 +813,14 @@ func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Nod return ast.WalkContinue, nil } -func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderStrikethrough(w util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) { if _, err := r.writeString(w, "~~"); err != nil { return ast.WalkStop, fmt.Errorf(": %w", err) } return ast.WalkContinue, nil } -func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { +func (r *Renderer) renderTaskCheckBox(w util.BufWriter, _ []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { if enter { var fill byte = ' ' if task := node.(*exast.TaskCheckBox); task.IsChecked { diff --git a/internal/testutils/git.go b/internal/testutils/git.go index 3bfd053..f5721b6 100644 --- a/internal/testutils/git.go +++ b/internal/testutils/git.go @@ -11,25 +11,26 @@ import ( "github.com/stretchr/testify/require" ) -var ( - author = &object.Signature{ - Name: "releaser-pleaser", - When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC), - } -) +var author = &object.Signature{ + Name: "releaser-pleaser", + When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC), +} type CommitOption func(*commitOptions) + type commitOptions struct { cleanFiles bool files []commitFile tags []string } + type commitFile struct { path string content string } type Commit func(*testing.T, *git.Repository) error + type Repo func(*testing.T) *git.Repository func WithCommit(message string, options ...CommitOption) Commit { @@ -61,9 +62,9 @@ func WithCommit(message string, options ...CommitOption) Commit { for _, fileInfo := range opts.files { file, err := wt.Filesystem.Create(fileInfo.path) require.NoError(t, err, "failed to create file %q", fileInfo.path) - defer file.Close() _, err = file.Write([]byte(fileInfo.content)) + file.Close() require.NoError(t, err, "failed to write content to file %q", fileInfo.path) } @@ -83,7 +84,6 @@ func WithCommit(message string, options ...CommitOption) Commit { } return nil - } } diff --git a/releasepr.go b/releasepr.go index eadb3b0..a6744c4 100644 --- a/releasepr.go +++ b/releasepr.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "regexp" + "strings" "text/template" "github.com/yuin/goldmark/ast" @@ -30,11 +31,14 @@ func init() { } } +// ReleasePullRequest +// +// TODO: Reuse [PullRequest] type ReleasePullRequest struct { ID int Title string Description string - Labels []string + Labels []Label Head string ReleaseCommit *Commit @@ -43,11 +47,11 @@ type ReleasePullRequest struct { func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) { rp := &ReleasePullRequest{ Head: head, - Labels: []string{LabelReleasePending}, + Labels: []Label{LabelReleasePending}, } rp.SetTitle(branch, version) - if err := rp.SetDescription(changelogEntry); err != nil { + if err := rp.SetDescription(changelogEntry, ReleaseOverrides{}); err != nil { return nil, err } @@ -57,7 +61,7 @@ func NewReleasePullRequest(head, branch, version, changelogEntry string) (*Relea type ReleaseOverrides struct { Prefix string Suffix string - // TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the changesets + // TODO: Doing the changelog for normal releases after previews requires to know about this while fetching the commits NextVersionType NextVersionType } @@ -88,18 +92,20 @@ func (n NextVersionType) String() string { } } -// PR Labels -const ( - LabelNextVersionTypeNormal = "rp-next-version::normal" - LabelNextVersionTypeRC = "rp-next-version::rc" - LabelNextVersionTypeBeta = "rp-next-version::beta" - LabelNextVersionTypeAlpha = "rp-next-version::alpha" +// Label is the string identifier of a pull/merge request label on the forge. +type Label string - LabelReleasePending = "rp-release::pending" - LabelReleaseTagged = "rp-release::tagged" +const ( + LabelNextVersionTypeNormal Label = "rp-next-version::normal" + LabelNextVersionTypeRC Label = "rp-next-version::rc" + LabelNextVersionTypeBeta Label = "rp-next-version::beta" + LabelNextVersionTypeAlpha Label = "rp-next-version::alpha" + + LabelReleasePending Label = "rp-release::pending" + LabelReleaseTagged Label = "rp-release::tagged" ) -var Labels = []string{ +var KnownLabels = []Label{ LabelNextVersionTypeNormal, LabelNextVersionTypeRC, LabelNextVersionTypeBeta, @@ -115,7 +121,6 @@ const ( ) const ( - MarkdownSectionOverrides = "overrides" MarkdownSectionChangelog = "changelog" ) @@ -150,6 +155,9 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R overrides.NextVersionType = NextVersionTypeBeta case LabelNextVersionTypeAlpha: overrides.NextVersionType = NextVersionTypeAlpha + case LabelReleasePending, LabelReleaseTagged: + // These labels have no effect on the versioning. + break } } @@ -190,51 +198,6 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele return overrides, nil } -func (pr *ReleasePullRequest) overridesText() (string, error) { - source := []byte(pr.Description) - gm := markdown.New() - descriptionAST := gm.Parser().Parse(text.NewReader(source)) - - var section *east.Section - - err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkContinue, nil - } - - if n.Type() != ast.TypeBlock || n.Kind() != east.KindSection { - return ast.WalkContinue, nil - } - - anySection, ok := n.(*east.Section) - if !ok { - return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n) - } - - if anySection.Name != MarkdownSectionOverrides { - return ast.WalkContinue, nil - } - - section = anySection - return ast.WalkStop, nil - }) - if err != nil { - return "", err - } - - if section == nil { - return "", nil - } - - outputBuffer := new(bytes.Buffer) - err = gm.Renderer().Render(outputBuffer, source, section) - if err != nil { - return "", err - } - - return outputBuffer.String(), nil -} - func (pr *ReleasePullRequest) ChangelogText() (string, error) { source := []byte(pr.Description) gm := markdown.New() @@ -289,11 +252,11 @@ func textFromLines(source []byte, n ast.Node) string { content = append(content, line.Value(source)...) } - return string(content) + return strings.TrimSpace(string(content)) } func (pr *ReleasePullRequest) SetTitle(branch, version string) { - pr.Title = fmt.Sprintf("chore(%s): release %s", branch, version) + pr.Title = fmt.Sprintf(TitleFormat, branch, version) } func (pr *ReleasePullRequest) Version() (string, error) { @@ -305,14 +268,9 @@ func (pr *ReleasePullRequest) Version() (string, error) { return matches[2], nil } -func (pr *ReleasePullRequest) SetDescription(changelogEntry string) error { - overrides, err := pr.overridesText() - if err != nil { - return err - } - +func (pr *ReleasePullRequest) SetDescription(changelogEntry string, overrides ReleaseOverrides) error { var description bytes.Buffer - err = releasePRTemplate.Execute(&description, map[string]any{ + err := releasePRTemplate.Execute(&description, map[string]any{ "Changelog": changelogEntry, "Overrides": overrides, }) diff --git a/releasepr.md.tpl b/releasepr.md.tpl index e48872b..6f74aa0 100644 --- a/releasepr.md.tpl +++ b/releasepr.md.tpl @@ -1,29 +1,32 @@ ---- - {{ .Changelog }} --- -## releaser-pleaser Instructions -{{ if .Overrides }} -{{- .Overrides -}} -{{- else }} - -> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. +
+

PR by releaser-pleaser 🤖

-### Prefix +If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. + +## Release Notes + +### Prefix / Start + +This will be added to the start of the release notes. ```rp-prefix +{{- if .Overrides.Prefix }} +{{ .Overrides.Prefix }}{{ end }} ``` -### Suffix +### Suffix / End + +This will be added to the end of the release notes. ```rp-suffix +{{- if .Overrides.Suffix }} +{{ .Overrides.Suffix }}{{ end }} ``` - - -{{ end }} -#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser) +
diff --git a/releasepr_test.go b/releasepr_test.go index 124b063..b1ffbb2 100644 --- a/releasepr_test.go +++ b/releasepr_test.go @@ -49,121 +49,96 @@ func TestReleasePullRequest_SetDescription(t *testing.T) { tests := []struct { name string - pr *ReleasePullRequest changelogEntry string + overrides ReleaseOverrides want string wantErr assert.ErrorAssertionFunc }{ { - name: "empty description", - pr: &ReleasePullRequest{}, + name: "no overrides", changelogEntry: `## v1.0.0`, - want: `--- - - + overrides: ReleaseOverrides{}, + want: ` ## v1.0.0 --- -## releaser-pleaser Instructions +
+

PR by releaser-pleaser 🤖

- -> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. +If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. -### Prefix +## Release Notes + +### Prefix / Start + +This will be added to the start of the release notes. ` + "```" + `rp-prefix ` + "```" + ` -### Suffix +### Suffix / End + +This will be added to the end of the release notes. ` + "```" + `rp-suffix ` + "```" + ` - - - -#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser) +
`, wantErr: assert.NoError, }, { - name: "existing overrides", - pr: &ReleasePullRequest{ - Description: `--- - - -## v0.1.0 - -### Features - -- bedazzle - - ---- - -## releaser-pleaser Instructions - - -> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. - -### Prefix - -` + "```" + `rp-prefix -This release is awesome! -` + "```" + ` - -### Suffix - -` + "```" + `rp-suffix -` + "```" + ` - - - -#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser) -`, - }, + name: "existing overrides", changelogEntry: `## v1.0.0`, - want: `--- - - + overrides: ReleaseOverrides{ + Prefix: "This release is awesome!", + Suffix: "Fooo", + }, + want: ` ## v1.0.0 --- -## releaser-pleaser Instructions +
+

PR by releaser-pleaser 🤖

- -> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. +If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. -### Prefix +## Release Notes + +### Prefix / Start + +This will be added to the start of the release notes. ` + "```" + `rp-prefix This release is awesome! ` + "```" + ` -### Suffix +### Suffix / End + +This will be added to the end of the release notes. ` + "```" + `rp-suffix +Fooo ` + "```" + ` - - -#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser) +
`, wantErr: assert.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.pr.SetDescription(tt.changelogEntry) + pr := &ReleasePullRequest{} + err := pr.SetDescription(tt.changelogEntry, tt.overrides) if !tt.wantErr(t, err) { return } - assert.Equal(t, tt.want, tt.pr.Description) + assert.Equal(t, tt.want, pr.Description) }) } } diff --git a/releaserpleaser.go b/releaserpleaser.go new file mode 100644 index 0000000..5dfcf12 --- /dev/null +++ b/releaserpleaser.go @@ -0,0 +1,406 @@ +package rp + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" +) + +const ( + PullRequestBranchFormat = "releaser-pleaser--branches--%s" +) + +type ReleaserPleaser struct { + forge Forge + logger *slog.Logger + targetBranch string + commitParser CommitParser + nextVersion VersioningStrategy + extraFiles []string + updaters []Updater +} + +func New(forge Forge, logger *slog.Logger, targetBranch string, commitParser CommitParser, versioningStrategy VersioningStrategy, extraFiles []string, updaters []Updater) *ReleaserPleaser { + return &ReleaserPleaser{ + forge: forge, + logger: logger, + targetBranch: targetBranch, + commitParser: commitParser, + nextVersion: versioningStrategy, + extraFiles: extraFiles, + updaters: updaters, + } +} + +func (rp *ReleaserPleaser) EnsureLabels(ctx context.Context) error { + // TODO: Wrap Error + return rp.forge.EnsureLabelsExist(ctx, KnownLabels) +} + +func (rp *ReleaserPleaser) Run(ctx context.Context) error { + err := rp.runOnboarding(ctx) + if err != nil { + return fmt.Errorf("failed to onboard repository: %w", err) + } + + err = rp.runCreatePendingReleases(ctx) + if err != nil { + return fmt.Errorf("failed to create pending releases: %w", err) + } + + err = rp.runReconcileReleasePR(ctx) + if err != nil { + return fmt.Errorf("failed to reconcile release pull request: %w", err) + } + + return nil +} + +func (rp *ReleaserPleaser) runOnboarding(ctx context.Context) error { + err := rp.EnsureLabels(ctx) + if err != nil { + return fmt.Errorf("failed to ensure all labels exist: %w", err) + } + + return nil +} + +func (rp *ReleaserPleaser) runCreatePendingReleases(ctx context.Context) error { + logger := rp.logger.With("method", "runCreatePendingReleases") + + logger.InfoContext(ctx, "checking for pending releases") + prs, err := rp.forge.PendingReleases(ctx, LabelReleasePending) + if err != nil { + return err + } + + if len(prs) == 0 { + logger.InfoContext(ctx, "No pending releases found") + return nil + } + + logger.InfoContext(ctx, "Found pending releases", "length", len(prs)) + + for _, pr := range prs { + err = rp.createPendingRelease(ctx, pr) + if err != nil { + return err + } + } + + return nil +} + +func (rp *ReleaserPleaser) createPendingRelease(ctx context.Context, pr *ReleasePullRequest) error { + logger := rp.logger.With( + "method", "createPendingRelease", + "pr.id", pr.ID, + "pr.title", pr.Title) + + if pr.ReleaseCommit == nil { + return fmt.Errorf("pull request is missing the merge commit") + } + + logger.Info("Creating release", "commit.hash", pr.ReleaseCommit.Hash) + + version, err := pr.Version() + if err != nil { + return err + } + + changelog, err := pr.ChangelogText() + if err != nil { + return err + } + + // TODO: pre-release & latest + + logger.DebugContext(ctx, "Creating release on forge") + err = rp.forge.CreateRelease(ctx, *pr.ReleaseCommit, version, changelog, false, true) + if err != nil { + return fmt.Errorf("failed to create release on forge: %w", err) + } + logger.DebugContext(ctx, "created release", "release.title", version, "release.url", rp.forge.ReleaseURL(version)) + + logger.DebugContext(ctx, "updating pr labels") + err = rp.forge.SetPullRequestLabels(ctx, pr, []Label{LabelReleasePending}, []Label{LabelReleaseTagged}) + if err != nil { + return err + } + logger.DebugContext(ctx, "updated pr labels") + + logger.InfoContext(ctx, "Created release", "release.title", version, "release.url", rp.forge.ReleaseURL(version)) + + return nil +} + +func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error { + logger := rp.logger.With("method", "runReconcileReleasePR") + + releases, err := rp.forge.LatestTags(ctx) + if err != nil { + return err + } + + if releases.Latest != nil { + logger.InfoContext(ctx, "found latest tag", "tag.hash", releases.Latest.Hash, "tag.name", releases.Latest.Name) + if releases.Stable != nil && releases.Latest.Hash != releases.Stable.Hash { + logger.InfoContext(ctx, "found stable tag", "tag.hash", releases.Stable.Hash, "tag.name", releases.Stable.Name) + } + } else { + logger.InfoContext(ctx, "no latest tag found") + } + + releasableCommits, err := rp.forge.CommitsSince(ctx, releases.Stable) + if err != nil { + return err + } + + logger.InfoContext(ctx, "Found releasable commits", "length", len(releasableCommits)) + + // TODO: Handle commit overrides + analyzedCommits, err := rp.commitParser.Analyze(releasableCommits) + if err != nil { + return err + } + + logger.InfoContext(ctx, "Analyzed commits", "length", len(analyzedCommits)) + + rpBranch := fmt.Sprintf(PullRequestBranchFormat, rp.targetBranch) + rpBranchRef := plumbing.NewBranchReferenceName(rpBranch) + // Check Forge for open PR + // Get any modifications from open PR + // Clone Repo + // Run Updaters + Changelog + // Upsert PR + pr, err := rp.forge.PullRequestForBranch(ctx, rpBranch) + if err != nil { + return err + } + + if pr != nil { + logger.InfoContext(ctx, "found existing release pull request", "pr.id", pr.ID, "pr.title", pr.Title) + } + + if len(analyzedCommits) == 0 { + if pr != nil { + logger.InfoContext(ctx, "closing existing pull requests, no commits available", "pr.id", pr.ID, "pr.title", pr.Title) + err = rp.forge.ClosePullRequest(ctx, pr) + if err != nil { + return err + } + } else { + logger.InfoContext(ctx, "No commits available for release") + } + + return nil + } + + var releaseOverrides ReleaseOverrides + if pr != nil { + releaseOverrides, err = pr.GetOverrides() + if err != nil { + return err + } + } + + versionBump := VersionBumpFromCommits(analyzedCommits) + // TODO: Set version in release pr + nextVersion, err := rp.nextVersion(releases, versionBump, releaseOverrides.NextVersionType) + if err != nil { + return err + } + logger.InfoContext(ctx, "next version", "version", nextVersion) + + logger.DebugContext(ctx, "cloning repository", "clone.url", rp.forge.CloneURL()) + repo, err := CloneRepo(ctx, rp.forge.CloneURL(), rp.targetBranch, rp.forge.GitAuth()) + if err != nil { + return fmt.Errorf("failed to clone repository: %w", err) + } + worktree, err := repo.Worktree() + if err != nil { + return err + } + + if branch, _ := repo.Branch(rpBranch); branch != nil { + logger.DebugContext(ctx, "deleting previous releaser-pleaser branch locally", "branch.name", rpBranch) + if err = repo.DeleteBranch(rpBranch); err != nil { + return err + } + } + + if err = worktree.Checkout(&git.CheckoutOptions{ + Branch: rpBranchRef, + Create: true, + }); err != nil { + return fmt.Errorf("failed to check out branch: %w", err) + } + + changelogEntry, err := NewChangelogEntry(analyzedCommits, nextVersion, rp.forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix) + if err != nil { + return fmt.Errorf("failed to build changelog entry: %w", err) + } + + // Info for updaters + info := ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry} + + updateFile := func(path string, updaters []Updater) error { + file, err := worktree.Filesystem.OpenFile(path, os.O_RDWR, 0) + if err != nil { + return err + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + updatedContent := string(content) + + for _, updater := range updaters { + updatedContent, err = updater.UpdateContent(updatedContent, info) + if err != nil { + return fmt.Errorf("failed to run updater %T on file %s", updater, path) + } + } + + err = file.Truncate(0) + if err != nil { + return fmt.Errorf("failed to replace file content: %w", err) + } + _, err = file.Seek(0, 0) + if err != nil { + return fmt.Errorf("failed to replace file content: %w", err) + } + _, err = file.Write([]byte(updatedContent)) + if err != nil { + return fmt.Errorf("failed to replace file content: %w", err) + } + + _, err = worktree.Add(path) + if err != nil { + return fmt.Errorf("failed to add updated file to git worktree: %w", err) + } + + return nil + } + + err = updateFile(ChangelogFile, []Updater{&ChangelogUpdater{}}) + if err != nil { + return fmt.Errorf("failed to update changelog file: %w", err) + } + + for _, path := range rp.extraFiles { + _, err = worktree.Filesystem.Stat(path) + if err != nil { + // TODO: Check for non existing file or dirs + return fmt.Errorf("failed to run file updater because the file %s does not exist: %w", path, err) + } + + err = updateFile(path, rp.updaters) + if err != nil { + return fmt.Errorf("failed to run file updater: %w", err) + } + } + + releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion) + releaseCommitHash, err := worktree.Commit(releaseCommitMessage, &git.CommitOptions{ + Author: GitSignature(), + Committer: GitSignature(), + }) + if err != nil { + return fmt.Errorf("failed to commit changes: %w", err) + } + + logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommitHash.String(), "commit.message", releaseCommitMessage) + + newReleasePRChanges := true + + // Check if anything changed in comparison to the remote branch (if exists) + if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName(GitRemoteName, rpBranch), false); err != nil { + if err.Error() != "reference not found" { + // "reference not found" is expected and we should always push + return err + } + } else { + remoteCommit, err := repo.CommitObject(remoteRef.Hash()) + if err != nil { + return err + } + + localCommit, err := repo.CommitObject(releaseCommitHash) + if err != nil { + return err + } + + diff, err := localCommit.PatchContext(ctx, remoteCommit) + if err != nil { + return err + } + + newReleasePRChanges = len(diff.FilePatches()) > 0 + } + + if newReleasePRChanges { + pushRefSpec := config.RefSpec(fmt.Sprintf( + "+%s:%s", + rpBranchRef, + // This needs to be the local branch name, not the remotes/origin ref + // See https://stackoverflow.com/a/75727620 + rpBranchRef, + )) + logger.DebugContext(ctx, "pushing branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String()) + if err = repo.PushContext(ctx, &git.PushOptions{ + RemoteName: GitRemoteName, + RefSpecs: []config.RefSpec{pushRefSpec}, + Force: true, + Auth: rp.forge.GitAuth(), + }); err != nil { + return fmt.Errorf("failed to push branch: %w", err) + } + + logger.InfoContext(ctx, "pushed branch", "commit.hash", releaseCommitHash.String(), "branch.name", rpBranch, "refspec", pushRefSpec.String()) + } else { + logger.InfoContext(ctx, "file content is already up-to-date in remote branch, skipping push") + } + + // Open/Update PR + if pr == nil { + pr, err = NewReleasePullRequest(rpBranch, rp.targetBranch, nextVersion, changelogEntry) + if err != nil { + return err + } + + err = rp.forge.CreatePullRequest(ctx, pr) + if err != nil { + return err + } + logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID) + } else { + pr.SetTitle(rp.targetBranch, nextVersion) + + overrides, err := pr.GetOverrides() + if err != nil { + return err + } + err = pr.SetDescription(changelogEntry, overrides) + if err != nil { + return err + } + + err = rp.forge.UpdatePullRequest(ctx, pr) + if err != nil { + return err + } + logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID) + } + + return nil +} diff --git a/updater.go b/updater.go index ce01d21..3aaedae 100644 --- a/updater.go +++ b/updater.go @@ -1,12 +1,47 @@ package rp import ( - "context" - - "github.com/go-git/go-git/v5" + "fmt" + "regexp" + "strings" ) -func RunUpdater(ctx context.Context, version string, worktree *git.Worktree) error { - // TODO: Implement updater for Go,Python,ExtraFilesMarkers - return nil +var ( + GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`) + ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`) +) + +type ReleaseInfo struct { + Version string + ChangelogEntry string +} + +type Updater interface { + UpdateContent(content string, info ReleaseInfo) (string, error) +} + +type GenericUpdater struct{} + +func (u *GenericUpdater) UpdateContent(content string, info ReleaseInfo) (string, error) { + // We strip the "v" prefix to avoid adding/removing it from the users input. + version := strings.TrimPrefix(info.Version, "v") + + return GenericUpdaterSemVerRegex.ReplaceAllString(content, version+"${2}"), nil +} + +type ChangelogUpdater struct{} + +func (u *ChangelogUpdater) UpdateContent(content string, info ReleaseInfo) (string, error) { + headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content) + if headerIndex == nil && len(content) != 0 { + return "", fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") + } + if headerIndex != nil { + // Remove the header from the content + content = content[headerIndex[1]:] + } + + content = ChangelogHeader + "\n\n" + info.ChangelogEntry + content + + return content, nil } diff --git a/updater_test.go b/updater_test.go new file mode 100644 index 0000000..c0e1419 --- /dev/null +++ b/updater_test.go @@ -0,0 +1,129 @@ +package rp + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type updaterTestCase struct { + name string + content string + info ReleaseInfo + want string + wantErr assert.ErrorAssertionFunc +} + +func runUpdaterTest(t *testing.T, updater Updater, tt updaterTestCase) { + t.Helper() + + got, err := updater.UpdateContent(tt.content, tt.info) + if !tt.wantErr(t, err, fmt.Sprintf("UpdateContent(%v, %v)", tt.content, tt.info)) { + return + } + assert.Equalf(t, tt.want, got, "UpdateContent(%v, %v)", tt.content, tt.info) +} + +func TestGenericUpdater_UpdateContent(t *testing.T) { + updater := &GenericUpdater{} + + tests := []updaterTestCase{ + { + name: "single line", + content: "v1.0.0 // x-releaser-pleaser-version", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "v1.2.0 // x-releaser-pleaser-version", + wantErr: assert.NoError, + }, + { + name: "multiline line", + content: "Foooo\n\v1.2.0\nv1.0.0 // x-releaser-pleaser-version\n", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "Foooo\n\v1.2.0\nv1.2.0 // x-releaser-pleaser-version\n", + wantErr: assert.NoError, + }, + { + name: "invalid existing version", + content: "1.0 // x-releaser-pleaser-version", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "1.0 // x-releaser-pleaser-version", + wantErr: assert.NoError, + }, + { + name: "complicated line", + content: "version: v1.2.0-alpha.1 => Awesome, isnt it? x-releaser-pleaser-version foobar", + info: ReleaseInfo{ + Version: "v1.2.0", + }, + want: "version: v1.2.0 => Awesome, isnt it? x-releaser-pleaser-version foobar", + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runUpdaterTest(t, updater, tt) + }) + } +} + +func TestChangelogUpdater_UpdateContent(t *testing.T) { + updater := &ChangelogUpdater{} + + tests := []updaterTestCase{ + { + name: "empty file", + content: "", + info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n"}, + want: "# Changelog\n\n## v1.0.0\n", + wantErr: assert.NoError, + }, + { + name: "well-formatted changelog", + content: `# Changelog + +## v0.0.1 + +- Bazzle + +## v0.1.0 + +### Bazuuum +`, + info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"}, + want: `# Changelog + +## v1.0.0 + +- Version 1, juhu. + +## v0.0.1 + +- Bazzle + +## v0.1.0 + +### Bazuuum +`, + wantErr: assert.NoError, + }, + { + name: "error on invalid header", + content: "What even is this file?", + info: ReleaseInfo{ChangelogEntry: "## v1.0.0\n\n- Version 1, juhu.\n"}, + want: "", + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runUpdaterTest(t, updater, tt) + }) + } +} diff --git a/versioning.go b/versioning.go index 126e06f..176a28d 100644 --- a/versioning.go +++ b/versioning.go @@ -13,7 +13,11 @@ type Releases struct { Stable *Tag } -func (r Releases) NextVersion(versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) { +type VersioningStrategy = func(Releases, conventionalcommits.VersionBump, NextVersionType) (string, error) + +var _ VersioningStrategy = SemVerNextVersion + +func SemVerNextVersion(r Releases, versionBump conventionalcommits.VersionBump, nextVersionType NextVersionType) (string, error) { latest, err := parseSemverWithDefault(r.Latest) if err != nil { return "", fmt.Errorf("failed to parse latest version: %w", err) @@ -65,24 +69,22 @@ func (r Releases) NextVersion(versionBump conventionalcommits.VersionBump, nextV return "v" + next.String(), nil } -func VersionBumpFromChangesets(changesets []Changeset) conventionalcommits.VersionBump { +func VersionBumpFromCommits(commits []AnalyzedCommit) conventionalcommits.VersionBump { bump := conventionalcommits.UnknownVersion - for _, changeset := range changesets { - for _, entry := range changeset.ChangelogEntries { - entryBump := conventionalcommits.UnknownVersion - switch { - case entry.BreakingChange: - entryBump = conventionalcommits.MajorVersion - case entry.Type == "feat": - entryBump = conventionalcommits.MinorVersion - case entry.Type == "fix": - entryBump = conventionalcommits.PatchVersion - } + for _, commit := range commits { + entryBump := conventionalcommits.UnknownVersion + switch { + case commit.BreakingChange: + entryBump = conventionalcommits.MajorVersion + case commit.Type == "feat": + entryBump = conventionalcommits.MinorVersion + case commit.Type == "fix": + entryBump = conventionalcommits.PatchVersion + } - if entryBump > bump { - bump = entryBump - } + if entryBump > bump { + bump = entryBump } } diff --git a/versioning_test.go b/versioning_test.go index 9319c63..b6a0995 100644 --- a/versioning_test.go +++ b/versioning_test.go @@ -10,23 +10,23 @@ import ( func TestReleases_NextVersion(t *testing.T) { type args struct { + releases Releases versionBump conventionalcommits.VersionBump nextVersionType NextVersionType } tests := []struct { - name string - releases Releases - args args - want string - wantErr assert.ErrorAssertionFunc + name string + args args + want string + wantErr assert.ErrorAssertionFunc }{ { name: "simple bump (major)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -35,12 +35,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "simple bump (minor)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -49,12 +48,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "simple bump (patch)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -63,12 +61,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "normal to prerelease (major)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeRC, }, @@ -77,12 +74,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "normal to prerelease (minor)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeRC, }, @@ -91,12 +87,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "normal to prerelease (patch)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -105,11 +100,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease bump (major)", - releases: Releases{ - Latest: &Tag{Name: "v2.0.0-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v2.0.0-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeRC, }, @@ -118,11 +113,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease bump (minor)", - releases: Releases{ - Latest: &Tag{Name: "v1.2.0-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.2.0-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeRC, }, @@ -131,11 +126,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease bump (patch)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.2-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.2-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -144,11 +139,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease different bump (major)", - releases: Releases{ - Latest: &Tag{Name: "v1.2.0-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.2.0-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeRC, }, @@ -157,11 +152,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease different bump (minor)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.2-rc.0"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.2-rc.0"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeRC, }, @@ -170,11 +165,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease to prerelease", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-alpha.2"}, - Stable: &Tag{Name: "v1.1.0"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-alpha.2"}, + Stable: &Tag{Name: "v1.1.0"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -183,11 +178,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease to normal (explicit)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-alpha.2"}, - Stable: &Tag{Name: "v1.1.0"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-alpha.2"}, + Stable: &Tag{Name: "v1.1.0"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeNormal, }, @@ -196,11 +191,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "prerelease to normal (implicit)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-alpha.2"}, - Stable: &Tag{Name: "v1.1.0"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-alpha.2"}, + Stable: &Tag{Name: "v1.1.0"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -209,11 +204,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "nil tag (major)", - releases: Releases{ - Latest: nil, - Stable: nil, - }, args: args{ + releases: Releases{ + Latest: nil, + Stable: nil, + }, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -222,11 +217,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "nil tag (minor)", - releases: Releases{ - Latest: nil, - Stable: nil, - }, args: args{ + releases: Releases{ + Latest: nil, + Stable: nil, + }, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -235,11 +230,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "nil tag (patch)", - releases: Releases{ - Latest: nil, - Stable: nil, - }, args: args{ + releases: Releases{ + Latest: nil, + Stable: nil, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -248,11 +243,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "nil stable release (major)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.0"}, - Stable: nil, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-rc.0"}, + Stable: nil, + }, versionBump: conventionalcommits.MajorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -261,12 +256,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "nil stable release (minor)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.0"}, - Stable: nil, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-rc.0"}, + Stable: nil, + }, versionBump: conventionalcommits.MinorVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -275,12 +269,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "nil stable release (patch)", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.0"}, - Stable: nil, - }, args: args{ - + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-rc.0"}, + Stable: nil, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeUndefined, }, @@ -290,11 +283,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "error on invalid tag semver", - releases: Releases{ - Latest: &Tag{Name: "foodazzle"}, - Stable: &Tag{Name: "foodazzle"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "foodazzle"}, + Stable: &Tag{Name: "foodazzle"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -303,11 +296,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "error on invalid tag prerelease", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1-rc.foo"}, - Stable: &Tag{Name: "v1.1.1-rc.foo"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1-rc.foo"}, + Stable: &Tag{Name: "v1.1.1-rc.foo"}, + }, versionBump: conventionalcommits.PatchVersion, nextVersionType: NextVersionTypeRC, }, @@ -316,11 +309,11 @@ func TestReleases_NextVersion(t *testing.T) { }, { name: "error on invalid bump", - releases: Releases{ - Latest: &Tag{Name: "v1.1.1"}, - Stable: &Tag{Name: "v1.1.1"}, - }, args: args{ + releases: Releases{ + Latest: &Tag{Name: "v1.1.1"}, + Stable: &Tag{Name: "v1.1.1"}, + }, versionBump: conventionalcommits.UnknownVersion, nextVersionType: NextVersionTypeUndefined, @@ -331,95 +324,65 @@ func TestReleases_NextVersion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.releases.NextVersion(tt.args.versionBump, tt.args.nextVersionType) - if !tt.wantErr(t, err, fmt.Sprintf("Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)) { + got, err := SemVerNextVersion(tt.args.releases, tt.args.versionBump, tt.args.nextVersionType) + if !tt.wantErr(t, err, fmt.Sprintf("SemVerNextVersion(Releases(%v, %v), %v, %v)", tt.args.releases.Latest, tt.args.releases.Stable, tt.args.versionBump, tt.args.nextVersionType)) { return } - assert.Equalf(t, tt.want, got, "Releases(%v, %v).NextVersion(%v, %v)", tt.releases.Latest, tt.releases.Stable, tt.args.versionBump, tt.args.nextVersionType) + assert.Equalf(t, tt.want, got, "SemVerNextVersion(Releases(%v, %v), %v, %v)", tt.args.releases.Latest, tt.args.releases.Stable, tt.args.versionBump, tt.args.nextVersionType) }) } } -func TestVersionBumpFromChangesets(t *testing.T) { +func TestVersionBumpFromCommits(t *testing.T) { tests := []struct { - name string - changesets []Changeset - want conventionalcommits.VersionBump + name string + analyzedCommits []AnalyzedCommit + want conventionalcommits.VersionBump }{ { - name: "no entries (unknown)", - changesets: []Changeset{}, - want: conventionalcommits.UnknownVersion, + name: "no entries (unknown)", + analyzedCommits: []AnalyzedCommit{}, + want: conventionalcommits.UnknownVersion, }, { - name: "non-release type (unknown)", - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}}}, - want: conventionalcommits.UnknownVersion, + name: "non-release type (unknown)", + analyzedCommits: []AnalyzedCommit{{Type: "docs"}}, + want: conventionalcommits.UnknownVersion, }, { - name: "single breaking (major)", - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}}}, - want: conventionalcommits.MajorVersion, + name: "single breaking (major)", + analyzedCommits: []AnalyzedCommit{{BreakingChange: true}}, + want: conventionalcommits.MajorVersion, }, { - name: "single feat (minor)", - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}}}, - want: conventionalcommits.MinorVersion, + name: "single feat (minor)", + analyzedCommits: []AnalyzedCommit{{Type: "feat"}}, + want: conventionalcommits.MinorVersion, }, { - name: "single fix (patch)", - changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}}, - want: conventionalcommits.PatchVersion, + name: "single fix (patch)", + analyzedCommits: []AnalyzedCommit{{Type: "fix"}}, + want: conventionalcommits.PatchVersion, }, { - name: "multiple changesets (major)", - changesets: []Changeset{ - {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}, - {ChangelogEntries: []AnalyzedCommit{{BreakingChange: true}}}, - }, - want: conventionalcommits.MajorVersion, + name: "multiple entries (major)", + analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}}, + want: conventionalcommits.MajorVersion, }, { - name: "multiple changesets (minor)", - changesets: []Changeset{ - {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}, - {ChangelogEntries: []AnalyzedCommit{{Type: "feat"}}}, - }, - want: conventionalcommits.MinorVersion, + name: "multiple entries (minor)", + analyzedCommits: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}}, + want: conventionalcommits.MinorVersion, }, { - name: "multiple changesets (patch)", - changesets: []Changeset{ - {ChangelogEntries: []AnalyzedCommit{{Type: "docs"}}}, - {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}}}, - }, - want: conventionalcommits.PatchVersion, - }, - { - name: "multiple entries in one changeset (major)", - changesets: []Changeset{ - {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}, {BreakingChange: true}}}, - }, - want: conventionalcommits.MajorVersion, - }, - { - name: "multiple entries in one changeset (minor)", - changesets: []Changeset{ - {ChangelogEntries: []AnalyzedCommit{{Type: "fix"}, {Type: "feat"}}}, - }, - want: conventionalcommits.MinorVersion, - }, - { - name: "multiple entries in one changeset (patch)", - changesets: []Changeset{ - {ChangelogEntries: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}}}, - }, - want: conventionalcommits.PatchVersion, + name: "multiple entries (patch)", + analyzedCommits: []AnalyzedCommit{{Type: "docs"}, {Type: "fix"}}, + want: conventionalcommits.PatchVersion, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, VersionBumpFromChangesets(tt.changesets), "VersionBumpFromChangesets(%v)", tt.changesets) + assert.Equalf(t, tt.want, VersionBumpFromCommits(tt.analyzedCommits), "VersionBumpFromCommits(%v)", tt.analyzedCommits) }) } }