diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod index 384008fd379..23ae6d8d0b8 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/tools v0.44.0 // indirect - k8s.io/apimachinery v0.35.4 // indirect + k8s.io/apimachinery v0.36.0 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum index 425f8f4e402..61c6448b28c 100644 --- a/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum +++ b/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum @@ -85,6 +85,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= diff --git a/internal/cli/alpha/internal/update/update.go b/internal/cli/alpha/internal/update/update.go index 2a5e18f3867..d6384131f24 100644 --- a/internal/cli/alpha/internal/update/update.go +++ b/internal/cli/alpha/internal/update/update.go @@ -25,6 +25,7 @@ import ( log "log/slog" "os" "os/exec" + "path/filepath" "regexp" "strings" "time" @@ -446,12 +447,23 @@ func (opts *Update) prepareAncestorBranch() error { if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil { return fmt.Errorf("failed to create %s from %s: %w", opts.AncestorBranch, opts.FromBranch, err) } + + // Preserve boilerplate before cleanup + boilerplate := preserveBoilerplate() + if err := cleanupBranch(); err != nil { return fmt.Errorf("failed to cleanup the %s : %w", opts.AncestorBranch, err) } + + // Restore boilerplate before regeneration so alpha generate can use it + if err := restoreBoilerplate(boilerplate); err != nil { + log.Warn("failed to restore boilerplate file", "error", err) + } + if err := regenerateProjectWithVersion(opts.FromVersion); err != nil { return fmt.Errorf("failed to regenerate project with fromVersion %s: %w", opts.FromVersion, err) } + gitCmd := helpers.GitCmd(opts.GitConfig, "add", "--all") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in %s: %w", opts.AncestorBranch, err) @@ -478,6 +490,43 @@ func cleanupBranch() error { return nil } +// preserveBoilerplate reads and returns the content of the boilerplate file if it exists. +// This is used to preserve the license header across regeneration cycles. +func preserveBoilerplate() []byte { + boilerplatePath := filepath.Join("hack", "boilerplate.go.txt") + content, err := os.ReadFile(boilerplatePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + log.Warn("failed to read boilerplate file for preservation", "path", boilerplatePath, "error", err) + return nil + } + log.Info("Preserving existing license header file for regeneration") + return content +} + +// restoreBoilerplate writes the boilerplate content back to the filesystem. +// This ensures the license header is preserved even when using release binaries +// that don't have boilerplate preservation logic. +func restoreBoilerplate(content []byte) error { + if content == nil { + return nil + } + + boilerplatePath := filepath.Join("hack", "boilerplate.go.txt") + if err := os.MkdirAll("hack", 0o755); err != nil { + return fmt.Errorf("failed to create hack directory: %w", err) + } + + if err := os.WriteFile(boilerplatePath, content, 0o644); err != nil { + return fmt.Errorf("failed to write boilerplate file: %w", err) + } + + log.Info("Restored license header file after regeneration") + return nil +} + // runMakeTargets runs the make targets needed to keep the tree consistent. // If skipConflicts is true, it avoids running targets that are guaranteed // to fail noisily when there are unresolved conflicts. @@ -654,12 +703,22 @@ func (opts *Update) prepareUpgradeBranch() error { return fmt.Errorf("failed to checkout base branch %s: %w", opts.UpgradeBranch, err) } + // Preserve boilerplate before cleanup + boilerplate := preserveBoilerplate() + if err := cleanupBranch(); err != nil { return fmt.Errorf("failed to cleanup the %s branch: %w", opts.UpgradeBranch, err) } + + // Restore boilerplate before regeneration so alpha generate can use it + if err := restoreBoilerplate(boilerplate); err != nil { + log.Warn("failed to restore boilerplate file", "error", err) + } + if err := regenerateProjectWithVersion(opts.ToVersion); err != nil { return fmt.Errorf("failed to regenerate project with version %s: %w", opts.ToVersion, err) } + gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in %s: %w", opts.UpgradeBranch, err) diff --git a/internal/cli/alpha/internal/update/update_test.go b/internal/cli/alpha/internal/update/update_test.go index aa8c99ffee6..10d7a770c42 100644 --- a/internal/cli/alpha/internal/update/update_test.go +++ b/internal/cli/alpha/internal/update/update_test.go @@ -585,4 +585,83 @@ exit 1` Expect(msg).To(Equal(helpers.ConflictCommitMessage(opts.FromVersion, opts.ToVersion))) }) }) + + Context("Boilerplate preservation", func() { + var tmpDir string + var originalWD string + + BeforeEach(func() { + var err error + originalWD, err = os.Getwd() + Expect(err).NotTo(HaveOccurred()) + tmpDir, err = os.MkdirTemp("", "boilerplate-test") + Expect(err).NotTo(HaveOccurred()) + Expect(os.Chdir(tmpDir)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.Chdir(originalWD)).To(Succeed()) + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + }) + + It("preserves and restores boilerplate file", func() { + // Create boilerplate file + boilerplateContent := []byte(`/* +Copyright 2025 Test. + +Licensed under the Apache License, Version 2.0 (the "License"); +*/`) + err := os.MkdirAll("hack", 0755) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(filepath.Join("hack", "boilerplate.go.txt"), boilerplateContent, 0644) + Expect(err).NotTo(HaveOccurred()) + + // Preserve the boilerplate + preserved := preserveBoilerplate() + Expect(preserved).NotTo(BeNil()) + Expect(preserved).To(Equal(boilerplateContent)) + + // Simulate cleanup (remove the file) + err = os.RemoveAll("hack") + Expect(err).NotTo(HaveOccurred()) + + // Restore the boilerplate + err = restoreBoilerplate(preserved) + Expect(err).NotTo(HaveOccurred()) + + // Verify it was restored + restored, err := os.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) + Expect(err).NotTo(HaveOccurred()) + Expect(restored).To(Equal(boilerplateContent)) + }) + + It("returns nil when boilerplate file doesn't exist", func() { + // No boilerplate file exists + preserved := preserveBoilerplate() + Expect(preserved).To(BeNil()) + }) + + It("handles nil content gracefully in restore", func() { + // Restoring nil should be a no-op + err := restoreBoilerplate(nil) + Expect(err).NotTo(HaveOccurred()) + + // Verify no file was created + _, err = os.Stat(filepath.Join("hack", "boilerplate.go.txt")) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) + + It("creates hack directory if it doesn't exist during restore", func() { + boilerplateContent := []byte("/* Test */") + + // Restore without hack directory existing + err := restoreBoilerplate(boilerplateContent) + Expect(err).NotTo(HaveOccurred()) + + // Verify directory and file were created + restored, err := os.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) + Expect(err).NotTo(HaveOccurred()) + Expect(restored).To(Equal(boilerplateContent)) + }) + }) })