Repository: kubernetes-sigs/kubebuilder (2026-03-29T15:37:43Z)

Issue:
Feature Request: Support `YEAR` placeholder in `hack/boilerplate.go.txt`
### What do you want to happen?

The scaffolded `hack/boilerplate.go.txt` currently contains a hard-coded copyright year (e.g. `Copyright 2026 The Kubernetes authors.`). Over time this becomes stale. Both [gengo](https://github.com/kubernetes/gengo/blob/5ee0d033ba5bcb073f0e69c32057520b82d75ccb/v2/execute.go#L59)[^1] and [controller-gen](https://github.com/kubernetes-sigs/controller-tools/pull/544)[^2] already support a `YEAR` placeholder that is substituted with the current year at code-generation time.

### Proposed change

1. **Scaffold `YEAR` instead of a resolved year.** `kubebuilder init` writes `Copyright YEAR The Kubernetes authors.` into `hack/boilerplate.go.txt` rather than the current year.

2. **Substitute `YEAR` when scaffolding files.** When kubebuilder reads `boilerplate.go.txt` from disk before scaffolding new files (e.g. `kubebuilder create api`), it replaces `YEAR` with the current year — matching gengo's behavior — so generated Go files always get the correct year in their header.

3. **Pass `year=$(YEAR)` to `controller-gen` in the generated `Makefile`.** The `generate` target becomes:
   ```makefile
   YEAR ?= $(shell date +%Y)
   ...
   "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
   ```
   This uses the `year=` option introduced in [controller-tools#544](https://github.com/kubernetes-sigs/controller-tools/pull/544) so `make generate` also resolves the placeholder in DeepCopy file headers.

### Why this is useful

- Copyright years in scaffolded files are always current without any manual edits.
- Consistent with the conventions already established in gengo and controller-gen.
- The `YEAR` variable in the Makefile can be overridden by the user (`make generate YEAR=2024`) for edge cases.

---


[^1]: https://github.com/kubernetes/gengo/blob/5ee0d033ba5bcb073f0e69c32057520b82d75ccb/v2/execute.go#L59
[^2]: https://github.com/kubernetes-sigs/controller-tools/pull/544

### Extra Labels

_No response_


Discussion:
If this is something that the authors would be interested in, I would be up for opening a PR. :)
Hi @felix-kaestner 

Please, feel free to push that !!
It would be amazing !!



Patch:
diff --git a/pkg/machinery/scaffold.go b/pkg/machinery/scaffold.go
index 018803b2d77..31edd49cb62 100644
--- a/pkg/machinery/scaffold.go
+++ b/pkg/machinery/scaffold.go
@@ -25,8 +25,10 @@ import (
 	"os"
 	"path/filepath"
 	"slices"
+	"strconv"
 	"strings"
 	"text/template"
+	"time"
 
 	"github.com/spf13/afero"
 	"golang.org/x/tools/imports"
@@ -108,10 +110,11 @@ func WithConfig(cfg config.Config) ScaffoldOption {
 	}
 }
 
-// WithBoilerplate provides the boilerplate to the Scaffold
+// WithBoilerplate provides the boilerplate to the Scaffold.
+// Any YEAR placeholder in the boilerplate is substituted with the current year.
 func WithBoilerplate(boilerplate string) ScaffoldOption {
 	return func(s *Scaffold) {
-		s.injector.boilerplate = boilerplate
+		s.injector.boilerplate = SubstituteYear(boilerplate)
 	}
 }
 
@@ -546,3 +549,9 @@ func (s Scaffold) writeFile(f *File) error {
 
 	return nil
 }
+
+// SubstituteYear replaces every occurrence of "YEAR" in the boilerplate string
+// with the current UTC year.
+func SubstituteYear(boilerplate string) string {
+	return strings.ReplaceAll(boilerplate, "YEAR", strconv.Itoa(time.Now().UTC().Year()))
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go
index b200fca0f90..fc6c804de2d 100644
--- a/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go
@@ -19,7 +19,6 @@ package hack
 import (
 	"fmt"
 	"path/filepath"
-	"time"
 
 	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
 )
@@ -42,9 +41,6 @@ type Boilerplate struct {
 
 	// Owner is the copyright owner - e.g. "The Kubernetes Authors"
 	Owner string
-
-	// Year is the copyright year
-	Year string
 }
 
 // Validate implements file.RequiresValidation
@@ -79,10 +75,6 @@ func (f *Boilerplate) SetTemplateDefaults() error {
 		}
 	}
 
-	if f.Year == "" {
-		f.Year = fmt.Sprintf("%v", time.Now().Year())
-	}
-
 	// Boilerplate given
 	if len(f.Boilerplate) > 0 {
 		f.TemplateBody = f.Boilerplate
@@ -96,9 +88,9 @@ func (f *Boilerplate) SetTemplateDefaults() error {
 
 const boilerplateTemplate = `/*
 {{ if .Owner -}}
-Copyright {{ .Year }} {{ .Owner }}.
+Copyright YEAR {{ .Owner }}.
 {{- else -}}
-Copyright {{ .Year }}.
+Copyright YEAR.
 {{- end }}
 {{ index .Licenses .License }}*/`
 
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go
index 49253d2d03c..165f727ce45 100644
--- a/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go
@@ -75,6 +75,8 @@ func (f *Makefile) SetTemplateDefaults() error {
 //nolint:lll
 const makefileTemplate = `# Image URL to use all building/pushing image targets
 IMG ?= {{ .Image }}
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -123,7 +125,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
 	{{ if .BoilerplatePath -}}
-	"$(CONTROLLER_GEN)" object:headerFile={{printf "%q" .BoilerplatePath}} paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile={{printf "%q" .BoilerplatePath}},year=$(YEAR) paths="./..."
 	{{- else -}}
 	"$(CONTROLLER_GEN)" object paths="./..."
 	{{- end }}


Test patch:
diff --git a/docs/book/src/cronjob-tutorial/testdata/project/Makefile b/docs/book/src/cronjob-tutorial/testdata/project/Makefile
index ba1ccce7417..12ac6f55fb0 100644
--- a/docs/book/src/cronjob-tutorial/testdata/project/Makefile
+++ b/docs/book/src/cronjob-tutorial/testdata/project/Makefile
@@ -1,5 +1,7 @@
 # Image URL to use all building/pushing image targets
 IMG ?= controller:latest
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -51,7 +53,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
-	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/docs/book/src/cronjob-tutorial/testdata/project/hack/boilerplate.go.txt b/docs/book/src/cronjob-tutorial/testdata/project/hack/boilerplate.go.txt
index 7917ef17315..6bd3b726196 100644
--- a/docs/book/src/cronjob-tutorial/testdata/project/hack/boilerplate.go.txt
+++ b/docs/book/src/cronjob-tutorial/testdata/project/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2026 The Kubernetes authors.
+Copyright YEAR The Kubernetes authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/docs/book/src/getting-started/testdata/project/Makefile b/docs/book/src/getting-started/testdata/project/Makefile
index 832989f2b16..4dd1f3f6df9 100644
--- a/docs/book/src/getting-started/testdata/project/Makefile
+++ b/docs/book/src/getting-started/testdata/project/Makefile
@@ -1,5 +1,7 @@
 # Image URL to use all building/pushing image targets
 IMG ?= controller:latest
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -47,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
-	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/docs/book/src/getting-started/testdata/project/hack/boilerplate.go.txt b/docs/book/src/getting-started/testdata/project/hack/boilerplate.go.txt
index 7917ef17315..6bd3b726196 100644
--- a/docs/book/src/getting-started/testdata/project/hack/boilerplate.go.txt
+++ b/docs/book/src/getting-started/testdata/project/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2026 The Kubernetes authors.
+Copyright YEAR The Kubernetes authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/docs/book/src/multiversion-tutorial/testdata/project/Makefile b/docs/book/src/multiversion-tutorial/testdata/project/Makefile
index ba1ccce7417..12ac6f55fb0 100644
--- a/docs/book/src/multiversion-tutorial/testdata/project/Makefile
+++ b/docs/book/src/multiversion-tutorial/testdata/project/Makefile
@@ -1,5 +1,7 @@
 # Image URL to use all building/pushing image targets
 IMG ?= controller:latest
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -51,7 +53,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
-	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/docs/book/src/multiversion-tutorial/testdata/project/hack/boilerplate.go.txt b/docs/book/src/multiversion-tutorial/testdata/project/hack/boilerplate.go.txt
index 7917ef17315..6bd3b726196 100644
--- a/docs/book/src/multiversion-tutorial/testdata/project/hack/boilerplate.go.txt
+++ b/docs/book/src/multiversion-tutorial/testdata/project/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2026 The Kubernetes authors.
+Copyright YEAR The Kubernetes authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/pkg/machinery/scaffold_test.go b/pkg/machinery/scaffold_test.go
index 84f5c42ca6f..f7b7be2d6f2 100644
--- a/pkg/machinery/scaffold_test.go
+++ b/pkg/machinery/scaffold_test.go
@@ -17,6 +17,8 @@ package machinery
 import (
 	"errors"
 	"os"
+	"strconv"
+	"time"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -106,6 +108,19 @@ var _ = Describe("Scaffold", func() {
 		})
 	})
 
+	Describe("SubstituteYear", func() {
+		It("should replace YEAR with the current UTC year", func() {
+			currentYear := strconv.Itoa(time.Now().UTC().Year())
+			Expect(SubstituteYear("Copyright YEAR The Authors.")).
+				To(Equal("Copyright " + currentYear + " The Authors."))
+		})
+
+		It("should leave strings without YEAR unchanged", func() {
+			Expect(SubstituteYear("Copyright 2024 The Authors.")).
+				To(Equal("Copyright 2024 The Authors."))
+		})
+	})
+
 	Describe("Scaffold.Execute", func() {
 		const (
 			path     = "filename"
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate_test.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate_test.go
new file mode 100644
index 00000000000..b1de02f70e3
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate_test.go
@@ -0,0 +1,38 @@
+/*
+Copyright 2026 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package hack_test
+
+import (
+	"strings"
+	"testing"
+
+	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
+)
+
+func TestBoilerplateTemplateContainsYEAR(t *testing.T) {
+	bp := &hack.Boilerplate{
+		Owner:   "The Kubernetes Authors",
+		License: "apache2",
+	}
+	if err := bp.SetTemplateDefaults(); err != nil {
+		t.Fatalf("SetTemplateDefaults() error: %v", err)
+	}
+	body := bp.GetBody()
+	if !strings.Contains(body, "YEAR") {
+		t.Errorf("boilerplate template body must contain literal YEAR token; got:\n%s", body)
+	}
+}
diff --git a/testdata/project-v4-multigroup/Makefile b/testdata/project-v4-multigroup/Makefile
index bd456b59866..5cad129ef16 100644
--- a/testdata/project-v4-multigroup/Makefile
+++ b/testdata/project-v4-multigroup/Makefile
@@ -1,5 +1,7 @@
 # Image URL to use all building/pushing image targets
 IMG ?= controller:latest
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -47,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
-	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/testdata/project-v4-multigroup/hack/boilerplate.go.txt b/testdata/project-v4-multigroup/hack/boilerplate.go.txt
index 7917ef17315..6bd3b726196 100644
--- a/testdata/project-v4-multigroup/hack/boilerplate.go.txt
+++ b/testdata/project-v4-multigroup/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2026 The Kubernetes authors.
+Copyright YEAR The Kubernetes authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/testdata/project-v4-with-plugins/Makefile b/testdata/project-v4-with-plugins/Makefile
index ab2fefe097c..aa35ef90845 100644
--- a/testdata/project-v4-with-plugins/Makefile
+++ b/testdata/project-v4-with-plugins/Makefile
@@ -1,5 +1,7 @@
 # Image URL to use all building/pushing image targets
 IMG ?= controller:latest
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -47,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
-	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/testdata/project-v4-with-plugins/hack/boilerplate.go.txt b/testdata/project-v4-with-plugins/hack/boilerplate.go.txt
index 7917ef17315..6bd3b726196 100644
--- a/testdata/project-v4-with-plugins/hack/boilerplate.go.txt
+++ b/testdata/project-v4-with-plugins/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2026 The Kubernetes authors.
+Copyright YEAR The Kubernetes authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/testdata/project-v4/Makefile b/testdata/project-v4/Makefile
index a365c8abef2..1e61bb5562d 100644
--- a/testdata/project-v4/Makefile
+++ b/testdata/project-v4/Makefile
@@ -1,5 +1,7 @@
 # Image URL to use all building/pushing image targets
 IMG ?= controller:latest
+# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
+YEAR ?= $(shell date +%Y)
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 ifeq (,$(shell go env GOBIN))
@@ -47,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
 
 .PHONY: generate
 generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
-	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."
+	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/testdata/project-v4/hack/boilerplate.go.txt b/testdata/project-v4/hack/boilerplate.go.txt
index 7917ef17315..6bd3b726196 100644
--- a/testdata/project-v4/hack/boilerplate.go.txt
+++ b/testdata/project-v4/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2026 The Kubernetes authors.
+Copyright YEAR The Kubernetes authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.