Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
59 changes: 59 additions & 0 deletions internal/cli/alpha/internal/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
log "log/slog"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
79 changes: 79 additions & 0 deletions internal/cli/alpha/internal/update/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
})
})
Loading