Skip to content

Commit aed12ba

Browse files
authored
feat(internal/librarian): derive copyright year from source files (#3367)
Instead of hardcoding the copyright year in librarian.yaml, determine it by inspecting a language-specific file that is guaranteed to exist prior to cleanup. This ensures the year stays accurate without requiring manual updates.
1 parent 38e19e3 commit aed12ba

File tree

12 files changed

+238
-115
lines changed

12 files changed

+238
-115
lines changed

devtools/cmd/migrate-sidekick/main.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,6 @@ func buildGAPIC(files []string, repoPath string) (map[string]*config.Library, er
351351
lib.SkipPublish = true
352352
}
353353

354-
// Parse library-level configuration
355-
if copyrightYear, ok := sidekick.Codec["copyright-year"]; ok && copyrightYear != "" {
356-
lib.CopyrightYear = copyrightYear
357-
}
358-
359354
if extraModules, ok := sidekick.Codec["extra-modules"]; ok {
360355
for _, module := range strToSlice(extraModules, false) {
361356
if module == "" {
@@ -530,11 +525,10 @@ func buildVeneer(files []string) (map[string]*config.Library, error) {
530525
}
531526
name := cargo.Package.Name
532527
veneers[name] = &config.Library{
533-
Name: name,
534-
Veneer: true,
535-
Output: dir,
536-
Version: cargo.Package.Version,
537-
CopyrightYear: "2025",
528+
Name: name,
529+
Veneer: true,
530+
Output: dir,
531+
Version: cargo.Package.Version,
538532
}
539533
if len(rustModules) > 0 {
540534
veneers[name].Rust = &config.RustCrate{
@@ -672,11 +666,10 @@ func buildConfig(libraries map[string]*config.Library, defaults *config.Config)
672666
expectedName := deriveLibraryName(apiPath)
673667
nameMatchesConvention := lib.Name == expectedName
674668
// Check if library has extra configuration beyond just name/api/version
675-
hasExtraConfig := lib.CopyrightYear != "" ||
676-
(lib.Rust != nil && (lib.Rust.PerServiceFeatures || len(lib.Rust.DisabledRustdocWarnings) > 0 ||
677-
lib.Rust.GenerateSetterSamples != "" || lib.Rust.GenerateRpcSamples ||
678-
len(lib.Rust.PackageDependencies) > 0 || len(lib.Rust.PaginationOverrides) > 0 ||
679-
lib.Rust.NameOverrides != ""))
669+
hasExtraConfig := (lib.Rust != nil && (lib.Rust.PerServiceFeatures || len(lib.Rust.DisabledRustdocWarnings) > 0 ||
670+
lib.Rust.GenerateSetterSamples != "" || lib.Rust.GenerateRpcSamples ||
671+
len(lib.Rust.PackageDependencies) > 0 || len(lib.Rust.PaginationOverrides) > 0 ||
672+
lib.Rust.NameOverrides != ""))
680673
// Only include in libraries section if specific data needs to be retained
681674
if !nameMatchesConvention || hasExtraConfig || len(lib.Channels) > 1 {
682675
libCopy := *lib

devtools/cmd/migrate-sidekick/main_test.go

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,7 @@ func TestBuildGAPIC(t *testing.T) {
177177
ServiceConfig: "google/cloud/security/publicca/v1/publicca_v1.yaml",
178178
},
179179
},
180-
Version: "1.1.0",
181-
CopyrightYear: "2025",
180+
Version: "1.1.0",
182181
Keep: []string{
183182
"src/errors.rs",
184183
"src/operation.rs",
@@ -222,7 +221,6 @@ func TestBuildGAPIC(t *testing.T) {
222221
},
223222
SkipPublish: true,
224223
Version: "1.2.0",
225-
CopyrightYear: "2025",
226224
SpecificationFormat: "openapi",
227225
Output: "testdata/read-sidekick-files/success-read",
228226
Rust: &config.RustCrate{
@@ -441,11 +439,10 @@ func TestBuildVeneer(t *testing.T) {
441439
},
442440
want: map[string]*config.Library{
443441
"google-cloud-storage": {
444-
Name: "google-cloud-storage",
445-
Veneer: true,
446-
Output: "testdata/build-veneer/success/lib-1",
447-
Version: "1.5.0",
448-
CopyrightYear: "2025",
442+
Name: "google-cloud-storage",
443+
Veneer: true,
444+
Output: "testdata/build-veneer/success/lib-1",
445+
Version: "1.5.0",
449446
Rust: &config.RustCrate{
450447
Modules: []*config.RustModule{
451448
{
@@ -484,12 +481,11 @@ func TestBuildVeneer(t *testing.T) {
484481
},
485482
},
486483
"google-cloud-spanner": {
487-
Name: "google-cloud-spanner",
488-
Veneer: true,
489-
Output: "testdata/build-veneer/success/lib-2",
490-
Version: "0.0.0",
491-
CopyrightYear: "2025",
492-
SkipGenerate: true,
484+
Name: "google-cloud-spanner",
485+
Veneer: true,
486+
Output: "testdata/build-veneer/success/lib-2",
487+
Version: "0.0.0",
488+
SkipGenerate: true,
493489
},
494490
},
495491
},
@@ -500,11 +496,10 @@ func TestBuildVeneer(t *testing.T) {
500496
},
501497
want: map[string]*config.Library{
502498
"google-cloud-storage-overridden": {
503-
Name: "google-cloud-storage-overridden",
504-
Veneer: true,
505-
Output: "testdata/build-veneer/with-overrides/lib-1",
506-
Version: "1.5.0",
507-
CopyrightYear: "2025",
499+
Name: "google-cloud-storage-overridden",
500+
Veneer: true,
501+
Output: "testdata/build-veneer/with-overrides/lib-1",
502+
Version: "1.5.0",
508503
Rust: &config.RustCrate{
509504
Modules: []*config.RustModule{
510505
{
@@ -542,12 +537,11 @@ func TestBuildVeneer(t *testing.T) {
542537
},
543538
want: map[string]*config.Library{
544539
"google-cloud-spanner": {
545-
Name: "google-cloud-spanner",
546-
Veneer: true,
547-
Output: "testdata/build-veneer/success/lib-2",
548-
Version: "0.0.0",
549-
CopyrightYear: "2025",
550-
SkipGenerate: true,
540+
Name: "google-cloud-spanner",
541+
Veneer: true,
542+
Output: "testdata/build-veneer/success/lib-2",
543+
Version: "0.0.0",
544+
SkipGenerate: true,
551545
},
552546
},
553547
},
@@ -559,11 +553,10 @@ func TestBuildVeneer(t *testing.T) {
559553
},
560554
want: map[string]*config.Library{
561555
"common": {
562-
Name: "common",
563-
Veneer: true,
564-
Output: "testdata/build-veneer/wkt/tests/common",
565-
Version: "0.0.0",
566-
CopyrightYear: "2025",
556+
Name: "common",
557+
Veneer: true,
558+
Output: "testdata/build-veneer/wkt/tests/common",
559+
Version: "0.0.0",
567560
Rust: &config.RustCrate{
568561
Modules: []*config.RustModule{
569562
{
@@ -580,11 +573,10 @@ func TestBuildVeneer(t *testing.T) {
580573
},
581574
},
582575
"google-cloud-wkt": {
583-
Name: "google-cloud-wkt",
584-
Veneer: true,
585-
Output: "testdata/build-veneer/wkt",
586-
Version: "1.2.0",
587-
CopyrightYear: "2025",
576+
Name: "google-cloud-wkt",
577+
Veneer: true,
578+
Output: "testdata/build-veneer/wkt",
579+
Version: "1.2.0",
588580
Rust: &config.RustCrate{
589581
Modules: []*config.RustModule{
590582
{
@@ -608,11 +600,10 @@ func TestBuildVeneer(t *testing.T) {
608600
},
609601
want: map[string]*config.Library{
610602
"google-cloud-storage": {
611-
Name: "google-cloud-storage",
612-
Veneer: true,
613-
Output: "testdata/build-veneer/success/lib-1",
614-
Version: "1.5.0",
615-
CopyrightYear: "2025",
603+
Name: "google-cloud-storage",
604+
Veneer: true,
605+
Output: "testdata/build-veneer/success/lib-1",
606+
Version: "1.5.0",
616607
Rust: &config.RustCrate{
617608
Modules: []*config.RustModule{
618609
{
@@ -712,8 +703,7 @@ func TestBuildConfig(t *testing.T) {
712703
ServiceConfig: "google/cloud/security/publicca/v1/publicca_v1.yaml",
713704
},
714705
},
715-
Version: "1.1.0",
716-
CopyrightYear: "2025",
706+
Version: "1.1.0",
717707
Rust: &config.RustCrate{
718708
RustDefault: config.RustDefault{
719709
DisabledRustdocWarnings: []string{"bare_urls", "broken_intra_doc_links", "redundant_explicit_links"},
@@ -746,8 +736,7 @@ func TestBuildConfig(t *testing.T) {
746736
ServiceConfig: "google/cloud/security/publicca/v1/publicca_v1.yaml",
747737
},
748738
},
749-
Version: "1.1.0",
750-
CopyrightYear: "2025",
739+
Version: "1.1.0",
751740
Rust: &config.RustCrate{
752741
RustDefault: config.RustDefault{
753742
DisabledRustdocWarnings: []string{"bare_urls", "broken_intra_doc_links", "redundant_explicit_links"},
@@ -772,8 +761,7 @@ func TestBuildConfig(t *testing.T) {
772761
Path: "google/cloud/orgpolicy/v1",
773762
},
774763
},
775-
Version: "1.1.0",
776-
CopyrightYear: "2025",
764+
Version: "1.1.0",
777765
Rust: &config.RustCrate{
778766
RustDefault: config.RustDefault{
779767
DisabledRustdocWarnings: []string{"bare_urls", "broken_intra_doc_links", "redundant_explicit_links"},
@@ -795,8 +783,7 @@ func TestBuildConfig(t *testing.T) {
795783
ServiceConfig: "",
796784
},
797785
},
798-
Version: "1.1.0",
799-
CopyrightYear: "2025",
786+
Version: "1.1.0",
800787
Rust: &config.RustCrate{
801788
RustDefault: config.RustDefault{
802789
DisabledRustdocWarnings: []string{"bare_urls", "broken_intra_doc_links", "redundant_explicit_links"},

internal/config/config.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,6 @@ type Library struct {
158158
// libraries).
159159
Channels []*Channel `yaml:"channels,omitempty"`
160160

161-
// CopyrightYear is the copyright year for the library.
162-
CopyrightYear string `yaml:"copyright_year,omitempty"`
163-
164161
// DescriptionOverride overrides the library description.
165162
DescriptionOverride string `yaml:"description_override,omitempty"`
166163

internal/librarian/copyright.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librarian
16+
17+
import (
18+
"bufio"
19+
"errors"
20+
"fmt"
21+
"io/fs"
22+
"os"
23+
"path/filepath"
24+
"regexp"
25+
"time"
26+
)
27+
28+
var copyrightYearRegex = regexp.MustCompile(`^(?://|#)\s*Copyright\s+(\d{4})\s+Google\s+LLC`)
29+
30+
// extractCopyrightYear reads the copyright year from language-specific files.
31+
// It returns the current year if the file does not exist or if no copyright
32+
// year is found.
33+
func extractCopyrightYear(dir, language string) (string, error) {
34+
var filePath string
35+
switch language {
36+
case languageRust:
37+
filePath = filepath.Join(dir, "Cargo.toml")
38+
case languagePython:
39+
filePath = filepath.Join(dir, "setup.py")
40+
default:
41+
return "", nil
42+
}
43+
44+
f, err := os.Open(filePath)
45+
if err != nil {
46+
if errors.Is(err, fs.ErrNotExist) {
47+
return fmt.Sprintf("%d", time.Now().Year()), nil
48+
}
49+
return "", err
50+
}
51+
defer f.Close()
52+
53+
scanner := bufio.NewScanner(f)
54+
for scanner.Scan() {
55+
line := scanner.Text()
56+
if matches := copyrightYearRegex.FindStringSubmatch(line); matches != nil {
57+
return matches[1], nil
58+
}
59+
}
60+
if err := scanner.Err(); err != nil {
61+
return "", err
62+
}
63+
return fmt.Sprintf("%d", time.Now().Year()), nil
64+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librarian
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"testing"
22+
"time"
23+
)
24+
25+
func TestExtractCopyrightYear(t *testing.T) {
26+
currentYear := fmt.Sprintf("%d", time.Now().Year())
27+
28+
for _, test := range []struct {
29+
name string
30+
filename string
31+
language string
32+
content string
33+
wantYear string
34+
}{
35+
{
36+
name: "rust file with copyright",
37+
filename: "Cargo.toml",
38+
language: languageRust,
39+
content: "# Copyright 2024 Google LLC\n[package]\nname = \"test\"\n",
40+
wantYear: "2024",
41+
},
42+
{
43+
name: "python file with copyright",
44+
filename: "setup.py",
45+
language: languagePython,
46+
content: "# -*- coding: utf-8 -*-\n# Copyright 2025 Google LLC\n\nimport setuptools\n",
47+
wantYear: "2025",
48+
},
49+
{
50+
name: "rust file does not exist",
51+
filename: "Cargo.toml",
52+
language: languageRust,
53+
wantYear: currentYear,
54+
},
55+
{
56+
name: "rust file without copyright line",
57+
filename: "Cargo.toml",
58+
language: languageRust,
59+
content: "[package]\nname = \"test\"\nversion = \"0.1.0\"\n",
60+
wantYear: currentYear,
61+
},
62+
{
63+
name: "python file without copyright line",
64+
filename: "setup.py",
65+
language: languagePython,
66+
content: "# -*- coding: utf-8 -*-\n\nimport setuptools\n",
67+
wantYear: currentYear,
68+
},
69+
} {
70+
t.Run(test.name, func(t *testing.T) {
71+
dir := t.TempDir()
72+
if err := os.WriteFile(filepath.Join(dir, test.filename), []byte(test.content), 0644); err != nil {
73+
t.Fatal(err)
74+
}
75+
got, err := extractCopyrightYear(dir, test.language)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
if got != test.wantYear {
80+
t.Errorf("got year %q, want %q", got, test.wantYear)
81+
}
82+
})
83+
}
84+
}

internal/librarian/create.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ import (
2020
"fmt"
2121
"path"
2222
"sort"
23-
"strconv"
2423
"strings"
25-
"time"
2624

2725
"github.com/googleapis/librarian/internal/config"
2826
"github.com/googleapis/librarian/internal/librarian/rust"
@@ -144,7 +142,6 @@ func addLibraryToLibrarianConfig(cfg *config.Config, name, output, specification
144142
Name: name,
145143
Output: output,
146144
SpecificationFormat: specificationFormat,
147-
CopyrightYear: strconv.Itoa(time.Now().Year()),
148145
Version: "0.1.0",
149146
}
150147
if serviceConfig != "" || specificationSource != "" {

0 commit comments

Comments
 (0)