Skip to content

Commit f0edfd6

Browse files
authored
feat: copy libraries and global files from container output to repo in release init command (#1790)
Updates #1008
1 parent ad446e1 commit f0edfd6

File tree

6 files changed

+414
-106
lines changed

6 files changed

+414
-106
lines changed

internal/librarian/command.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"log/slog"
22+
"os"
2223
"path"
2324
"path/filepath"
2425
"strings"
@@ -191,6 +192,26 @@ func formatTimestamp(t time.Time) string {
191192
return t.Format(yyyyMMddHHmmss)
192193
}
193194

195+
// cleanAndCopyLibrary cleans the files of the given library in repoDir and copies
196+
// the new files from outputDir.
197+
func cleanAndCopyLibrary(state *config.LibrarianState, repoDir, libraryID, outputDir string) error {
198+
library := findLibraryByID(state, libraryID)
199+
if library == nil {
200+
return fmt.Errorf("library %q not found during clean and copy, despite being found in earlier steps", libraryID)
201+
}
202+
slog.Info("Clean destinations and copy generated results for library", "id", libraryID)
203+
if err := clean(repoDir, library.RemoveRegex, library.PreserveRegex); err != nil {
204+
return err
205+
}
206+
// os.CopyFS in Go1.24 returns error when copying from a symbolic link
207+
// https://github.com/golang/go/blob/9d828e80fa1f3cc52de60428cae446b35b576de8/src/os/dir.go#L143-L144
208+
if err := os.CopyFS(repoDir, os.DirFS(outputDir)); err != nil {
209+
return err
210+
}
211+
slog.Info("Library updated", "id", libraryID)
212+
return nil
213+
}
214+
194215
// commitAndPush creates a commit and push request to GitHub for the generated
195216
// changes.
196217
// It uses the GitHub client to create a PR with the specified branch, title, and

internal/librarian/command_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,84 @@ func TestCloneOrOpenLanguageRepo(t *testing.T) {
275275
}
276276
}
277277

278+
func TestCleanAndCopyLibrary(t *testing.T) {
279+
t.Parallel()
280+
for _, test := range []struct {
281+
name string
282+
libraryID string
283+
state *config.LibrarianState
284+
repo gitrepo.Repository
285+
outputDir string
286+
setup func(t *testing.T, outputDir string)
287+
wantErr bool
288+
errContains string
289+
}{
290+
{
291+
name: "library not found",
292+
libraryID: "non-existent-library",
293+
state: &config.LibrarianState{
294+
Libraries: []*config.LibraryState{
295+
{
296+
ID: "some-library",
297+
},
298+
},
299+
},
300+
repo: newTestGitRepo(t),
301+
wantErr: true,
302+
},
303+
{
304+
name: "clean fails",
305+
libraryID: "some-library",
306+
state: &config.LibrarianState{
307+
Libraries: []*config.LibraryState{
308+
{
309+
ID: "some-library",
310+
RemoveRegex: []string{"["}, // Invalid regex
311+
},
312+
},
313+
},
314+
repo: newTestGitRepo(t),
315+
wantErr: true,
316+
},
317+
{
318+
name: "copy fails on symlink",
319+
libraryID: "some-library",
320+
state: &config.LibrarianState{
321+
Libraries: []*config.LibraryState{
322+
{
323+
ID: "some-library",
324+
},
325+
},
326+
},
327+
repo: newTestGitRepo(t),
328+
setup: func(t *testing.T, outputDir string) {
329+
// Create a symlink in the output directory to trigger an error.
330+
if err := os.Symlink("target", filepath.Join(outputDir, "symlink")); err != nil {
331+
t.Fatalf("os.Symlink() = %v", err)
332+
}
333+
},
334+
wantErr: true,
335+
},
336+
} {
337+
t.Run(test.name, func(t *testing.T) {
338+
outputDir := t.TempDir()
339+
if test.setup != nil {
340+
test.setup(t, outputDir)
341+
}
342+
err := cleanAndCopyLibrary(test.state, test.repo.GetDir(), test.libraryID, outputDir)
343+
if test.wantErr {
344+
if err == nil {
345+
t.Errorf("%s should return error", test.name)
346+
}
347+
return
348+
}
349+
if err != nil {
350+
t.Fatal(err)
351+
}
352+
})
353+
}
354+
}
355+
278356
func TestCommitAndPush(t *testing.T) {
279357
for _, test := range []struct {
280358
name string

internal/librarian/generate.go

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -236,31 +236,13 @@ func (r *generateRunner) runGenerateCommand(ctx context.Context, libraryID, outp
236236
return "", err
237237
}
238238

239-
if err := r.cleanAndCopyLibrary(libraryID, outputDir); err != nil {
239+
if err := cleanAndCopyLibrary(r.state, r.repo.GetDir(), libraryID, outputDir); err != nil {
240240
return "", err
241241
}
242242

243243
return libraryID, nil
244244
}
245245

246-
func (r *generateRunner) cleanAndCopyLibrary(libraryID, outputDir string) error {
247-
library := findLibraryByID(r.state, libraryID)
248-
if library == nil {
249-
return fmt.Errorf("library %q not found during clean and copy, despite being found in earlier steps", libraryID)
250-
}
251-
slog.Info("Clean destinations and copy generated results for library", "id", libraryID)
252-
if err := clean(r.repo.GetDir(), library.RemoveRegex, library.PreserveRegex); err != nil {
253-
return err
254-
}
255-
// os.CopyFS in Go1.24 returns error when copying from a symbolic link
256-
// https://github.com/golang/go/blob/9d828e80fa1f3cc52de60428cae446b35b576de8/src/os/dir.go#L143-L144
257-
if err := os.CopyFS(r.repo.GetDir(), os.DirFS(outputDir)); err != nil {
258-
return err
259-
}
260-
slog.Info("Library updated", "id", libraryID)
261-
return nil
262-
}
263-
264246
// runBuildCommand orchestrates the building of an API library using a containerized
265247
// environment.
266248
//

internal/librarian/generate_test.go

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,86 +1541,3 @@ func TestCompileRegexps(t *testing.T) {
15411541
})
15421542
}
15431543
}
1544-
1545-
func TestCleanAndCopyLibrary(t *testing.T) {
1546-
t.Parallel()
1547-
for _, test := range []struct {
1548-
name string
1549-
libraryID string
1550-
state *config.LibrarianState
1551-
repo gitrepo.Repository
1552-
outputDir string
1553-
setup func(t *testing.T, r *generateRunner, outputDir string)
1554-
wantErr bool
1555-
errContains string
1556-
}{
1557-
{
1558-
name: "library not found",
1559-
libraryID: "non-existent-library",
1560-
state: &config.LibrarianState{
1561-
Libraries: []*config.LibraryState{
1562-
{
1563-
ID: "some-library",
1564-
},
1565-
},
1566-
},
1567-
repo: newTestGitRepo(t),
1568-
wantErr: true,
1569-
},
1570-
{
1571-
name: "clean fails",
1572-
libraryID: "some-library",
1573-
state: &config.LibrarianState{
1574-
Libraries: []*config.LibraryState{
1575-
{
1576-
ID: "some-library",
1577-
RemoveRegex: []string{"["}, // Invalid regex
1578-
},
1579-
},
1580-
},
1581-
repo: newTestGitRepo(t),
1582-
wantErr: true,
1583-
},
1584-
{
1585-
name: "copy fails on symlink",
1586-
libraryID: "some-library",
1587-
state: &config.LibrarianState{
1588-
Libraries: []*config.LibraryState{
1589-
{
1590-
ID: "some-library",
1591-
},
1592-
},
1593-
},
1594-
repo: newTestGitRepo(t),
1595-
setup: func(t *testing.T, r *generateRunner, outputDir string) {
1596-
// Create a symlink in the output directory to trigger an error.
1597-
if err := os.Symlink("target", filepath.Join(outputDir, "symlink")); err != nil {
1598-
t.Fatalf("os.Symlink() = %v", err)
1599-
}
1600-
},
1601-
wantErr: true,
1602-
},
1603-
} {
1604-
t.Run(test.name, func(t *testing.T) {
1605-
t.Parallel()
1606-
r := &generateRunner{
1607-
state: test.state,
1608-
repo: test.repo,
1609-
}
1610-
outputDir := t.TempDir()
1611-
if test.setup != nil {
1612-
test.setup(t, r, outputDir)
1613-
}
1614-
err := r.cleanAndCopyLibrary(test.libraryID, outputDir)
1615-
if test.wantErr {
1616-
if err == nil {
1617-
t.Errorf("%s should return error", test.name)
1618-
}
1619-
return
1620-
}
1621-
if err != nil {
1622-
t.Fatal(err)
1623-
}
1624-
})
1625-
}
1626-
}

internal/librarian/release_init.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,31 @@ func (r *initRunner) runInitCommand(ctx context.Context, outputDir string) error
126126
LibraryVersion: r.cfg.LibraryVersion,
127127
Output: outputDir,
128128
}
129-
return r.containerClient.ReleaseInit(ctx, initRequest)
129+
130+
if err := r.containerClient.ReleaseInit(ctx, initRequest); err != nil {
131+
return err
132+
}
133+
134+
for _, library := range r.state.Libraries {
135+
if r.cfg.Library != "" {
136+
if r.cfg.Library != library.ID {
137+
continue
138+
}
139+
// Only copy one library to repository.
140+
if err := cleanAndCopyLibrary(r.state, r.repo.GetDir(), r.cfg.Library, outputDir); err != nil {
141+
return err
142+
}
143+
144+
break
145+
}
146+
147+
// Copy all libraries to repository.
148+
if err := cleanAndCopyLibrary(r.state, r.repo.GetDir(), library.ID, outputDir); err != nil {
149+
return err
150+
}
151+
}
152+
153+
return cleanAndCopyGlobalAllowlist(r.librarianConfig, r.repo.GetDir(), outputDir)
130154
}
131155

132156
// updateLibrary updates the library which is the index-th library in the given
@@ -192,3 +216,25 @@ func getChangeType(commit *conventionalcommits.ConventionalCommit) string {
192216

193217
return changeType
194218
}
219+
220+
// cleanAndCopyGlobalAllowlist cleans the files listed in global allowlist in
221+
// repoDir, excluding read-only files and copies global files from outputDir.
222+
func cleanAndCopyGlobalAllowlist(cfg *config.LibrarianConfig, repoDir, outputDir string) error {
223+
for _, globalFile := range cfg.GlobalFilesAllowlist {
224+
if globalFile.Permissions == config.PermissionReadOnly {
225+
continue
226+
}
227+
228+
dst := filepath.Join(repoDir, globalFile.Path)
229+
if err := os.Remove(dst); err != nil {
230+
return fmt.Errorf("failed to remove global file, %s: %w", dst, err)
231+
}
232+
233+
src := filepath.Join(outputDir, globalFile.Path)
234+
if err := os.CopyFS(filepath.Dir(dst), os.DirFS(filepath.Dir(src))); err != nil {
235+
return fmt.Errorf("failed to copy global file %s to %s: %w", src, dst, err)
236+
}
237+
}
238+
239+
return nil
240+
}

0 commit comments

Comments
 (0)