Skip to content

Commit a8ddd21

Browse files
authored
fix(internal/librarian): don't clean dir for release init copies (#1824)
We should intentionally copy over only the files that are in the output directory of the release init container. We don't mandate today that all libraries files must be copied over, so we should selectively overwrite files when required. Fixes: #1806
1 parent 5ebd352 commit a8ddd21

File tree

4 files changed

+233
-18
lines changed

4 files changed

+233
-18
lines changed

internal/librarian/command.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,60 @@ func cleanAndCopyLibrary(state *config.LibrarianState, repoDir, libraryID, outpu
208208
return copyLibrary(repoDir, outputDir, library)
209209
}
210210

211+
func copyLibraryFiles(state *config.LibrarianState, dest, libraryID, src string) error {
212+
library := findLibraryByID(state, libraryID)
213+
if library == nil {
214+
return fmt.Errorf("library %q not found", libraryID)
215+
}
216+
slog.Info("Copying library files", "id", library.ID, "destination", dest, "source", src)
217+
for _, srcRoot := range library.SourceRoots {
218+
dstPath := filepath.Join(dest, srcRoot)
219+
srcPath := filepath.Join(src, srcRoot)
220+
files, err := getDirectoryFilesnames(srcPath)
221+
if err != nil {
222+
return err
223+
}
224+
for _, file := range files {
225+
slog.Info("Copying file", "file", file)
226+
srcFile := filepath.Join(srcPath, file)
227+
dstFile := filepath.Join(dstPath, file)
228+
if err := copyFile(dstFile, srcFile); err != nil {
229+
return fmt.Errorf("failed to copy file %q for library %s: %w", srcFile, library.ID, err)
230+
}
231+
}
232+
}
233+
return nil
234+
}
235+
236+
func getDirectoryFilesnames(dir string) ([]string, error) {
237+
if _, err := os.Stat(dir); err != nil {
238+
// Skip dirs that don't exist
239+
if os.IsNotExist(err) {
240+
return nil, nil
241+
}
242+
return nil, err
243+
}
244+
245+
var fileNames []string
246+
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
247+
if err != nil {
248+
return err
249+
}
250+
if !d.IsDir() {
251+
relativePath, err := filepath.Rel(dir, path)
252+
if err != nil {
253+
return err
254+
}
255+
fileNames = append(fileNames, relativePath)
256+
}
257+
return nil
258+
})
259+
if err != nil {
260+
return nil, err
261+
}
262+
return fileNames, nil
263+
}
264+
211265
// copyLibrary copies library file from src to dst.
212266
func copyLibrary(dst, src string, library *config.LibraryState) error {
213267
slog.Info("Copying library", "id", library.ID, "destination", dst, "source", src)

internal/librarian/command_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,141 @@ func TestCommitAndPush(t *testing.T) {
725725
}
726726
}
727727

728+
func TestCopyLibraryFiles(t *testing.T) {
729+
t.Parallel()
730+
setup := func(src string, files []string) {
731+
for _, relPath := range files {
732+
fullPath := filepath.Join(src, relPath)
733+
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
734+
t.Error(err)
735+
}
736+
737+
if _, err := os.Create(fullPath); err != nil {
738+
t.Error(err)
739+
}
740+
}
741+
}
742+
for _, test := range []struct {
743+
name string
744+
repoDir string
745+
outputDir string
746+
libraryID string
747+
state *config.LibrarianState
748+
filesToCreate []string
749+
wantFiles []string
750+
skipFiles []string
751+
wantErr bool
752+
wantErrMsg string
753+
}{
754+
{
755+
name: "copy library files",
756+
repoDir: filepath.Join(t.TempDir(), "dst"),
757+
outputDir: filepath.Join(t.TempDir(), "src"),
758+
libraryID: "example-library",
759+
state: &config.LibrarianState{
760+
Libraries: []*config.LibraryState{
761+
{
762+
ID: "example-library",
763+
SourceRoots: []string{
764+
"a/path",
765+
"another/path",
766+
},
767+
},
768+
},
769+
},
770+
filesToCreate: []string{
771+
"a/path/example.txt",
772+
"another/path/example.txt",
773+
"skipped/path/example.txt",
774+
},
775+
wantFiles: []string{
776+
"a/path/example.txt",
777+
"another/path/example.txt",
778+
},
779+
skipFiles: []string{
780+
"skipped/path/example.txt",
781+
},
782+
},
783+
{
784+
name: "library not found",
785+
repoDir: filepath.Join(t.TempDir(), "dst"),
786+
outputDir: filepath.Join(t.TempDir(), "src"),
787+
libraryID: "non-existent-library",
788+
state: &config.LibrarianState{
789+
Libraries: []*config.LibraryState{
790+
{
791+
ID: "example-library",
792+
},
793+
},
794+
},
795+
wantErr: true,
796+
wantErrMsg: "not found",
797+
},
798+
{
799+
repoDir: filepath.Join(t.TempDir(), "dst"),
800+
name: "one source root empty",
801+
outputDir: filepath.Join(t.TempDir(), "src"),
802+
libraryID: "example-library",
803+
state: &config.LibrarianState{
804+
Libraries: []*config.LibraryState{
805+
{
806+
ID: "example-library",
807+
SourceRoots: []string{
808+
"a/path",
809+
"another/path",
810+
},
811+
},
812+
},
813+
},
814+
filesToCreate: []string{
815+
"a/path/example.txt",
816+
"skipped/path/example.txt",
817+
},
818+
wantFiles: []string{
819+
"a/path/example.txt",
820+
},
821+
skipFiles: []string{
822+
"skipped/path/example.txt",
823+
},
824+
},
825+
} {
826+
t.Run(test.name, func(t *testing.T) {
827+
if !test.wantErr {
828+
setup(test.outputDir, test.filesToCreate)
829+
}
830+
err := copyLibraryFiles(test.state, test.repoDir, test.libraryID, test.outputDir)
831+
if test.wantErr {
832+
if err == nil {
833+
t.Errorf("copyLibraryFiles() shoud fail")
834+
}
835+
836+
if !strings.Contains(err.Error(), test.wantErrMsg) {
837+
t.Errorf("want error message: %s, got: %s", test.wantErrMsg, err.Error())
838+
}
839+
840+
return
841+
}
842+
if err != nil {
843+
t.Errorf("failed to run copyLibraryFiles(): %s", err.Error())
844+
}
845+
846+
for _, file := range test.wantFiles {
847+
fullPath := filepath.Join(test.repoDir, file)
848+
if _, err := os.Stat(fullPath); err != nil {
849+
t.Errorf("file %s is not copied to %s", file, test.repoDir)
850+
}
851+
}
852+
853+
for _, file := range test.skipFiles {
854+
fullPath := filepath.Join(test.repoDir, file)
855+
if _, err := os.Stat(fullPath); !os.IsNotExist(err) {
856+
t.Errorf("file %s should not be copied to %s", file, test.repoDir)
857+
}
858+
}
859+
})
860+
}
861+
}
862+
728863
func TestCopyFile(t *testing.T) {
729864
t.Parallel()
730865
for _, test := range []struct {

internal/librarian/release_init.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func (r *initRunner) runInitCommand(ctx context.Context, outputDir string) error
152152
return fmt.Errorf("failed to copy librarian dir from %s to %s: %w", src, dst, err)
153153
}
154154

155-
if err := cleanAndCopyGlobalAllowlist(r.librarianConfig, dst, src); err != nil {
155+
if err := copyGlobalAllowlist(r.librarianConfig, dst, src, true); err != nil {
156156
return fmt.Errorf("failed to copy global allowlist from %s to %s: %w", src, dst, err)
157157
}
158158

@@ -176,20 +176,20 @@ func (r *initRunner) runInitCommand(ctx context.Context, outputDir string) error
176176
continue
177177
}
178178
// Only copy one library to repository.
179-
if err := cleanAndCopyLibrary(r.state, r.repo.GetDir(), r.cfg.Library, outputDir); err != nil {
179+
if err := copyLibraryFiles(r.state, r.repo.GetDir(), r.cfg.Library, outputDir); err != nil {
180180
return err
181181
}
182182

183183
break
184184
}
185185

186186
// Copy all libraries to repository.
187-
if err := cleanAndCopyLibrary(r.state, r.repo.GetDir(), library.ID, outputDir); err != nil {
187+
if err := copyLibraryFiles(r.state, r.repo.GetDir(), library.ID, outputDir); err != nil {
188188
return err
189189
}
190190
}
191191

192-
return cleanAndCopyGlobalAllowlist(r.librarianConfig, r.repo.GetDir(), outputDir)
192+
return copyGlobalAllowlist(r.librarianConfig, r.repo.GetDir(), outputDir, false)
193193
}
194194

195195
// updateLibrary updates the given library in the following way:
@@ -253,29 +253,26 @@ func getChangeType(commit *conventionalcommits.ConventionalCommit) string {
253253
return changeType
254254
}
255255

256-
// cleanAndCopyGlobalAllowlist cleans the files listed in global allowlist in
257-
// src, excluding read-only files and copies global files from src.
258-
func cleanAndCopyGlobalAllowlist(cfg *config.LibrarianConfig, dst, src string) error {
256+
// copyGlobalAllowlist copies files in the global file allowlist excluding
257+
//
258+
// read-only files and copies global files from src.
259+
func copyGlobalAllowlist(cfg *config.LibrarianConfig, dst, src string, copyReadOnly bool) error {
259260
if cfg == nil {
260261
slog.Info("librarian config is not setup, skip copying global allowlist")
261262
return nil
262263
}
264+
slog.Info("Copying global allowlist files", "destination", dst, "source", src)
263265
for _, globalFile := range cfg.GlobalFilesAllowlist {
264-
if globalFile.Permissions == config.PermissionReadOnly {
266+
if globalFile.Permissions == config.PermissionReadOnly && !copyReadOnly {
267+
slog.Debug("skipping read-only file", "path", globalFile.Path)
265268
continue
266269
}
267-
268-
dstPath := filepath.Join(dst, globalFile.Path)
269-
if err := os.Remove(dstPath); err != nil {
270-
return fmt.Errorf("failed to remove global file %s: %w", dstPath, err)
271-
}
272-
273270
srcPath := filepath.Join(src, globalFile.Path)
271+
dstPath := filepath.Join(dst, globalFile.Path)
274272
if err := copyFile(dstPath, srcPath); err != nil {
275273
return fmt.Errorf("failed to copy global file %s from %s: %w", dstPath, srcPath, err)
276274
}
277275
}
278-
279276
return nil
280277
}
281278

internal/librarian/release_init_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ func TestUpdateLibrary(t *testing.T) {
549549
}
550550
}
551551

552-
func TestCleanAndCopyGlobalAllowlist(t *testing.T) {
552+
func TestCopyGlobalAllowlist(t *testing.T) {
553553
t.Parallel()
554554
for _, test := range []struct {
555555
name string
@@ -560,6 +560,7 @@ func TestCleanAndCopyGlobalAllowlist(t *testing.T) {
560560
doNotCreateOutput bool // do not create files in output dir.
561561
wantErr bool
562562
wantErrMsg string
563+
copyReadOnly bool
563564
}{
564565
{
565566
name: "copied all global allowlist",
@@ -634,7 +635,7 @@ func TestCleanAndCopyGlobalAllowlist(t *testing.T) {
634635
"ignored/path/example.txt",
635636
},
636637
wantErr: true,
637-
wantErrMsg: "failed to remove global file",
638+
wantErrMsg: "failed to open file",
638639
},
639640
{
640641
name: "output doesn't have the global file",
@@ -653,6 +654,34 @@ func TestCleanAndCopyGlobalAllowlist(t *testing.T) {
653654
wantErr: true,
654655
wantErrMsg: "failed to copy global file",
655656
},
657+
{
658+
name: "copies read-only files",
659+
copyReadOnly: true,
660+
cfg: &config.LibrarianConfig{
661+
GlobalFilesAllowlist: []*config.GlobalFile{
662+
{
663+
Path: "one/path/example.txt",
664+
Permissions: "read-write",
665+
},
666+
{
667+
Path: "another/path/example.txt",
668+
Permissions: "read-only",
669+
},
670+
},
671+
},
672+
files: []string{
673+
"one/path/example.txt",
674+
"another/path/example.txt",
675+
"ignored/path/example.txt",
676+
},
677+
copied: []string{
678+
"one/path/example.txt",
679+
"another/path/example.txt",
680+
},
681+
skipped: []string{
682+
"ignored/path/example.txt",
683+
},
684+
},
656685
} {
657686
t.Run(test.name, func(t *testing.T) {
658687
output := t.TempDir()
@@ -687,7 +716,7 @@ func TestCleanAndCopyGlobalAllowlist(t *testing.T) {
687716
}
688717
}
689718

690-
err := cleanAndCopyGlobalAllowlist(test.cfg, repo, output)
719+
err := copyGlobalAllowlist(test.cfg, repo, output, test.copyReadOnly)
691720

692721
if test.wantErr {
693722
if err == nil {

0 commit comments

Comments
 (0)