diff --git a/copier/copier.go b/copier/copier.go index d989c23e199..38468e2ae39 100644 --- a/copier/copier.go +++ b/copier/copier.go @@ -14,6 +14,7 @@ import ( "path" "path/filepath" "slices" + "sort" "strconv" "strings" "sync" @@ -48,6 +49,7 @@ func init() { // "**" component in the pattern, filepath.Glob() will be called with the "**" // replaced with all of the subdirectories under that point, and the results // will be concatenated. +// The matched paths are returned in lexical order, which makes the output deterministic. func extendedGlob(pattern string) (matches []string, err error) { subdirs := func(dir string) []string { var subdirectories []string @@ -113,6 +115,7 @@ func extendedGlob(pattern string) (matches []string, err error) { } matches = append(matches, theseMatches...) } + sort.Strings(matches) return matches, nil } diff --git a/copier/copier_test.go b/copier/copier_test.go index 0b4dc87c634..14469ad944e 100644 --- a/copier/copier_test.go +++ b/copier/copier_test.go @@ -2341,3 +2341,18 @@ func TestConditionalRemoveNoChroot(t *testing.T) { testConditionalRemove(t) canChroot = couldChroot } + +func TestSortedExtendedGlob(t *testing.T) { + tmpdir := t.TempDir() + buf := []byte("buffer") + expect := []string{} + for _, name := range []string{"z", "y", "x", "a", "b", "c", "d", "e", "f"} { + require.NoError(t, os.WriteFile(filepath.Join(tmpdir, name), buf, 0o600)) + expect = append(expect, filepath.Join(tmpdir, name)) + } + sort.Strings(expect) + + matched, err := extendedGlob(filepath.Join(tmpdir, "*")) + require.NoError(t, err, "globbing") + require.ElementsMatch(t, expect, matched, "sorted globbing") +}