diff --git a/.changeset/config.json b/.changeset/config.json index fcc9db5..d7c0c94 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,7 +1,7 @@ { "root": "github.com/gojekfarm/xtools", "baseBranch": "main", - "ignore": null, - "ignorePaths": null, + "ignore": [""], + "ignorePaths": ["examples/*"], "dependentBump": "patch" } \ No newline at end of file diff --git a/.changeset/lucky-dove-dream.md b/.changeset/lucky-dove-dream.md new file mode 100644 index 0000000..78cca70 --- /dev/null +++ b/.changeset/lucky-dove-dream.md @@ -0,0 +1,5 @@ +--- +"cmd/changeset": patch +--- + +Remove changelog generation from version command diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml new file mode 100644 index 0000000..724dcbe --- /dev/null +++ b/.github/workflows/changeset-check.yml @@ -0,0 +1,26 @@ +name: Changeset Check + +on: + pull_request: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.x" + + - name: Install changeset + run: go install github.com/gojekfarm/xtools/cmd/changeset@latest + + - name: Check for changeset + run: | + changeset status --since=origin/main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..21fe5a8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Publish + +on: + push: + branches: [main] + paths: + - ".changeset/release-manifest.json" + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.x" + + - name: Install changeset + run: go install github.com/gojekfarm/xtools/cmd/changeset@latest + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Publish + run: changeset publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ad624ca --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.x" + + - name: Install changeset + run: go install github.com/gojekfarm/xtools/cmd/changeset@latest + + - name: Check for changesets + id: check + run: | + if compgen -G ".changeset/*.md" > /dev/null 2>&1; then + echo "has_changesets=true" >> $GITHUB_OUTPUT + else + echo "has_changesets=false" >> $GITHUB_OUTPUT + fi + + - name: Create Release PR + if: steps.check.outputs.has_changesets == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Create release branch + BRANCH="changeset-release/main" + git checkout -B $BRANCH + + # Version packages + changeset version + + # Check if there are changes to commit + if git diff --quiet; then + echo "No version changes to commit" + exit 0 + fi + + # Commit changes + git add -A + git commit -m "chore: version packages" + git push -f origin $BRANCH + + # Create or update PR + gh pr create --base main --head $BRANCH \ + --title "chore: version packages" \ + --body "This PR was auto-generated by the release workflow." \ + 2>/dev/null || gh pr edit $BRANCH --title "chore: version packages" diff --git a/.gitignore b/.gitignore index 0680fa4..48f687b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,3 @@ go.work.sum .cursor/ .tasks/ -release-manifest.json diff --git a/cmd/changeset/README.md b/cmd/changeset/README.md index 9d5536d..0c4f3a8 100644 --- a/cmd/changeset/README.md +++ b/cmd/changeset/README.md @@ -67,7 +67,7 @@ $ changeset status ### `changeset version` -Consume changesets, update go.mod files, and generate changelogs. +Consume changesets and update go.mod files. ```bash $ changeset version @@ -154,7 +154,6 @@ main │ ├── Reads all .changeset/*.md │ ├── Computes: xkafka v0.10.0 → v0.11.0 │ ├── Updates go.mod files - │ ├── Updates CHANGELOG.md │ ├── Deletes .changeset/*.md │ └── Writes .changeset/release-manifest.json │ @@ -212,7 +211,6 @@ changeset version ├── Reads .changeset/*.md (changesets) ├── Computes version bumps ├── Updates go.mod files - ├── Updates CHANGELOG.md ├── Deletes consumed changesets └── Writes .changeset/release-manifest.json ← created diff --git a/cmd/changeset/app/add.go b/cmd/changeset/app/add.go index 8fa5b61..0888bb6 100644 --- a/cmd/changeset/app/add.go +++ b/cmd/changeset/app/add.go @@ -212,7 +212,7 @@ func createChangesetInteractive(dir string) (*changeset.Changeset, error) { huh.NewGroup( huh.NewText(). Title("Describe the changes"). - Description("This will appear in the changelog"). + Description("Summary of the change"). CharLimit(1000). Value(&summary), ), diff --git a/cmd/changeset/app/cli.go b/cmd/changeset/app/cli.go index 2765717..f279750 100644 --- a/cmd/changeset/app/cli.go +++ b/cmd/changeset/app/cli.go @@ -134,8 +134,8 @@ func BuildStatusCommand() *cli.Command { func BuildVersionCommand() *cli.Command { return &cli.Command{ Name: "version", - Usage: "Update versions and changelogs from changesets", - Description: "Consumes all changesets, updates go.mod files, generates changelogs,\n" + + Usage: "Update versions from changesets", + Description: "Consumes all changesets, updates go.mod files,\n" + "and writes a release manifest for the publish step.\n\n" + "Examples:\n" + " changeset version\n" + diff --git a/cmd/changeset/app/version.go b/cmd/changeset/app/version.go index ca71749..164dc13 100644 --- a/cmd/changeset/app/version.go +++ b/cmd/changeset/app/version.go @@ -113,30 +113,8 @@ func Version(ctx context.Context, cmd *cli.Command) error { slog.Info("Updated go.mod", "path", gomodPath) } - // Update changelogs for each released module (skip for snapshots) + // Delete consumed changesets (skip for snapshots) if !snapshot { - slog.Info("Updating changelogs") - for _, r := range releases { - mod := graph.FindModule(r.Module) - if mod == nil { - continue - } - - // Generate changelog entry for this module - entry := changeset.GenerateChangelog([]changeset.Release{r}, changesets) - if entry == nil { - continue - } - - changelogPath := filepath.Join(mod.Path, "CHANGELOG.md") - if err := changeset.UpdateChangelog(changelogPath, entry); err != nil { - return cli.Exit(fmt.Sprintf("Failed to update changelog for %s: %v", r.Module, err), 1) - } - - slog.Info("Updated changelog", "path", changelogPath, "module", displayModule(r.Module)) - } - - // Delete consumed changesets (skip for snapshots) slog.Info("Deleting consumed changesets") for _, cs := range changesets { if err := changeset.DeleteChangeset(cs); err != nil { @@ -146,7 +124,7 @@ func Version(ctx context.Context, cmd *cli.Command) error { } } } else { - slog.Info("Snapshot mode: Skipping changelog updates and changeset deletion") + slog.Info("Snapshot mode: Skipping changeset deletion") } // Write release manifest diff --git a/cmd/changeset/changeset/changelog.go b/cmd/changeset/changeset/changelog.go deleted file mode 100644 index 098dbda..0000000 --- a/cmd/changeset/changeset/changelog.go +++ /dev/null @@ -1,222 +0,0 @@ -package changeset - -import ( - "fmt" - "os" - "sort" - "strings" - "time" -) - -// ChangelogEntry represents a single changelog entry. -type ChangelogEntry struct { - Version string - Date time.Time - Changes map[Bump][]string // Bump type -> list of summaries -} - -// GenerateChangelog creates a changelog entry from releases and changesets. -// It groups changes by bump type for better readability. -func GenerateChangelog(releases []Release, changesets []*Changeset) *ChangelogEntry { - if len(releases) == 0 { - return nil - } - - // Use the first release version as the entry version - // (assuming all releases in a batch have the same version pattern) - version := releases[0].Version - - entry := &ChangelogEntry{ - Version: version, - Date: time.Now(), - Changes: make(map[Bump][]string), - } - - // Collect all unique summaries - summarySet := make(map[string]bool) - for _, cs := range changesets { - if cs.Summary != "" && !summarySet[cs.Summary] { - summarySet[cs.Summary] = true - // Find the highest bump in this changeset to categorize it - highestBump := BumpPatch - for _, bump := range cs.Modules { - if bump.Compare(highestBump) > 0 { - highestBump = bump - } - } - entry.Changes[highestBump] = append(entry.Changes[highestBump], cs.Summary) - } - } - - return entry -} - -// FormatChangelogEntry formats a changelog entry as markdown. -func FormatChangelogEntry(entry *ChangelogEntry) string { - var sb strings.Builder - - sb.WriteString(fmt.Sprintf("## %s (%s)\n\n", entry.Version, entry.Date.Format("2006-01-02"))) - - // Order: major, minor, patch - bumpOrder := []Bump{BumpMajor, BumpMinor, BumpPatch} - bumpHeaders := map[Bump]string{ - BumpMajor: "### Breaking Changes", - BumpMinor: "### Features", - BumpPatch: "### Bug Fixes", - } - - for _, bump := range bumpOrder { - changes := entry.Changes[bump] - if len(changes) == 0 { - continue - } - - sb.WriteString(bumpHeaders[bump] + "\n\n") - for _, change := range changes { - // Handle multi-line summaries - lines := strings.Split(change, "\n") - for i, line := range lines { - if i == 0 { - sb.WriteString("- " + line + "\n") - } else if strings.TrimSpace(line) != "" { - sb.WriteString(" " + line + "\n") - } - } - } - sb.WriteString("\n") - } - - return sb.String() -} - -// UpdateChangelog prepends a new entry to an existing CHANGELOG.md. -// Creates the file if it doesn't exist. -func UpdateChangelog(path string, entry *ChangelogEntry) error { - newContent := FormatChangelogEntry(entry) - - // Read existing content - existingContent, err := os.ReadFile(path) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("reading changelog: %w", err) - } - - var finalContent string - if len(existingContent) == 0 { - // New file - add header - finalContent = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n" + newContent - } else { - // Prepend to existing content after the header - existing := string(existingContent) - // Find where to insert (after the title and intro) - insertPos := 0 - lines := strings.Split(existing, "\n") - for i, line := range lines { - if strings.HasPrefix(line, "## ") { - // Found first version entry - insertPos = strings.Index(existing, line) - break - } - if i == len(lines)-1 { - // No version entries found, append to end - insertPos = len(existing) - } - } - - if insertPos > 0 { - finalContent = existing[:insertPos] + newContent + existing[insertPos:] - } else { - finalContent = existing + "\n" + newContent - } - } - - if err := os.WriteFile(path, []byte(finalContent), 0644); err != nil { - return fmt.Errorf("writing changelog: %w", err) - } - - return nil -} - -// ParseChangelog reads an existing CHANGELOG.md. -// Returns entries sorted by version (newest first). -func ParseChangelog(path string) ([]ChangelogEntry, error) { - content, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("reading changelog: %w", err) - } - - var entries []ChangelogEntry - var currentEntry *ChangelogEntry - var currentBump Bump - var currentChanges []string - - lines := strings.Split(string(content), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "## ") { - // Save previous entry - if currentEntry != nil { - if len(currentChanges) > 0 && currentBump != "" { - currentEntry.Changes[currentBump] = currentChanges - } - entries = append(entries, *currentEntry) - } - - // Parse version header: "## v1.0.0 (2024-01-01)" - header := strings.TrimPrefix(line, "## ") - parts := strings.SplitN(header, " ", 2) - version := parts[0] - - var date time.Time - if len(parts) > 1 { - dateStr := strings.Trim(parts[1], "()") - date, _ = time.Parse("2006-01-02", dateStr) - } - - currentEntry = &ChangelogEntry{ - Version: version, - Date: date, - Changes: make(map[Bump][]string), - } - currentBump = "" - currentChanges = nil - } else if strings.HasPrefix(line, "### ") { - // Save changes for previous bump type - if currentEntry != nil && len(currentChanges) > 0 && currentBump != "" { - currentEntry.Changes[currentBump] = currentChanges - currentChanges = nil - } - - // Parse bump type header - header := strings.TrimPrefix(line, "### ") - switch header { - case "Breaking Changes": - currentBump = BumpMajor - case "Features": - currentBump = BumpMinor - case "Bug Fixes": - currentBump = BumpPatch - default: - currentBump = "" - } - } else if strings.HasPrefix(line, "- ") && currentEntry != nil && currentBump != "" { - currentChanges = append(currentChanges, strings.TrimPrefix(line, "- ")) - } - } - - // Save last entry - if currentEntry != nil { - if len(currentChanges) > 0 && currentBump != "" { - currentEntry.Changes[currentBump] = currentChanges - } - entries = append(entries, *currentEntry) - } - - // Sort by version (newest first) - sort.Slice(entries, func(i, j int) bool { - return entries[i].Version > entries[j].Version - }) - - return entries, nil -} diff --git a/cmd/changeset/changeset/changelog_test.go b/cmd/changeset/changeset/changelog_test.go deleted file mode 100644 index 81d65bb..0000000 --- a/cmd/changeset/changeset/changelog_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package changeset - -import ( - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestFormatChangelogEntry(t *testing.T) { - tests := []struct { - name string - entry *ChangelogEntry - contains []string - excludes []string - }{ - { - name: "patch only", - entry: &ChangelogEntry{ - Version: "v1.0.0", - Date: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC), - Changes: map[Bump][]string{ - BumpPatch: {"Fixed a bug"}, - }, - }, - contains: []string{"## v1.0.0", "2024-01-15", "### Bug Fixes", "Fixed a bug"}, - excludes: []string{"### Features", "### Breaking Changes"}, - }, - { - name: "minor only", - entry: &ChangelogEntry{ - Version: "v1.1.0", - Date: time.Date(2024, 2, 20, 0, 0, 0, 0, time.UTC), - Changes: map[Bump][]string{ - BumpMinor: {"Added new feature"}, - }, - }, - contains: []string{"## v1.1.0", "### Features", "Added new feature"}, - excludes: []string{"### Bug Fixes", "### Breaking Changes"}, - }, - { - name: "major only", - entry: &ChangelogEntry{ - Version: "v2.0.0", - Date: time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC), - Changes: map[Bump][]string{ - BumpMajor: {"Breaking API change"}, - }, - }, - contains: []string{"## v2.0.0", "### Breaking Changes", "Breaking API change"}, - excludes: []string{"### Features", "### Bug Fixes"}, - }, - { - name: "mixed changes", - entry: &ChangelogEntry{ - Version: "v2.1.1", - Date: time.Date(2024, 4, 5, 0, 0, 0, 0, time.UTC), - Changes: map[Bump][]string{ - BumpMajor: {"Breaking change"}, - BumpMinor: {"New feature"}, - BumpPatch: {"Bug fix"}, - }, - }, - contains: []string{ - "### Breaking Changes", - "### Features", - "### Bug Fixes", - "Breaking change", - "New feature", - "Bug fix", - }, - }, - { - name: "multiple changes per type", - entry: &ChangelogEntry{ - Version: "v1.0.1", - Date: time.Date(2024, 5, 1, 0, 0, 0, 0, time.UTC), - Changes: map[Bump][]string{ - BumpPatch: {"Fix one", "Fix two", "Fix three"}, - }, - }, - contains: []string{"- Fix one", "- Fix two", "- Fix three"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := FormatChangelogEntry(tt.entry) - - for _, want := range tt.contains { - assert.Contains(t, got, want) - } - - for _, exclude := range tt.excludes { - assert.NotContains(t, got, exclude) - } - }) - } -} - -func TestFormatChangelogEntryOrder(t *testing.T) { - entry := &ChangelogEntry{ - Version: "v1.0.0", - Date: time.Now(), - Changes: map[Bump][]string{ - BumpMajor: {"Major change"}, - BumpMinor: {"Minor change"}, - BumpPatch: {"Patch change"}, - }, - } - - got := FormatChangelogEntry(entry) - - majorIdx := strings.Index(got, "### Breaking Changes") - minorIdx := strings.Index(got, "### Features") - patchIdx := strings.Index(got, "### Bug Fixes") - - require.NotEqual(t, -1, majorIdx, "missing Breaking Changes header") - require.NotEqual(t, -1, minorIdx, "missing Features header") - require.NotEqual(t, -1, patchIdx, "missing Bug Fixes header") - - assert.Less(t, majorIdx, minorIdx, "Breaking Changes should come before Features") - assert.Less(t, minorIdx, patchIdx, "Features should come before Bug Fixes") -} - -func TestGenerateChangelog(t *testing.T) { - releases := []Release{ - {Module: "pkg", Version: "v1.0.0", Bump: BumpMinor}, - } - changesets := []*Changeset{ - { - Modules: map[string]Bump{"pkg": BumpMinor}, - Summary: "Added new feature", - }, - } - - entry := GenerateChangelog(releases, changesets) - - require.NotNil(t, entry) - assert.Equal(t, "v1.0.0", entry.Version) - assert.Len(t, entry.Changes[BumpMinor], 1) -} - -func TestGenerateChangelogEmpty(t *testing.T) { - entry := GenerateChangelog([]Release{}, []*Changeset{}) - assert.Nil(t, entry) -} - -func TestGenerateChangelogDeduplicates(t *testing.T) { - releases := []Release{ - {Module: "pkg", Version: "v1.0.0", Bump: BumpMinor}, - } - changesets := []*Changeset{ - {Modules: map[string]Bump{"pkg": BumpMinor}, Summary: "Same change"}, - {Modules: map[string]Bump{"pkg": BumpMinor}, Summary: "Same change"}, - } - - entry := GenerateChangelog(releases, changesets) - - require.NotNil(t, entry) - - total := 0 - for _, changes := range entry.Changes { - total += len(changes) - } - assert.Equal(t, 1, total, "should deduplicate identical summaries") -} diff --git a/cmd/changeset/changeset/changeset b/cmd/changeset/changeset/changeset deleted file mode 100755 index eee9b8b..0000000 Binary files a/cmd/changeset/changeset/changeset and /dev/null differ diff --git a/cmd/changeset/e2e/workflow_test.go b/cmd/changeset/e2e/workflow_test.go index 3dc854e..2514fd9 100644 --- a/cmd/changeset/e2e/workflow_test.go +++ b/cmd/changeset/e2e/workflow_test.go @@ -40,12 +40,6 @@ func TestE2E_FullReleaseWorkflow(t *testing.T) { require.NoError(t, err) assert.Empty(t, changesetsAfter, "changesets should be consumed") - // Verify CHANGELOG was updated - testutil.AssertFileContains(t, - filepath.Join(dir, "libA", "CHANGELOG.md"), - "0.2.0", - ) - // Step 3: Create tags (without push since no remote) tagResult := runCLI(t, dir, "tag") require.Equal(t, 0, tagResult.ExitCode, "tag should succeed")