@@ -17,7 +17,6 @@ package github
1717import (
1818 "context"
1919 "fmt"
20- "path/filepath"
2120
2221 "github.com/google/go-github/v60/github"
2322)
@@ -45,11 +44,14 @@ func (c *Client) CreateReleasePR(ctx context.Context, req PRRequest) (*PRResult,
4544 return nil , fmt .Errorf ("creating branch: %w" , err )
4645 }
4746
48- // Commit the files to the new branch
49- for _ , filePath := range req .Files {
50- if err := c .commitFile (ctx , req .Owner , req .Repo , req .HeadBranch , filePath , req .TriggeredBy ); err != nil {
51- return nil , fmt .Errorf ("committing file %s: %w" , filePath , err )
52- }
47+ // Deduplicate files - each file already has all YAML path changes applied
48+ // on disk, so committing the same file twice causes 409 conflicts due to
49+ // GitHub API eventual consistency with sequential SHA updates.
50+ uniqueFiles := deduplicateFiles (req .Files )
51+
52+ // Commit all files to the new branch in a single atomic commit
53+ if err := c .commitFiles (ctx , req .Owner , req .Repo , req .HeadBranch , uniqueFiles , req .TriggeredBy ); err != nil {
54+ return nil , fmt .Errorf ("committing files: %w" , err )
5355 }
5456
5557 // Create the pull request
@@ -72,43 +74,81 @@ func (c *Client) CreateReleasePR(ctx context.Context, req PRRequest) (*PRResult,
7274 }, nil
7375}
7476
75- // commitFile commits a single file to a branch.
77+ // deduplicateFiles returns a new slice with duplicate file paths removed,
78+ // preserving the order of first occurrence.
79+ func deduplicateFiles (files []string ) []string {
80+ seen := make (map [string ]bool , len (files ))
81+ unique := make ([]string , 0 , len (files ))
82+ for _ , f := range files {
83+ if ! seen [f ] {
84+ seen [f ] = true
85+ unique = append (unique , f )
86+ }
87+ }
88+ return unique
89+ }
90+
91+ // commitFiles commits all files to a branch in a single atomic commit using the Git Data API.
7692// If triggeredBy is non-empty, a git trailer is added to the commit message.
77- func (c * Client ) commitFile (ctx context.Context , owner , repo , branch , filePath , triggeredBy string ) error {
78- // Read file content using the fileReader interface
79- content , err := c .fileReader . ReadFile ( filePath )
93+ func (c * Client ) commitFiles (ctx context.Context , owner , repo , branch string , files [] string , triggeredBy string ) error {
94+ // Get the current branch reference
95+ ref , _ , err := c .client . Git . GetRef ( ctx , owner , repo , "refs/heads/" + branch )
8096 if err != nil {
81- return fmt .Errorf ("reading file : %w" , err )
97+ return fmt .Errorf ("getting branch ref : %w" , err )
8298 }
8399
84- // Get current file ( to get SHA for update)
85- existingFile , _ , _ , err := c .client .Repositories . GetContents (
86- ctx , owner , repo , filePath ,
87- & github. RepositoryContentGetOptions { Ref : branch },
88- )
100+ // Get the commit to find the base tree
101+ baseCommit , _ , err := c .client .Git . GetCommit ( ctx , owner , repo , ref . GetObject (). GetSHA ())
102+ if err != nil {
103+ return fmt . Errorf ( "getting base commit: %w" , err )
104+ }
89105
90- message := fmt .Sprintf ("Update %s for release" , filepath .Base (filePath ))
91- if triggeredBy != "" {
92- message += fmt .Sprintf ("\n \n Release-Triggered-By: %s" , triggeredBy )
106+ // Build tree entries for all files
107+ entries := make ([]* github.TreeEntry , 0 , len (files ))
108+ for _ , filePath := range files {
109+ content , err := c .fileReader .ReadFile (filePath )
110+ if err != nil {
111+ return fmt .Errorf ("reading file %s: %w" , filePath , err )
112+ }
113+ contentStr := string (content )
114+ entries = append (entries , & github.TreeEntry {
115+ Path : github .String (filePath ),
116+ Mode : github .String ("100644" ),
117+ Type : github .String ("blob" ),
118+ Content : github .String (contentStr ),
119+ })
93120 }
94121
95- opts := & github. RepositoryContentFileOptions {
96- Message : github . String ( message ),
97- Content : content ,
98- Branch : github . String ( branch ),
122+ // Create a new tree with all file changes
123+ tree , _ , err := c . client . Git . CreateTree ( ctx , owner , repo , baseCommit . GetTree (). GetSHA (), entries )
124+ if err != nil {
125+ return fmt . Errorf ( "creating tree: %w" , err )
99126 }
100127
101- if err == nil && existingFile != nil {
102- // File exists - update it
103- opts .SHA = existingFile .SHA
104- _ , _ , err = c .client .Repositories .UpdateFile (ctx , owner , repo , filePath , opts )
105- } else {
106- // File doesn't exist - create it
107- _ , _ , err = c .client .Repositories .CreateFile (ctx , owner , repo , filePath , opts )
128+ // Build commit message
129+ message := "Update release files"
130+ if triggeredBy != "" {
131+ message += fmt .Sprintf ("\n \n Release-Triggered-By: %s" , triggeredBy )
132+ }
133+
134+ // Create the commit
135+ commit , _ , err := c .client .Git .CreateCommit (ctx , owner , repo ,
136+ & github.Commit {
137+ Message : github .String (message ),
138+ Tree : tree ,
139+ Parents : []* github.Commit {baseCommit },
140+ },
141+ nil ,
142+ )
143+ if err != nil {
144+ return fmt .Errorf ("creating commit: %w" , err )
108145 }
109146
147+ // Update the branch reference to point to the new commit
148+ ref .Object .SHA = commit .SHA
149+ _ , _ , err = c .client .Git .UpdateRef (ctx , owner , repo , ref , false )
110150 if err != nil {
111- return fmt .Errorf ("updating file : %w" , err )
151+ return fmt .Errorf ("updating ref : %w" , err )
112152 }
113153
114154 return nil
0 commit comments