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
This commit is contained in:
Kayvan Sylvan
2025-07-21 07:36:30 -07:00
parent b8008a34fb
commit 91c1aca0dd
6 changed files with 59 additions and 48 deletions

View File

@@ -87,7 +87,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
go run ./cmd/generate_changelog --process-prs go run ./cmd/generate_changelog --process-prs ${{ env.new_tag }}
- name: Commit changes - name: Commit changes
run: | run: |
# These files are modified by the version bump process # These files are modified by the version bump process

View File

@@ -62,7 +62,7 @@ func (g *Generator) ProcessIncomingPR(prNumber int) error {
} }
// ProcessIncomingPRs aggregates all incoming PR files for release and includes direct commits // 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")) files, err := filepath.Glob(filepath.Join(g.cfg.IncomingDir, "*.txt"))
if err != nil { if err != nil {
return fmt.Errorf("failed to scan incoming directory: %w", err) 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, "; ")) 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) processedPRs := make(map[int]bool)
processedCommitSHAs := make(map[string]bool)
for _, file := range files { for _, file := range files {
// Extract PR number from filename (e.g., "1640.txt" -> 1640) // Extract PR number from filename (e.g., "1640.txt" -> 1640)
filename := filepath.Base(file) filename := filepath.Base(file)
if prNumStr := strings.TrimSuffix(filename, ".txt"); prNumStr != filename { if prNumStr := strings.TrimSuffix(filename, ".txt"); prNumStr != filename {
if prNum, err := strconv.Atoi(prNumStr); err == nil { if prNum, err := strconv.Atoi(prNumStr); err == nil {
processedPRs[prNum] = true 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 // 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 { if err != nil {
return fmt.Errorf("failed to get direct commits since last release: %w", err) return fmt.Errorf("failed to get direct commits since last release: %w", err)
} }
if directCommitsContent != "" { if directCommitsContent != "" {
if content.Len() > 0 { if content.Len() > 0 {
content.WriteString("\n") content.WriteString("\n\n")
} }
content.WriteString(directCommitsContent) content.WriteString(directCommitsContent)
} }
// Check if we have any content at all // Check if we have any content at all
if content.Len() == 0 { if content.Len() == 0 {
if len(files) == 0 { if len(files) == 0 {
@@ -121,11 +129,6 @@ func (g *Generator) ProcessIncomingPRs() error {
return nil 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", entry := fmt.Sprintf("## %s (%s)\n\n%s",
version, time.Now().Format("2006-01-02"), content.String()) 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 // 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 // Get the latest tag to determine what commits are unreleased
latestTag, err := g.gitWalker.GetLatestTag() latestTag, err := g.gitWalker.GetLatestTag()
if err != nil { if err != nil {
@@ -189,16 +192,23 @@ func (g *Generator) getDirectCommitsSinceLastRelease(processedPRs map[int]bool)
continue 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] { if commit.PRNumber > 0 && processedPRs[commit.PRNumber] {
continue 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) // Only include commits that are NOT part of any PR (direct commits)
if commit.PRNumber == 0 { if commit.PRNumber == 0 {
directCommits = append(directCommits, commit) directCommits = append(directCommits, commit)
} }
} }
if len(directCommits) == 0 { if len(directCommits) == 0 {
return "", nil // No direct commits 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') { for insertionPoint < len(contentStr) && (contentStr[insertionPoint] == '\n' || contentStr[insertionPoint] == '\r') {
insertionPoint++ 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 { } else {
// Header not found, prepend everything. // Header not found, prepend everything.
newContent = fmt.Sprintf("%s\n\n%s\n\n%s", header, entry, contentStr) newContent = fmt.Sprintf("%s\n\n%s\n\n%s", header, entry, contentStr)

View File

@@ -1,19 +1,19 @@
package config package config
type Config struct { type Config struct {
RepoPath string RepoPath string
OutputFile string OutputFile string
Limit int Limit int
Version string Version string
SaveData bool SaveData bool
CacheFile string CacheFile string
NoCache bool NoCache bool
RebuildCache bool RebuildCache bool
GitHubToken string GitHubToken string
ForcePRSync bool ForcePRSync bool
EnableAISummary bool EnableAISummary bool
IncomingPR int IncomingPR int
ProcessPRs bool ProcessPRsVersion string
IncomingDir string IncomingDir string
Push bool Push bool
} }

View File

@@ -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.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().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().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().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") rootCmd.Flags().BoolVar(&cfg.Push, "push", false, "Enable automatic git push after creating an incoming entry")
} }
func run(cmd *cobra.Command, args []string) error { 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") 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) return generator.ProcessIncomingPR(cfg.IncomingPR)
} }
if cfg.ProcessPRs { if cfg.ProcessPRsVersion != "" {
return generator.ProcessIncomingPRs() return generator.ProcessIncomingPRs(cfg.ProcessPRsVersion)
} }
output, err := generator.Generate() output, err := generator.Generate()

View File

@@ -156,9 +156,9 @@ Add to `internal/config/config.go`:
```go ```go
type Config struct { type Config struct {
// ... existing fields // ... existing fields
IncomingPR int // PR number for --incoming-pr IncomingPR int // PR number for --incoming-pr
ProcessPRs bool // Flag for --process-prs ProcessPRsVersion string // Flag for --process-prs (new version string)
IncomingDir string // Directory for incoming files (default: ./cmd/generate_changelog/incoming/) IncomingDir string // Directory for incoming files (default: ./cmd/generate_changelog/incoming/)
} }
``` ```
@@ -166,7 +166,7 @@ type Config struct {
```go ```go
rootCmd.Flags().IntVar(&cfg.IncomingPR, "incoming-pr", 0, "Pre-process PR for changelog (provide PR number)") 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().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 ### Phase 1: Implement Developer Tooling
- [ ] Add new command line flags and configuration - [x] Add new command line flags and configuration
- [ ] Implement `--incoming-pr` functionality - [x] Implement `--incoming-pr` functionality
- [ ] Add validation for PR states and git status - [x] Add validation for PR states and git status
- [ ] Create auto-commit logic - [x] Create auto-commit logic
### Phase 2: Integration (CI/CD) Readiness ### Phase 2: Integration (CI/CD) Readiness
- [ ] Implement `--process-prs` functionality - [x] Implement `--process-prs` functionality
- [ ] Add CHANGELOG.md insertion logic - [x] Add CHANGELOG.md insertion logic
- [ ] Update database storage for version entries - [x] Update database storage for version entries
### Phase 3: Deployment ### Phase 3: Deployment
- [ ] Update GitHub Actions workflow - [x] Update GitHub Actions workflow
- [ ] Create developer documentation in ./docs/ directory - [x] 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] Test full end-to-end workflow (the PR that includes these modifications can be its first production test)
### Phase 4: Adoption ### Phase 4: Adoption

View File

@@ -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. 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 **Mutual Exclusivity**: Cannot be used with `--incoming-pr` flag