Skip to content

Commit 2e6da57

Browse files
MrAliaspellared
andauthored
Support custom version reference files (#1002)
* Parse module version ref from config * Use version refs in prerelease * Add a changelog entry * Test errors from updateVersionGoFile * Apply suggestions from code review Co-authored-by: Robert Pająk <[email protected]> * Add modules sect. to versions-example --------- Co-authored-by: Robert Pająk <[email protected]>
1 parent 14fb9d6 commit 2e6da57

File tree

8 files changed

+232
-42
lines changed

8 files changed

+232
-42
lines changed

.chloggen/version-refs.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
2+
change_type: enhancement
3+
4+
# The name of the component, or a single word describing the area of concern, (e.g. crosslink)
5+
component: multimod
6+
7+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
8+
note: Support custom version reference files
9+
10+
# One or more tracking issues related to the change
11+
issues: [994]
12+
13+
# (Optional) One or more lines of additional information to render under the primary note.
14+
# These lines will be padded with 2 spaces and then inserted directly into the document.
15+
# Use pipe (|) for multiline entries.
16+
subtext: |
17+
A new "modules" section is parsed in the version configuration. This section
18+
defines relative file paths for files that references the version of the
19+
module set. If this section is not used for a module the default "version.go"
20+
file at the root of the module directory is still assumed.

multimod/docs/versions-example.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,9 @@ module-sets:
5858
modules:
5959
- go.opentelemetry.io/otel/oteltest
6060
excluded-modules:
61-
- go.opentelemetry.io/otel/internal/tools
61+
- go.opentelemetry.io/otel/internal/tools
62+
modules:
63+
go.opentelemetry.io/otel:
64+
version-refs:
65+
- ./versions.go
66+
- ./internal/distro/distro.go

multimod/internal/prerelease/prerelease.go

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -149,54 +149,57 @@ func (p prerelease) checkModuleSetUpToDate(repo *git.Repository) (bool, error) {
149149
// updateAllVersionGo updates the version.go file containing a hardcoded semver version string
150150
// for modules within a set, if the file exists.
151151
func (p prerelease) updateAllVersionGo() error {
152+
var err error
152153
for _, modPath := range p.ModSetPaths() {
153154
modFilePath := p.ModPathMap[modPath]
155+
root := filepath.Dir(string(modFilePath))
154156

155-
versionGoDir := filepath.Dir(string(modFilePath))
156-
versionGoFilePath := filepath.Join(versionGoDir, "version.go")
157-
158-
// check if version.go file exists
159-
_, err := os.Stat(versionGoFilePath)
160-
if err != nil {
161-
if os.IsNotExist(err) {
162-
continue
163-
}
164-
return fmt.Errorf("could not check existence of %v: %w", versionGoFilePath, err)
165-
}
166-
if err = updateVersionGoFile(versionGoFilePath, p.ModSetVersion()); err != nil {
167-
return fmt.Errorf("could not update %v: %w", versionGoFilePath, err)
157+
vRefs := p.ModInfoMap[modPath].VersionRefs(root)
158+
if len(vRefs) == 0 {
159+
vRefs = defaultFileRefs(root)
168160
}
169161

162+
for _, vRef := range vRefs {
163+
e := updateVersionGoFile(vRef, p.ModSetVersion())
164+
err = errors.Join(err, e)
165+
}
170166
}
171-
return nil
167+
return err
172168
}
173169

174-
// updateVersionGoFile updates one version.go file.
175-
// TODO: a potential improvement is to use an AST package rather than regex to perform replacement.
176-
func updateVersionGoFile(filePath string, newVersion string) error {
177-
if !strings.HasSuffix(filePath, "version.go") {
178-
return errors.New("cannot update file passed that does not end with version.go")
179-
}
180-
log.Printf("... Updating file %v\n", filePath)
181-
182-
newVersionGoFile, err := os.ReadFile(filepath.Clean(filePath))
170+
func defaultFileRefs(root string) []string {
171+
path := filepath.Join(root, "version.go")
172+
_, err := os.Stat(path)
183173
if err != nil {
184-
panic(err)
174+
if !os.IsNotExist(err) {
175+
log.Printf("Warning: could not check existence of %v: %v\n", path, err)
176+
}
177+
// The file does not exist, or we cannot check its existence.
178+
return nil
185179
}
180+
return []string{path}
181+
}
186182

187-
oldVersionRegex := shared.SemverRegexNumberOnly
188-
r, err := regexp.Compile(oldVersionRegex)
183+
var verRegex = regexp.MustCompile(shared.SemverRegexNumberOnly)
184+
185+
// updateVersionGoFile updates all versions within the file at path to use the
186+
// new version number ver.
187+
func updateVersionGoFile(path string, ver string) error {
188+
// TODO: There is a potential improvement is to use an AST package rather than regex
189+
// to perform replacement.
190+
log.Printf("... Updating version references in %s to %s\n", path, ver)
191+
192+
data, err := os.ReadFile(filepath.Clean(path))
189193
if err != nil {
190-
return fmt.Errorf("error compiling regex: %w", err)
194+
return fmt.Errorf("error reading version.go file %v: %w", path, err)
191195
}
192196

193-
newVersionNumberOnly := strings.TrimPrefix(newVersion, "v")
194-
195-
newVersionGoFile = r.ReplaceAll(newVersionGoFile, []byte(newVersionNumberOnly))
197+
v := strings.TrimPrefix(ver, "v")
198+
data = verRegex.ReplaceAll(data, []byte(v))
196199

197-
// overwrite the version.go file
198-
if err := os.WriteFile(filePath, newVersionGoFile, 0600); err != nil {
199-
return fmt.Errorf("error overwriting go.mod file: %w", err)
200+
// Overwrite filePath.
201+
if err := os.WriteFile(path, data, 0600); err != nil {
202+
return fmt.Errorf("error overwriting %s file: %w", path, err)
200203
}
201204

202205
return nil

multimod/internal/prerelease/prerelease_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ func TestUpdateAllVersionGo(t *testing.T) {
198198
"func Version() string {\n\t" +
199199
"return \"1.0.0-OLD\"\n" +
200200
"}\n"),
201+
filepath.Join(tmpRootDir, "test", "test1", "internal", "version", "distro.go"): []byte("package version\"\n\n" +
202+
"// Version is the current release version of OpenTelemetry in use.\n" +
203+
"func Version() string {\n\t" +
204+
"return \"1.0.0-OLD\"\n" +
205+
"}\n"),
201206
filepath.Join(tmpRootDir, "test", "version.go"): []byte("package test2 // import \"go.opentelemetry.io/test/test2\"\n\n" +
202207
"// version is the current release version of OpenTelemetry in use.\n" +
203208
"func version() string {\n\t" +
@@ -219,6 +224,11 @@ func TestUpdateAllVersionGo(t *testing.T) {
219224
"func Version() string {\n\t" +
220225
"return \"1.2.3-RC1+meta\"\n" +
221226
"}\n"),
227+
filepath.Join(tmpRootDir, "test", "test1", "internal", "version", "distro.go"): []byte("package version\"\n\n" +
228+
"// Version is the current release version of OpenTelemetry in use.\n" +
229+
"func Version() string {\n\t" +
230+
"return \"1.2.3-RC1+meta\"\n" +
231+
"}\n"),
222232
filepath.Join(tmpRootDir, "test", "version.go"): []byte("package test2 // import \"go.opentelemetry.io/test/test2\"\n\n" +
223233
"// version is the current release version of OpenTelemetry in use.\n" +
224234
"func version() string {\n\t" +
@@ -235,6 +245,11 @@ func TestUpdateAllVersionGo(t *testing.T) {
235245
"func Version() string {\n\t" +
236246
"return \"1.0.0-OLD\"\n" +
237247
"}\n"),
248+
filepath.Join(tmpRootDir, "test", "test1", "internal", "version", "distro.go"): []byte("package version\"\n\n" +
249+
"// Version is the current release version of OpenTelemetry in use.\n" +
250+
"func Version() string {\n\t" +
251+
"return \"1.0.0-OLD\"\n" +
252+
"}\n"),
238253
filepath.Join(tmpRootDir, "test", "version.go"): []byte("package test2 // import \"go.opentelemetry.io/test/test2\"\n\n" +
239254
"// version is the current release version of OpenTelemetry in use.\n" +
240255
"func version() string {\n\t" +
@@ -251,6 +266,11 @@ func TestUpdateAllVersionGo(t *testing.T) {
251266
"func Version() string {\n\t" +
252267
"return \"1.0.0-OLD\"\n" +
253268
"}\n"),
269+
filepath.Join(tmpRootDir, "test", "test1", "internal", "version", "distro.go"): []byte("package version\"\n\n" +
270+
"// Version is the current release version of OpenTelemetry in use.\n" +
271+
"func Version() string {\n\t" +
272+
"return \"1.0.0-OLD\"\n" +
273+
"}\n"),
254274
filepath.Join(tmpRootDir, "test", "version.go"): []byte("package test2 // import \"go.opentelemetry.io/test/test2\"\n\n" +
255275
"// version is the current release version of OpenTelemetry in use.\n" +
256276
"func version() string {\n\t" +
@@ -612,3 +632,22 @@ func TestUpdateAll(t *testing.T) {
612632
)
613633
}
614634
}
635+
636+
func TestUpdateVersionGoFileErrs(t *testing.T) {
637+
t.Run("NotExist", func(t *testing.T) {
638+
path := filepath.Join(t.TempDir(), "version.go")
639+
err := updateVersionGoFile(path, "")
640+
assert.ErrorIs(t, err, os.ErrNotExist, "version.go does not exist")
641+
})
642+
t.Run("ReadOnly", func(t *testing.T) {
643+
path := filepath.Join(t.TempDir(), "version.go")
644+
645+
const readOnlyPerm = 0444
646+
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, readOnlyPerm) // nolint:gosec // Var filepath okay in test.
647+
require.NoError(t, err)
648+
require.NoError(t, file.Close())
649+
650+
err = updateVersionGoFile(path, "")
651+
assert.ErrorIs(t, err, os.ErrPermission, "write error not embedded")
652+
})
653+
}

multimod/internal/prerelease/test_data/update_all_version_go/versions_valid.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ module-sets:
2727
- go.opentelemetry.io/testroot/v2
2828
excluded-modules:
2929
- go.opentelemetry.io/test/testexcluded
30+
modules:
31+
go.opentelemetry.io/test/test1:
32+
version-refs:
33+
- ./version.go
34+
- ./internal/version/distro.go
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright The OpenTelemetry Authors
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+
# http://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+
module-sets:
16+
mod-set-1:
17+
version: v0.0.1
18+
modules:
19+
- go.opentelemetry.io/test/test1
20+
- go.opentelemetry.io/test/test2
21+
mod-set-2:
22+
version: v0.1.0
23+
modules:
24+
- go.opentelemetry.io/test3
25+
excluded-modules:
26+
- go.opentelemetry.io/excluded1
27+
modules:
28+
go.opentelemetry.io/test/test1:
29+
version-refs:
30+
- ./versions.go
31+
- ./internal/version/versions.go
32+
go.opentelemetry.io/test/test2:
33+
version-refs:
34+
- ./internal/deeper/version/versions.go
35+
go.opentelemetry.io/excluded1:
36+
version-refs:
37+
- ./sub/pkg/version/versions.go
38+
go.opentelemetry.io/unknown:
39+
version-refs:
40+
- ../unknown/versions.go

multimod/internal/shared/versions.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package shared
1616

1717
import (
1818
"fmt"
19+
"path/filepath"
1920

2021
"github.com/spf13/viper"
2122
)
@@ -29,8 +30,9 @@ const (
2930

3031
// versionConfig is needed to parse the versions.yaml file with viper.
3132
type versionConfig struct {
32-
ModuleSets ModuleSetMap `mapstructure:"module-sets"`
33-
ExcludedModules []ModulePath `mapstructure:"excluded-modules"`
33+
ModuleSets ModuleSetMap `mapstructure:"module-sets"`
34+
ExcludedModules []ModulePath `mapstructure:"excluded-modules"`
35+
Modules map[ModulePath]ModuleDef `mapstructure:"modules"`
3436
ignoreExcluded bool
3537
}
3638

@@ -56,6 +58,13 @@ type ModuleRef struct {
5658
Version string
5759
}
5860

61+
// ModuleDef are the definitions related to a module.
62+
type ModuleDef struct {
63+
// VersionRefs are the files that contain the module version and need to be
64+
// updated when the module version is updated.
65+
VersionRefs []string `mapstructure:"version-refs"`
66+
}
67+
5968
// ModuleInfoMap is a mapping from a module's import path to its ModuleInfo struct.
6069
type ModuleInfoMap map[ModulePath]ModuleInfo
6170

@@ -64,6 +73,21 @@ type ModuleInfoMap map[ModulePath]ModuleInfo
6473
type ModuleInfo struct {
6574
ModuleSetName string
6675
Version string
76+
77+
versionRefs []string
78+
}
79+
80+
// VersionRefs returns the path to all version references for the module. which
81+
// are the files that contain the module version and need to be updated when
82+
// the module version is updated.
83+
//
84+
// All paths are relative to the root path provided.
85+
func (mi ModuleInfo) VersionRefs(root string) []string {
86+
out := make([]string, len(mi.versionRefs))
87+
for i, ref := range mi.versionRefs {
88+
out[i] = filepath.Join(root, ref)
89+
}
90+
return out
6791
}
6892

6993
// ModuleFilePath holds the file path to the go.mod file within the repo,
@@ -80,22 +104,25 @@ type ModuleTagName string
80104
// readVersioningFile reads in a versioning file (typically given as versions.yaml) and returns
81105
// a versionConfig struct.
82106
func readVersioningFile(versioningFilename string) (versionConfig, error) {
83-
viper.SetConfigFile(versioningFilename)
107+
// Allow '.' in configuration keys.
108+
// (i.e go.opentelemetry.io/otel is a valid key).
109+
v := viper.NewWithOptions(viper.KeyDelimiter("\\"))
110+
v.SetConfigFile(versioningFilename)
84111

85112
var versionCfg versionConfig
86113

87-
if err := viper.ReadInConfig(); err != nil {
114+
if err := v.ReadInConfig(); err != nil {
88115
return versionConfig{}, fmt.Errorf("error reading versionsConfig file: %w", err)
89116
}
90117

91-
if err := viper.Unmarshal(&versionCfg); err != nil {
118+
if err := v.Unmarshal(&versionCfg); err != nil {
92119
return versionConfig{}, fmt.Errorf("unable to unmarshal versionsConfig: %w", err)
93120
}
94121

95-
if viper.ConfigFileUsed() != versioningFilename {
122+
if v.ConfigFileUsed() != versioningFilename {
96123
return versionConfig{}, fmt.Errorf(
97124
"config file used (%v) does not match input file (%v)",
98-
viper.ConfigFileUsed(),
125+
v.ConfigFileUsed(),
99126
versioningFilename,
100127
)
101128
}
@@ -125,7 +152,11 @@ func (versionCfg versionConfig) buildModuleMap() (ModuleInfoMap, error) {
125152
if versionCfg.shouldExcludeModule(modPath) {
126153
return nil, fmt.Errorf("module %v is an excluded module and should not be versioned", modPath)
127154
}
128-
modMap[modPath] = ModuleInfo{setName, moduleSet.Version}
155+
modMap[modPath] = ModuleInfo{
156+
setName,
157+
moduleSet.Version,
158+
versionCfg.Modules[modPath].VersionRefs,
159+
}
129160
}
130161
}
131162

multimod/internal/shared/versions_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,50 @@ func TestBuildModulePathMap(t *testing.T) {
352352
require.NoError(t, err)
353353
assert.Equal(t, expected, actual)
354354
}
355+
356+
func TestAltVersionFiles(t *testing.T) {
357+
vFile := filepath.Join(testDataDir, "alt_version_files/versions.yaml")
358+
actual, err := readVersioningFile(vFile)
359+
require.NoError(t, err)
360+
assert.Equal(t, versionConfig{
361+
ModuleSets: ModuleSetMap{
362+
"mod-set-1": ModuleSet{
363+
Version: "v0.0.1",
364+
Modules: []ModulePath{
365+
"go.opentelemetry.io/test/test1",
366+
"go.opentelemetry.io/test/test2",
367+
},
368+
},
369+
"mod-set-2": ModuleSet{
370+
Version: "v0.1.0",
371+
Modules: []ModulePath{
372+
"go.opentelemetry.io/test3",
373+
},
374+
},
375+
},
376+
ExcludedModules: []ModulePath{"go.opentelemetry.io/excluded1"},
377+
Modules: map[ModulePath]ModuleDef{
378+
"go.opentelemetry.io/excluded1": {
379+
VersionRefs: []string{
380+
"./sub/pkg/version/versions.go",
381+
},
382+
},
383+
"go.opentelemetry.io/test/test1": {
384+
VersionRefs: []string{
385+
"./versions.go",
386+
"./internal/version/versions.go",
387+
},
388+
},
389+
"go.opentelemetry.io/test/test2": {
390+
VersionRefs: []string{
391+
"./internal/deeper/version/versions.go",
392+
},
393+
},
394+
"go.opentelemetry.io/unknown": {
395+
VersionRefs: []string{
396+
"../unknown/versions.go",
397+
},
398+
},
399+
},
400+
}, actual)
401+
}

0 commit comments

Comments
 (0)