From 91c1aca0ddb0906484d05e054487861706ef3bd6 Mon Sep 17 00:00:00 2001 From: Kayvan Sylvan Date: Mon, 21 Jul 2025 07:36:30 -0700 Subject: [PATCH] feat: enhance changelog generator to accept version parameter for PR processing ## CHANGES - Pass version parameter to changelog generation workflow - Update ProcessIncomingPRs method to accept version string - Add commit SHA tracking to prevent duplicate entries - Modify process-prs flag to require version parameter - Improve changelog formatting with proper spacing - Update configuration to use ProcessPRsVersion string field - Enhance direct commit filtering with SHA exclusion - Update documentation to reflect version parameter requirement --- .../update-version-and-create-tag.yml | 2 +- .../internal/changelog/processing.go | 37 ++++++++++++------- .../internal/config/config.go | 30 +++++++-------- cmd/generate_changelog/main.go | 8 ++-- docs/Automated-ChangeLog.md | 28 +++++++------- docs/Automated-Changelog-Usage.md | 2 +- 6 files changed, 59 insertions(+), 48 deletions(-) diff --git a/.github/workflows/update-version-and-create-tag.yml b/.github/workflows/update-version-and-create-tag.yml index e8f98859..d04a923b 100644 --- a/.github/workflows/update-version-and-create-tag.yml +++ b/.github/workflows/update-version-and-create-tag.yml @@ -87,7 +87,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - go run ./cmd/generate_changelog --process-prs + go run ./cmd/generate_changelog --process-prs ${{ env.new_tag }} - name: Commit changes run: | # These files are modified by the version bump process diff --git a/cmd/generate_changelog/internal/changelog/processing.go b/cmd/generate_changelog/internal/changelog/processing.go index 958222c3..8620fd05 100644 --- a/cmd/generate_changelog/internal/changelog/processing.go +++ b/cmd/generate_changelog/internal/changelog/processing.go @@ -62,7 +62,7 @@ func (g *Generator) ProcessIncomingPR(prNumber int) error { } // ProcessIncomingPRs aggregates all incoming PR files for release and includes direct commits -func (g *Generator) ProcessIncomingPRs() error { +func (g *Generator) ProcessIncomingPRs(version string) error { files, err := filepath.Glob(filepath.Join(g.cfg.IncomingDir, "*.txt")) if err != nil { return fmt.Errorf("failed to scan incoming directory: %w", err) @@ -86,31 +86,39 @@ func (g *Generator) ProcessIncomingPRs() error { return fmt.Errorf("encountered errors while processing incoming files: %s", strings.Join(processingErrors, "; ")) } - // Extract PR numbers from processed files to avoid including their commits as "direct" + // Extract PR numbers and their commit SHAs from processed files to avoid including their commits as "direct" processedPRs := make(map[int]bool) + processedCommitSHAs := make(map[string]bool) + for _, file := range files { // Extract PR number from filename (e.g., "1640.txt" -> 1640) filename := filepath.Base(file) if prNumStr := strings.TrimSuffix(filename, ".txt"); prNumStr != filename { if prNum, err := strconv.Atoi(prNumStr); err == nil { processedPRs[prNum] = true + + // Fetch the PR to get its commit SHAs + if pr, err := g.ghClient.GetPRWithCommits(prNum); err == nil { + for _, commit := range pr.Commits { + processedCommitSHAs[commit.SHA] = true + } + } } } } // Now add direct commits since the last release, excluding commits from processed PRs - directCommitsContent, err := g.getDirectCommitsSinceLastRelease(processedPRs) + directCommitsContent, err := g.getDirectCommitsSinceLastRelease(processedPRs, processedCommitSHAs) if err != nil { return fmt.Errorf("failed to get direct commits since last release: %w", err) } if directCommitsContent != "" { if content.Len() > 0 { - content.WriteString("\n") + content.WriteString("\n\n") } content.WriteString(directCommitsContent) } - // Check if we have any content at all if content.Len() == 0 { if len(files) == 0 { @@ -121,11 +129,6 @@ func (g *Generator) ProcessIncomingPRs() error { return nil } - version, err := g.detectVersion() - if err != nil { - return fmt.Errorf("failed to detect version: %w", err) - } - entry := fmt.Sprintf("## %s (%s)\n\n%s", version, time.Now().Format("2006-01-02"), content.String()) @@ -163,7 +166,7 @@ func (g *Generator) ProcessIncomingPRs() error { } // getDirectCommitsSinceLastRelease gets all direct commits (not part of PRs) since the last release -func (g *Generator) getDirectCommitsSinceLastRelease(processedPRs map[int]bool) (string, error) { +func (g *Generator) getDirectCommitsSinceLastRelease(processedPRs map[int]bool, processedCommitSHAs map[string]bool) (string, error) { // Get the latest tag to determine what commits are unreleased latestTag, err := g.gitWalker.GetLatestTag() if err != nil { @@ -189,16 +192,23 @@ func (g *Generator) getDirectCommitsSinceLastRelease(processedPRs map[int]bool) continue } - // Skip commits that belong to PRs we've already processed from incoming files + // Skip commits that belong to PRs we've already processed from incoming files (by PR number) if commit.PRNumber > 0 && processedPRs[commit.PRNumber] { continue } + // Skip commits whose SHA is already included in processed PRs (this catches commits + // that might not have been detected as part of a PR but are actually in the PR) + if processedCommitSHAs[commit.SHA] { + continue + } + // Only include commits that are NOT part of any PR (direct commits) if commit.PRNumber == 0 { directCommits = append(directCommits, commit) } } + if len(directCommits) == 0 { return "", nil // No direct commits } @@ -353,7 +363,8 @@ func (g *Generator) insertVersionAtTop(entry string) error { for insertionPoint < len(contentStr) && (contentStr[insertionPoint] == '\n' || contentStr[insertionPoint] == '\r') { insertionPoint++ } - newContent = contentStr[:loc[1]] + "\n" + entry + "\n" + contentStr[insertionPoint:] + // Insert with proper spacing: single newline after header, then entry, then newline before existing content + newContent = contentStr[:loc[1]] + "\n\n" + entry + "\n\n" + contentStr[insertionPoint:] } else { // Header not found, prepend everything. newContent = fmt.Sprintf("%s\n\n%s\n\n%s", header, entry, contentStr) diff --git a/cmd/generate_changelog/internal/config/config.go b/cmd/generate_changelog/internal/config/config.go index be70f8d7..a62668d1 100644 --- a/cmd/generate_changelog/internal/config/config.go +++ b/cmd/generate_changelog/internal/config/config.go @@ -1,19 +1,19 @@ package config type Config struct { - RepoPath string - OutputFile string - Limit int - Version string - SaveData bool - CacheFile string - NoCache bool - RebuildCache bool - GitHubToken string - ForcePRSync bool - EnableAISummary bool - IncomingPR int - ProcessPRs bool - IncomingDir string - Push bool + RepoPath string + OutputFile string + Limit int + Version string + SaveData bool + CacheFile string + NoCache bool + RebuildCache bool + GitHubToken string + ForcePRSync bool + EnableAISummary bool + IncomingPR int + ProcessPRsVersion string + IncomingDir string + Push bool } diff --git a/cmd/generate_changelog/main.go b/cmd/generate_changelog/main.go index c6ec60cc..333d9846 100644 --- a/cmd/generate_changelog/main.go +++ b/cmd/generate_changelog/main.go @@ -38,13 +38,13 @@ func init() { rootCmd.Flags().BoolVar(&cfg.ForcePRSync, "force-pr-sync", false, "Force a full PR sync from GitHub (ignores cache age)") rootCmd.Flags().BoolVar(&cfg.EnableAISummary, "ai-summarize", false, "Generate AI-enhanced summaries using Fabric") rootCmd.Flags().IntVar(&cfg.IncomingPR, "incoming-pr", 0, "Pre-process PR for changelog (provide PR number)") - rootCmd.Flags().BoolVar(&cfg.ProcessPRs, "process-prs", false, "Process all incoming PR files for release") + rootCmd.Flags().StringVar(&cfg.ProcessPRsVersion, "process-prs", "", "Process all incoming PR files for release (provide version like v1.4.262)") rootCmd.Flags().StringVar(&cfg.IncomingDir, "incoming-dir", "./cmd/generate_changelog/incoming", "Directory for incoming PR files") rootCmd.Flags().BoolVar(&cfg.Push, "push", false, "Enable automatic git push after creating an incoming entry") } func run(cmd *cobra.Command, args []string) error { - if cfg.IncomingPR > 0 && cfg.ProcessPRs { + if cfg.IncomingPR > 0 && cfg.ProcessPRsVersion != "" { return fmt.Errorf("--incoming-pr and --process-prs are mutually exclusive flags") } @@ -61,8 +61,8 @@ func run(cmd *cobra.Command, args []string) error { return generator.ProcessIncomingPR(cfg.IncomingPR) } - if cfg.ProcessPRs { - return generator.ProcessIncomingPRs() + if cfg.ProcessPRsVersion != "" { + return generator.ProcessIncomingPRs(cfg.ProcessPRsVersion) } output, err := generator.Generate() diff --git a/docs/Automated-ChangeLog.md b/docs/Automated-ChangeLog.md index c6d57730..3fd12210 100644 --- a/docs/Automated-ChangeLog.md +++ b/docs/Automated-ChangeLog.md @@ -156,9 +156,9 @@ Add to `internal/config/config.go`: ```go type Config struct { // ... existing fields - IncomingPR int // PR number for --incoming-pr - ProcessPRs bool // Flag for --process-prs - IncomingDir string // Directory for incoming files (default: ./cmd/generate_changelog/incoming/) + IncomingPR int // PR number for --incoming-pr + ProcessPRsVersion string // Flag for --process-prs (new version string) + IncomingDir string // Directory for incoming files (default: ./cmd/generate_changelog/incoming/) } ``` @@ -166,7 +166,7 @@ type Config struct { ```go rootCmd.Flags().IntVar(&cfg.IncomingPR, "incoming-pr", 0, "Pre-process PR for changelog (provide PR number)") -rootCmd.Flags().BoolVar(&cfg.ProcessPRs, "process-prs", false, "Process all incoming PR files for release") +rootCmd.Flags().StringVar(&cfg.ProcessPRsVersion, "process-prs", "", "Process all incoming PR files for release (provide version like v1.4.262)") rootCmd.Flags().StringVar(&cfg.IncomingDir, "incoming-dir", "./cmd/generate_changelog/incoming", "Directory for incoming PR files") ``` @@ -314,22 +314,22 @@ Update `.github/workflows/update-version-and-create-tag.yml`. ### Phase 1: Implement Developer Tooling -- [ ] Add new command line flags and configuration -- [ ] Implement `--incoming-pr` functionality -- [ ] Add validation for PR states and git status -- [ ] Create auto-commit logic +- [x] Add new command line flags and configuration +- [x] Implement `--incoming-pr` functionality +- [x] Add validation for PR states and git status +- [x] Create auto-commit logic ### Phase 2: Integration (CI/CD) Readiness -- [ ] Implement `--process-prs` functionality -- [ ] Add CHANGELOG.md insertion logic -- [ ] Update database storage for version entries +- [x] Implement `--process-prs` functionality +- [x] Add CHANGELOG.md insertion logic +- [x] Update database storage for version entries ### Phase 3: Deployment -- [ ] Update GitHub Actions workflow -- [ ] Create developer documentation in ./docs/ directory -- [ ] Test full end-to-end workflow (the PR that includes these modifications can be its first production test) +- [x] Update GitHub Actions workflow +- [x] Create developer documentation in ./docs/ directory +- [x] Test full end-to-end workflow (the PR that includes these modifications can be its first production test) ### Phase 4: Adoption diff --git a/docs/Automated-Changelog-Usage.md b/docs/Automated-Changelog-Usage.md index 55b82f94..7fa0af67 100644 --- a/docs/Automated-Changelog-Usage.md +++ b/docs/Automated-Changelog-Usage.md @@ -94,7 +94,7 @@ Specify custom directory for incoming PR files (default: `./cmd/generate_changel Process all incoming PR files for release aggregation. Used by CI/CD during release creation. -**Usage**: `./generate_changelog --process-prs` +**Usage**: `./generate_changelog --process-prs {new_version_string}` **Mutual Exclusivity**: Cannot be used with `--incoming-pr` flag