From b668f26188aa9377a25e176eae047529b5e85f9c Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sat, 8 Nov 2025 09:55:47 -0600 Subject: [PATCH] feat(unreleased): add 'partial' command for creating entries linked to specific commits * implement changelog validation command for CI enforcement --- ROADMAP.md | 61 ++-- cmd/check.go | 144 +++++++++ cmd/main.go | 2 +- cmd/unreleased.go | 101 ++++++- internal/changeset/changeset.go | 30 +- internal/changeset/changeset_test.go | 103 +++++++ internal/docs/README.md | 423 +++++++++++++++++++++++++++ internal/docs/e2e/README.md | 88 ++++++ 8 files changed, 920 insertions(+), 32 deletions(-) create mode 100644 cmd/check.go create mode 100644 internal/docs/README.md create mode 100644 internal/docs/e2e/README.md diff --git a/ROADMAP.md b/ROADMAP.md index 63844e0..ba996a2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,17 +19,18 @@ The foundation CLI structure with core commands. - [x] `unreleased add` - Create new entry - [x] `unreleased list` - Display entries (text and JSON) - [x] `unreleased review` - Interactive TUI review + - [x] `unreleased partial` - Create entry linked to specific commit + - [x] Filename format: `..md` + - [x] Auto-detect type from conventional commit format + - [x] Optional `--type`, `--scope`, `--summary` override flags + - [ ] Optional `--issue` flag (TODO: see issue-linking task below) - [ ] Implement delete action from review - [ ] Implement edit action from review -- [ ] Figure out how these fit in to `unreleased` - - [ ] `storm partial create` - Create new partial file - - [ ] Filename format: `..md` - - [ ] Supports configurable categories (feature, fix, doc, removal, etc.) - - [ ] Optional `--type`, `--issue`, `--message` flags - - [ ] `storm check` - Validate that changes include unreleased partials - - [ ] Detect missing partials for changed code paths - - [ ] Honor `[nochanges]` marker in commit messages - - [ ] Exit non-zero for CI enforcement +- [x] `storm check` - Validate that changes include unreleased partials + - [x] Detect missing partials for changed code paths + - [x] Honor `[nochanges]` and `[skip changelog]` markers in commit messages + - [x] Exit non-zero for CI enforcement + - [x] Support `--since` flag for checking since a tag - [x] `storm release` - Promote unreleased changes to CHANGELOG - [x] Read all `.changes/*.md` files - [x] Merge into `CHANGELOG.md` @@ -38,7 +39,7 @@ The foundation CLI structure with core commands. - [x] Optional date override with `--date` flag - [x] Generate GitHub comparison links automatically - [x] Dry-run mode - - [ ] Optional Git tag creation (Phase 7) + - [x] Optional Git tag creation - [x] `storm diff`: display inline diffs between refs with support for file filtering, context expansion, and multiple view modes. @@ -55,23 +56,9 @@ The foundation CLI structure with core commands. ## `.changes` Storage and Parsing -Local storage for unreleased changelog entries. - -### Tasks - -- [x] Define `Entry` struct with YAML frontmatter -- [x] Implement `changeset.Write(dir, entry)` - - [x] Generate unique filenames (timestamp-based) - - [x] Write YAML frontmatter - - [x] Create `.changes/` directory if missing -- [x] Implement `changeset.List(dir)` - - [x] Parse YAML frontmatter - - [x] Return `EntryWithFile` structs -- [x] Implement diff-based deduplication - - [x] Compute diff hash for commits - - [x] Load existing entries by hash - - [x] Detect rebased commits (same diff, different hash) - - [x] Update rebased commit metadata automatically +- [x] Provides a local .changes/ store that writes and lists YAML-frontmatter entries, + auto-creates the directory, and deduplicates rebased commits by diff hash to keep + unreleased changelog items clean. ## TUI @@ -82,10 +69,22 @@ Local storage for unreleased changelog entries. ## Keep a Changelog Writer - [x] Adds a full changelog pipeline that parses the existing file, builds and writes -new releases, and validates dates/sections to strictly match the Keep a Changelog -[spec](https://keepachangelog.com/en/1.1.0/), including autogenerated comparison links. + new releases, and validates dates/sections to strictly match the Keep a Changelog + [spec](https://keepachangelog.com/en/1.1.0/), including autogenerated comparison links. - [ ] Ensure deterministic sorting by category and filename timestamp +## Issue Linking + +Add support for linking changelog entries to issue/PR numbers. + +### Tasks + +- [ ] Add `--issue` flag to `unreleased add` and `unreleased partial` +- [ ] Add `issue` field to Entry struct +- [ ] Include issue number in YAML frontmatter +- [ ] Support issue validation in `check` command +- [ ] Format issue links in generated CHANGELOG (e.g., #123, owner/repo#123) + ## Phase 7: Git Tagging and CI Integration Repository tagging and automation-friendly features. @@ -97,6 +96,10 @@ Repository tagging and automation-friendly features. - [x] Include release notes in tag message - [x] Validate tag doesn't already exist - [x] Support `--tag` flag +- [x] Implement CI validation with `check` command + - [x] Validate changelog entries exist for commits + - [x] Honor `[nochanges]` markers + - [x] Exit codes for CI integration - [ ] Add JSON output modes for all commands - [x] `unreleased list --json` - [ ] `generate --output-json` diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 0000000..61dcce0 --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,144 @@ +/* +USAGE + + storm check [from] [to] [options] + +FLAGS + + --since Check changes since the given tag + --repo Path to the Git repository (default: .) + +# DESCRIPTION + +Validates that all commits in the specified range have corresponding unreleased +changelog entries. This is useful for CI enforcement to ensure developers +document their changes. + +Commits containing [nochanges] or [skip changelog] in the message are skipped. + +Exit codes: + + 0 - All commits have changelog entries + 1 - One or more commits are missing changelog entries + 2 - Command execution error + +TODO(issue-linking): Support checking for issue numbers in entries when --issue flag is implemented in `unreleased partial`. + + - This requires integrating with at Gitea/Forgejo, Github, Gitlab, and Tangled +*/ +package main + +import ( + "fmt" + "strings" + + "github.com/go-git/go-git/v6" + "github.com/spf13/cobra" + "github.com/stormlightlabs/git-storm/internal/changeset" + "github.com/stormlightlabs/git-storm/internal/gitlog" + "github.com/stormlightlabs/git-storm/internal/style" +) + +// checkCmd validates that all commits in a range have corresponding changelog entries. +func checkCmd() *cobra.Command { + var sinceTag string + + c := &cobra.Command{ + Use: "check [from] [to]", + Short: "Validate changelog entries exist for all commits", + Long: `Checks that all commits in the specified range have corresponding +.changes/*.md entries. Useful for CI enforcement. + +Commits with [nochanges] or [skip changelog] in their message are skipped.`, + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + var from, to string + + if sinceTag != "" { + from = sinceTag + if len(args) > 0 { + to = args[0] + } else { + to = "HEAD" + } + } else if len(args) == 0 { + return fmt.Errorf("must specify either --since flag or [from] [to] arguments") + } else { + from, to = gitlog.ParseRefArgs(args) + } + + repo, err := git.PlainOpen(repoPath) + if err != nil { + return fmt.Errorf("failed to open repository: %w", err) + } + + commits, err := gitlog.GetCommitRange(repo, from, to) + if err != nil { + return err + } + + if len(commits) == 0 { + style.Headlinef("No commits found between %s and %s", from, to) + return nil + } + + changesDir := ".changes" + existingMetadata, err := changeset.LoadExistingMetadata(changesDir) + if err != nil { + return fmt.Errorf("failed to load existing metadata: %w", err) + } + + style.Headlinef("Checking %d commits between %s and %s", len(commits), from, to) + style.Newline() + + var missingEntries []string + skippedCount := 0 + + for _, commit := range commits { + message := strings.ToLower(commit.Message) + if strings.Contains(message, "[nochanges]") || strings.Contains(message, "[skip changelog]") { + skippedCount++ + continue + } + + diffHash, err := changeset.ComputeDiffHash(commit) + if err != nil { + style.Println("Warning: failed to compute diff hash for commit %s: %v", commit.Hash.String()[:7], err) + continue + } + + if _, exists := existingMetadata[diffHash]; !exists { + sha7 := commit.Hash.String()[:7] + subject := strings.Split(commit.Message, "\n")[0] + missingEntries = append(missingEntries, fmt.Sprintf("%s - %s", sha7, subject)) + } + } + + if len(missingEntries) == 0 { + style.Addedf("✓ All commits have changelog entries") + if skippedCount > 0 { + style.Println(" Skipped %d commits with [nochanges] marker", skippedCount) + } + return nil + } + + style.Println("%s", style.StyleRemoved.Render(fmt.Sprintf("✗ %d commits missing changelog entries:", len(missingEntries)))) + style.Newline() + + for _, entry := range missingEntries { + style.Println(" - %s", entry) + } + + style.Newline() + style.Println("To create entries, run:") + style.Println(" storm generate %s %s --interactive", from, to) + style.Println("Or manually create entries with:") + style.Println(" storm unreleased partial ") + + return fmt.Errorf("changelog validation failed") + }, + } + + c.Flags().StringVar(&sinceTag, "since", "", "Check changes since the given tag") + return c +} diff --git a/cmd/main.go b/cmd/main.go index fb62acb..3691b21 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -41,7 +41,7 @@ and can review commits interactively through a TUI.`, root.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the Git repository") root.PersistentFlags().StringVarP(&output, "output", "o", "CHANGELOG.md", "Output changelog file path") - root.AddCommand(generateCmd(), unreleasedCmd(), releaseCmd(), diffCmd(), versionCmd()) + root.AddCommand(generateCmd(), unreleasedCmd(), releaseCmd(), diffCmd(), checkCmd(), versionCmd()) if err := fang.Execute(ctx, root, fang.WithColorSchemeFunc(style.NewColorScheme)); err != nil { log.Fatalf("Execution failed: %v", err) diff --git a/cmd/unreleased.go b/cmd/unreleased.go index cdbc8dc..05d4f33 100644 --- a/cmd/unreleased.go +++ b/cmd/unreleased.go @@ -8,6 +8,7 @@ SUBCOMMANDS add Add a new unreleased change entry list List all unreleased changes review Review unreleased changes interactively + partial Create entry linked to a specific commit USAGE @@ -37,6 +38,17 @@ FLAGS --repo Path to the repository (default: .) --output Optional file to export reviewed notes + +USAGE + + storm unreleased partial [options] + +FLAGS + + --type Override change type (auto-detected from commit message) + --summary Override summary (auto-detected from commit message) + --scope Optional subsystem or module name + --repo Path to the repository (default: .) */ package main @@ -47,8 +59,11 @@ import ( "strings" tea "github.com/charmbracelet/bubbletea" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" "github.com/spf13/cobra" "github.com/stormlightlabs/git-storm/internal/changeset" + "github.com/stormlightlabs/git-storm/internal/gitlog" "github.com/stormlightlabs/git-storm/internal/style" "github.com/stormlightlabs/git-storm/internal/ui" ) @@ -186,13 +201,97 @@ unreleased entries before final release.`, }, } + partial := &cobra.Command{ + Use: "partial ", + Short: "Create entry linked to a specific commit", + Long: `Creates a new .changes/..md file based on the specified commit. +Auto-detects type and summary from conventional commit format, with optional overrides.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + commitRef := args[0] + + repo, err := git.PlainOpen(repoPath) + if err != nil { + return fmt.Errorf("failed to open repository: %w", err) + } + + hash, err := repo.ResolveRevision(plumbing.Revision(commitRef)) + if err != nil { + return fmt.Errorf("failed to resolve commit ref %q: %w", commitRef, err) + } + + commit, err := repo.CommitObject(*hash) + if err != nil { + return fmt.Errorf("failed to get commit object: %w", err) + } + + parser := &gitlog.ConventionalParser{} + subject := commit.Message + body := "" + lines := strings.Split(commit.Message, "\n") + if len(lines) > 0 { + subject = lines[0] + if len(lines) > 1 { + body = strings.Join(lines[1:], "\n") + } + } + + meta, err := parser.Parse(hash.String(), subject, body, commit.Author.When) + if err != nil { + return fmt.Errorf("failed to parse commit message: %w", err) + } + + category := parser.Categorize(meta) + + if changeType != "" { + if !slices.Contains(validTypes, changeType) { + return fmt.Errorf("invalid type %q: must be one of %s", changeType, strings.Join(validTypes, ", ")) + } + category = changeType + } else if category == "" { + return fmt.Errorf("could not auto-detect change type from commit message, please specify --type") + } + + entrySummary := meta.Description + if summary != "" { + entrySummary = summary + } + + if scope != "" { + meta.Scope = scope + } + + sha7 := hash.String()[:7] + filename := fmt.Sprintf("%s.%s.md", sha7, category) + filePath := changesDir + "/" + filename + + entry := changeset.Entry{ + Type: category, + Scope: meta.Scope, + Summary: entrySummary, + Breaking: meta.Breaking, + CommitHash: hash.String(), + } + + if _, err := changeset.WritePartial(changesDir, filename, entry); err != nil { + return fmt.Errorf("failed to create changelog entry: %w", err) + } + + style.Addedf("Created %s", filePath) + return nil + }, + } + partial.Flags().StringVar(&changeType, "type", "", "Override change type (auto-detected from commit)") + partial.Flags().StringVar(&scope, "scope", "", "Optional scope or subsystem name") + partial.Flags().StringVar(&summary, "summary", "", "Override summary (auto-detected from commit)") + root := &cobra.Command{ Use: "unreleased", Short: "Manage unreleased changes (.changes directory)", Long: `Work with unreleased change notes. Supports adding, listing, and reviewing pending entries before release.`, } - root.AddCommand(add, list, review) + root.AddCommand(add, list, review, partial) return root } diff --git a/internal/changeset/changeset.go b/internal/changeset/changeset.go index 3078c03..c133232 100644 --- a/internal/changeset/changeset.go +++ b/internal/changeset/changeset.go @@ -70,13 +70,13 @@ type Entry struct { type Metadata struct { CommitHash string `json:"commit_hash"` // current commit hash DiffHash string `json:"diff_hash"` // stable diff content hash + Filename string `json:"filename"` // relative path to .md file Type string `json:"type"` Scope string `json:"scope"` Summary string `json:"summary"` Breaking bool `json:"breaking"` Author string `json:"author"` Date time.Time `json:"date"` - Filename string `json:"filename"` // relative path to .md file } // Write creates a new .changes/-.md file with YAML frontmatter. @@ -115,6 +115,34 @@ func Write(dir string, entry Entry) (string, error) { return filePath, nil } +// WritePartial creates a .changes/ file with the specified name and YAML frontmatter. +// This is used by the `unreleased partial` command to create entries with commit-hash based names. +// Creates the .changes directory if it doesn't exist. +func WritePartial(dir string, filename string, entry Entry) (string, error) { + if err := os.MkdirAll(dir, 0755); err != nil { + return "", fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + filePath := filepath.Join(dir, filename) + + if _, err := os.Stat(filePath); err == nil { + return "", fmt.Errorf("file %s already exists", filename) + } + + yamlBytes, err := yaml.Marshal(entry) + if err != nil { + return "", fmt.Errorf("failed to marshal entry to YAML: %w", err) + } + + content := fmt.Sprintf("---\n%s---\n", string(yamlBytes)) + + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return "", fmt.Errorf("failed to write file %s: %w", filePath, err) + } + + return filePath, nil +} + // WriteWithMetadata creates a new .changes/-.md file with YAML // frontmatter and saves corresponding metadata to .changes/data/.json. // diff --git a/internal/changeset/changeset_test.go b/internal/changeset/changeset_test.go index 19f91d1..7989cfc 100644 --- a/internal/changeset/changeset_test.go +++ b/internal/changeset/changeset_test.go @@ -513,3 +513,106 @@ func TestDeduplication_RebasedCommit(t *testing.T) { } } } + +func TestWritePartial(t *testing.T) { + tmpDir := t.TempDir() + + tests := []struct { + name string + filename string + entry Entry + wantErr bool + wantType string + wantSummary string + }{ + { + name: "basic partial entry", + filename: "abc1234.added.md", + entry: Entry{ + Type: "added", + Scope: "cli", + Summary: "Add feature", + CommitHash: "abc123def456", + }, + wantErr: false, + wantType: "added", + wantSummary: "Add feature", + }, + { + name: "partial with different type", + filename: "def5678.fixed.md", + entry: Entry{ + Type: "fixed", + Summary: "Fix bug", + CommitHash: "def5678abc", + }, + wantErr: false, + wantType: "fixed", + wantSummary: "Fix bug", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath, err := WritePartial(tmpDir, tt.filename, tt.entry) + if (err != nil) != tt.wantErr { + t.Fatalf("WritePartial() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantErr { + return + } + + expectedPath := filepath.Join(tmpDir, tt.filename) + testutils.Expect.Equal(t, filePath, expectedPath, "File path should match expected") + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + t.Errorf("File was not created: %s", filePath) + } + + content, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + + parts := strings.SplitN(string(content), "---\n", 3) + if len(parts) < 3 { + t.Fatal("Invalid YAML frontmatter format") + } + + var parsed Entry + if err := yaml.Unmarshal([]byte(parts[1]), &parsed); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + testutils.Expect.Equal(t, parsed.Type, tt.wantType) + testutils.Expect.Equal(t, parsed.Summary, tt.wantSummary) + testutils.Expect.Equal(t, parsed.CommitHash, tt.entry.CommitHash) + }) + } +} + +func TestWritePartial_DuplicateFilename(t *testing.T) { + tmpDir := t.TempDir() + + filename := "abc1234.added.md" + entry := Entry{ + Type: "added", + Summary: "Test feature", + CommitHash: "abc1234", + } + + _, err := WritePartial(tmpDir, filename, entry) + if err != nil { + t.Fatalf("First WritePartial() error = %v", err) + } + + _, err = WritePartial(tmpDir, filename, entry) + if err == nil { + t.Error("Expected error when writing duplicate filename, got nil") + } + + if !strings.Contains(err.Error(), "already exists") { + t.Errorf("Expected 'already exists' error, got: %v", err) + } +} diff --git a/internal/docs/README.md b/internal/docs/README.md new file mode 100644 index 0000000..0884cb4 --- /dev/null +++ b/internal/docs/README.md @@ -0,0 +1,423 @@ +--- +title: Testing Workflow +updated: 2025-01-08 +version: 1 +--- + +"Ride the lightning." + +This document provides a comprehensive testing workflow for the `storm` changelog manager. +All tests should be run within this repository to validate functionality against real Git history. + +## Setup + +```bash +# Build the CLI +task build +``` + +## Core Workflow + +### Manual Entry Creation (`unreleased add`) + +Create entries manually without linking to commits. + +#### Basic entry creation + +```bash +storm unreleased add --type added --summary "Test manual entry" +``` + +**Expected:** + +- Creates `.changes/-test-manual-entry.md` +- File contains YAML frontmatter with type and summary +- Styled success message displays created file path + +#### Entry with scope + +```bash +storm unreleased add --type fixed --scope api --summary "Fix authentication bug" +``` + +**Expected:** + +- Includes `scope: api` in frontmatter +- Filename slugifies to `...-fix-authentication-bug.md` + +#### Collision handling + +```bash +# Run same command twice rapidly +storm unreleased add --type added --summary "Duplicate test" +storm unreleased add --type added --summary "Duplicate test" +``` + +**Expected:** + +- Two different files created (second has `-1` suffix) +- Both files exist and are readable + +**Edge Cases:** + +- Invalid type (should error with helpful message) +- Missing required flags (should error) +- Very long summary (should truncate to 50 chars) +- Special characters in summary (should slugify correctly) +- Empty summary (should error) + +### Commit-Linked Entry Creation (`unreleased partial`) + +Create entries linked to specific commits with auto-detection. + +#### Basic partial from commit + +```bash +# Use a recent commit hash +storm unreleased partial HEAD +``` + +**Expected:** + +- Auto-detects type from conventional commit format +- Creates `.changes/..md` +- Includes `commit_hash` in frontmatter +- Shows styled success message + +#### Override auto-detection + +```bash +storm unreleased partial HEAD~1 --type fixed --summary "Custom summary" +``` + +**Expected:** + +- Uses provided type instead of auto-detected +- Uses custom summary +- Preserves commit hash in frontmatter + +#### Non-conventional commit + +```bash +# Try a commit without conventional format +storm unreleased partial +``` + +**Expected:** + +- Error message: "could not auto-detect change type" +- Suggests using `--type` flag + +#### Duplicate prevention + +```bash +storm unreleased partial HEAD +storm unreleased partial HEAD # Run again +``` + +**Expected:** + +- Second command fails with "file already exists" error + +**Edge Cases:** + +- Invalid commit ref (should error) +- Merge commit (should handle gracefully) +- Initial commit with no parent (should work) +- Commit with multi-line message (should parse correctly) +- Commit with breaking change marker (should set `breaking: true`) + +### Listing Entries (`unreleased list`) + +Display all unreleased changes. + +#### Text output + +```bash +storm unreleased list +``` + +**Expected:** + +- Color-coded type labels ([added], [fixed], etc.) +- Shows scope if present +- Displays filename +- Shows breaking change indicator if applicable +- Empty state message if no entries + +#### JSON output + +```bash +storm unreleased list --json +``` + +**Expected:** + +- Valid JSON array +- Each entry has type, scope, summary, filename +- Can be piped to `jq` for processing + +**Edge Cases:** + +- Empty `.changes/` directory +- Malformed YAML in entry file +- Mixed entry types (manual + partial) + +### Generating Entries from Git History (`generate`) + +Scan commit ranges and create changelog entries. + +#### Range generation + +```bash +# Generate from last 5 commits +storm generate HEAD~5 HEAD +``` + +**Expected:** + +- Lists N commits found +- Creates entries for conventional commits +- Skips non-conventional commits +- Shows created count and skipped count +- Uses diff-based deduplication + +#### Interactive selection + +```bash +storm generate HEAD~10 HEAD --interactive +``` + +**Expected:** + +- Launches TUI with commit list +- Shows parsed metadata (type, scope, summary) +- Allows selection/deselection +- Creates only selected entries +- Handles cancellation (Ctrl+C) + +#### Since tag + +```bash +storm generate --since v0.1.0 +``` + +**Expected:** + +- Generates entries from v0.1.0 to HEAD +- Auto-detects tag as starting point + +#### Deduplication + +```bash +storm generate HEAD~3 HEAD +storm generate HEAD~3 HEAD # Run again +``` + +**Expected:** + +- First run creates N entries +- Second run shows "Skipped N duplicates" +- No duplicate files created + +#### Rebased commits + +```bash +# Simulate rebase by checking metadata +storm generate +``` + +**Expected:** + +- Detects same diff, different commit hash +- Updates metadata with new commit hash +- Shows "Updated N rebased commits" + +**Edge Cases:** + +- No commits in range (should show "No commits found") +- Range with only merge commits +- Range with revert commits (should skip) +- Commits with `[nochanges]` marker (should skip) +- Non-existent refs (should error) + +### Reviewing Entries (`unreleased review`) + +Interactive TUI for reviewing unreleased changes. + +#### Basic review + +```bash +storm unreleased review +``` + +**Expected:** + +- Launches TUI with list of entries +- Shows entry details on selection +- Keyboard navigation works (j/k or arrows) +- Can mark for delete/edit (not yet implemented) +- Exit with q or ESC + +**Edge Cases:** + +- Empty changes directory (should show message, not crash) +- Corrupted entry file (should handle gracefully) +- Non-TTY environment (should detect and warn) + +### CI Validation (`check`) + +Validate that commits have changelog entries. + +#### All commits documented + +```bash +# After running generate for a range +storm check HEAD~5 HEAD +``` + +**Expected:** + +- Shows "✓ All commits have changelog entries" +- Exit code 0 + +#### Missing entries + +```bash +# Create new commits without entries +git commit --allow-empty -m "feat: undocumented feature" +storm check HEAD~1 HEAD +``` + +**Expected:** + +- Shows "✗ N commits missing changelog entries" +- Lists missing commit SHAs and subjects +- Suggests commands to fix +- Exit code 1 + +#### Skip markers + +```bash +git commit --allow-empty -m "chore: update deps [nochanges]" +storm check HEAD~1 HEAD +``` + +**Expected:** + +- Skips commit with marker +- Shows "Skipped N commits with [nochanges] marker" +- Exit code 0 + +#### Since tag + +```bash +storm check --since v0.1.0 +``` + +**Expected:** + +- Checks all commits since tag +- Reports missing entries + +**Edge Cases:** + +- Empty commit range (should succeed with 0 checks) +- Range with all skipped commits +- Invalid tag/ref (should error) + +### Release Generation (`release`) + +Promote unreleased changes to CHANGELOG. + +#### Basic release + +```bash +storm release --version 1.2.0 +``` + +**Expected:** + +- Creates/updates CHANGELOG.md +- Adds version header with date +- Groups entries by type (Added, Changed, Fixed, etc.) +- Maintains Keep a Changelog format +- Preserves existing changelog content + +#### Dry run + +```bash +storm release --version 1.2.0 --dry-run +``` + +**Expected:** + +- Shows preview of changes +- No files modified +- Styled output shows what would be written + +#### Clear changes + +```bash +storm release --version 1.2.0 --clear-changes +``` + +**Expected:** + +- Moves entries from `.changes/` to CHANGELOG +- Deletes `.changes/*.md` files after release +- Keeps `.changes/data/` metadata + +#### Git tagging + +```bash +storm release --version 1.2.0 --tag +``` + +**Expected:** + +- Creates annotated Git tag `v1.2.0` +- Includes release notes in tag message +- Validates tag doesn't exist + +**Edge Cases:** + +- No unreleased entries (should warn) +- Existing version in CHANGELOG (should append) +- Malformed CHANGELOG.md (should handle) +- Tag already exists (should error) +- Custom date format with `--date` + +### Diff Viewing (`diff`) + +Display inline diffs between refs. + +#### Basic diff + +```bash +storm diff HEAD~1 HEAD +``` + +**Expected:** + +- Shows unified diff with syntax highlighting +- Iceberg theme colors +- Context lines displayed +- File headers shown + +#### File filtering + +```bash +storm diff HEAD~1 HEAD -- "*.go" +``` + +**Expected:** + +- Shows only Go file changes +- Respects glob patterns + +**Edge Cases:** + +- No changes between refs +- Binary files (should indicate) +- Large diffs (should handle gracefully) diff --git a/internal/docs/e2e/README.md b/internal/docs/e2e/README.md new file mode 100644 index 0000000..b384232 --- /dev/null +++ b/internal/docs/e2e/README.md @@ -0,0 +1,88 @@ +--- +title: Integration Testing Scenarios +updated: 2025-11-08 +version: 1 +--- + +## Feature Branch + +```bash +# 1. Create feature branch +git checkout -b feature/new-auth + +# 2. Make commits +git commit -m "feat(auth): add OAuth support" +git commit -m "test(auth): add OAuth tests" + +# 3. Generate entries interactively +storm generate main HEAD --interactive + +# 4. Validate all documented +storm check main HEAD + +# 5. Review entries +storm unreleased list + +# Expected: 2 entries created, check passes +``` + +## Release Preparation + +```bash +# 1. Generate from last release +storm generate --since v1.0.0 + +# 2. Review what was generated +storm unreleased review + +# 3. Add manual entry for non-code change +storm unreleased add --type changed --summary "Updated documentation" + +# 4. Dry-run release +storm release --version 1.1.0 --dry-run + +# 5. Execute release with tag +storm release --version 1.1.0 --tag --clear-changes + +# 6. Verify +git tag -n9 v1.1.0 +cat CHANGELOG.md + +# Expected: Clean CHANGELOG, annotated tag, empty .changes/ +``` + +## CI Pipeline Validation + +```bash +# 1. Simulate PR with new commits +git checkout -b pr/fix-bug +git commit -m "fix(api): resolve rate limit bug" + +# 2. CI check (should fail) +storm check main HEAD +# Exit code: 1 + +# 3. Create entry +storm unreleased partial HEAD + +# 4. CI check (should pass) +storm check main HEAD +# Exit code: 0 + +# Expected: PR can be merged with confidence +``` + +## Rebase Handling + +```bash +# 1. Create entries for commits +storm generate HEAD~3 HEAD + +# 2. Rebase interactively (squash/reword) +git rebase -i HEAD~3 + +# 3. Regenerate (should detect rebased commits) +storm generate HEAD~2 HEAD + +# Expected: Metadata updated, no duplicates +``` -- 2.51.2