Skip to content

Commit 637f1ad

Browse files
committed
feat: Require components to opt out of auto release calculation
1 parent 3277d53 commit 637f1ad

File tree

11 files changed

+226
-66
lines changed

11 files changed

+226
-66
lines changed

docs/user/reference/config/components.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A component definition tells azldev where to find the spec file, how to customiz
99
| Field | TOML Key | Type | Required | Description |
1010
|-------|----------|------|----------|-------------|
1111
| Spec source | `spec` | [SpecSource](#spec-source) | No | Where to find the spec file for this component. Inherited from distro defaults if not specified. |
12+
| Release calculation | `release-calculation` | string | No | Controls how the Release tag is managed during rendering. `"auto"` (default) = auto-bump; `"manual"` = skip all automatic Release manipulation |
1213
| Overlays | `overlays` | array of [Overlay](overlays.md) | No | Modifications to apply to the spec and/or source files |
1314
| Build config | `build` | [BuildConfig](#build-configuration) | No | Build-time options (macros, conditionals, check config) |
1415
| Source files | `source-files` | array of [SourceFileReference](#source-file-references) | No | Additional source files to download for this component |

internal/app/azldev/core/components/resolver.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ func (r *Resolver) createComponentFromConfig(componentConfig *projectconfig.Comp
463463
componentConfig.Name, err)
464464
}
465465

466+
if componentConfig.ReleaseCalculation == "" {
467+
componentConfig.ReleaseCalculation = projectconfig.ReleaseCalculationAuto
468+
}
469+
466470
return &resolvedComponent{
467471
env: r.env,
468472
config: *componentConfig,

internal/app/azldev/core/components/resolver_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,18 @@ func setupTestSpec(t *testing.T, testEnv *testutils.TestEnv, path string) projec
3636
SourceType: projectconfig.SpecSourceTypeLocal,
3737
Path: path,
3838
},
39+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
3940
}
4041
}
4142

4243
// Constructs a test component's config, adds it to the test environment, and returns a copy of it.
4344
func addTestComponentToConfig(t *testing.T, env *testutils.TestEnv) projectconfig.ComponentConfig {
4445
t.Helper()
4546

46-
component := projectconfig.ComponentConfig{Name: "test-component"}
47+
component := projectconfig.ComponentConfig{
48+
Name: "test-component",
49+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
50+
}
4751

4852
env.Config.Components[component.Name] = component
4953

@@ -242,6 +246,7 @@ func TestFindAllComponents_MergesComponentPresentBySpecAndConfig(t *testing.T) {
242246
SourceType: projectconfig.SpecSourceTypeLocal,
243247
Path: testSpecPath,
244248
},
249+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
245250
}
246251

247252
// Find!

internal/app/azldev/core/sources/release.go

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -84,42 +84,33 @@ func BumpStaticRelease(releaseValue string, commitCount int) (string, error) {
8484
return fmt.Sprintf("%d%s", newRelease, suffix), nil
8585
}
8686

87-
// HasUserReleaseOverlay reports whether the given overlay list contains an overlay
88-
// that explicitly sets or updates the Release tag. This is used to determine whether
89-
// a user has configured the component to handle a non-standard Release value
90-
// (e.g. one using a custom macro like %{pkg_release}).
91-
func HasUserReleaseOverlay(overlays []projectconfig.ComponentOverlay) bool {
92-
for _, overlay := range overlays {
93-
if !strings.EqualFold(overlay.Tag, "Release") {
94-
continue
95-
}
96-
97-
if overlay.Type == projectconfig.ComponentOverlaySetSpecTag ||
98-
overlay.Type == projectconfig.ComponentOverlayUpdateSpecTag {
99-
return true
100-
}
101-
}
102-
103-
return false
104-
}
105-
10687
// tryBumpStaticRelease checks whether the component's spec uses %autorelease.
10788
// If not, it bumps the static Release tag by commitCount and applies the change
10889
// as an overlay to the spec file in-place. This ensures that components with static
10990
// release numbers get deterministic version bumps matching the number of synthetic
11091
// commits applied from the project repository.
11192
//
93+
// When the component's release calculation is "manual", this function is a no-op.
94+
//
11295
// When the spec uses %autorelease, this function is a no-op because rpmautospec
11396
// already resolves the release number from git history.
11497
//
11598
// When the Release tag uses a non-standard value (not %autorelease and not a leading
116-
// integer, e.g. %{pkg_release}), the component must define an explicit overlay that
117-
// sets the Release tag. If no such overlay exists, an error is returned.
99+
// integer, e.g. %{pkg_release}), the component must set release-calculation to
100+
// "manual", and likely define an explicit overlay that sets the Release tag.
101+
// If release-calculation is not set, an error is returned.
118102
func (p *sourcePreparerImpl) tryBumpStaticRelease(
119103
component components.Component,
120104
sourcesDirPath string,
121105
commitCount int,
122106
) error {
107+
if component.GetConfig().ReleaseCalculation == projectconfig.ReleaseCalculationManual {
108+
slog.Debug("Component uses manual release calculation; skipping static release bump",
109+
"component", component.GetName())
110+
111+
return nil
112+
}
113+
123114
specPath, err := p.resolveSpecPath(component, sourcesDirPath)
124115
if err != nil {
125116
return err
@@ -138,21 +129,14 @@ func (p *sourcePreparerImpl) tryBumpStaticRelease(
138129
return nil
139130
}
140131

141-
// Skip static release bump if the user has defined an explicit overlay for the Release tag.
142-
if HasUserReleaseOverlay(component.GetConfig().Overlays) {
143-
slog.Debug("Component has an explicit Release overlay; skipping static release bump",
144-
"component", component.GetName())
145-
146-
return nil
147-
}
148-
149132
newRelease, err := BumpStaticRelease(releaseValue, commitCount)
150133
if err != nil {
151134
// The Release tag does not start with an integer (e.g. %{pkg_release})
152-
// and the user did not provide an explicit overlay to set it.
135+
// and the user did not set release-calculation to "manual".
153136
return fmt.Errorf(
154137
"component %#q has a non-standard Release tag value %#q that cannot be auto-bumped; "+
155-
"add a \"spec-set-tag\" overlay for the Release tag in the component configuration:\n%w",
138+
"set 'release-calculation = \"manual\"' in the component configuration "+
139+
"and add a \"spec-set-tag\" overlay for the Release tag if needed:\n%w",
156140
component.GetName(), releaseValue, err)
157141
}
158142

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package sources
5+
6+
import (
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components/components_testutils"
11+
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
12+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
13+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
14+
"github.com/spf13/afero"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
"go.uber.org/mock/gomock"
18+
)
19+
20+
const testSourcesDir = "/sources"
21+
22+
func newTestPreparer(memFS afero.Fs) *sourcePreparerImpl {
23+
return &sourcePreparerImpl{
24+
fs: memFS,
25+
}
26+
}
27+
28+
func writeTestSpec(t *testing.T, memFS afero.Fs, name, release string) {
29+
t.Helper()
30+
31+
specDir := filepath.Join(testSourcesDir, name)
32+
require.NoError(t, fileutils.MkdirAll(memFS, specDir))
33+
34+
specPath := filepath.Join(specDir, name+".spec")
35+
content := []byte("Name: " + name + "\nVersion: 1.0.0\nRelease: " + release + "\nSummary: Test\nLicense: MIT\n")
36+
37+
require.NoError(t, fileutils.WriteFile(memFS, specPath, content, fileperms.PublicFile))
38+
}
39+
40+
func mockComponent(
41+
ctrl *gomock.Controller, name string, config *projectconfig.ComponentConfig,
42+
) *components_testutils.MockComponent {
43+
comp := components_testutils.NewMockComponent(ctrl)
44+
comp.EXPECT().GetName().AnyTimes().Return(name)
45+
comp.EXPECT().GetConfig().AnyTimes().Return(config)
46+
47+
return comp
48+
}
49+
50+
func TestTryBumpStaticRelease_ManualSkips(t *testing.T) {
51+
ctrl := gomock.NewController(t)
52+
memFS := afero.NewMemMapFs()
53+
preparer := newTestPreparer(memFS)
54+
55+
comp := mockComponent(ctrl, "kernel", &projectconfig.ComponentConfig{
56+
ReleaseCalculation: projectconfig.ReleaseCalculationManual,
57+
})
58+
59+
// No spec file needed — should skip before reading anything.
60+
err := preparer.tryBumpStaticRelease(comp, testSourcesDir, 3)
61+
require.NoError(t, err)
62+
}
63+
64+
func TestTryBumpStaticRelease_AutoreleaseSkips(t *testing.T) {
65+
ctrl := gomock.NewController(t)
66+
memFS := afero.NewMemMapFs()
67+
preparer := newTestPreparer(memFS)
68+
69+
writeTestSpec(t, memFS, "test-pkg", "%autorelease")
70+
71+
comp := mockComponent(ctrl, "test-pkg", &projectconfig.ComponentConfig{
72+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
73+
})
74+
75+
err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "test-pkg"), 3)
76+
require.NoError(t, err)
77+
}
78+
79+
func TestTryBumpStaticRelease_StaticBumps(t *testing.T) {
80+
ctrl := gomock.NewController(t)
81+
memFS := afero.NewMemMapFs()
82+
preparer := newTestPreparer(memFS)
83+
84+
writeTestSpec(t, memFS, "test-pkg", "1%{?dist}")
85+
86+
comp := mockComponent(ctrl, "test-pkg", &projectconfig.ComponentConfig{
87+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
88+
})
89+
90+
err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "test-pkg"), 3)
91+
require.NoError(t, err)
92+
93+
// Verify the spec was updated.
94+
specPath := filepath.Join(testSourcesDir, "test-pkg", "test-pkg.spec")
95+
content, err := fileutils.ReadFile(memFS, specPath)
96+
require.NoError(t, err)
97+
assert.Contains(t, string(content), "Release: 4%{?dist}")
98+
}
99+
100+
func TestTryBumpStaticRelease_NonStandardErrorsWithoutManual(t *testing.T) {
101+
ctrl := gomock.NewController(t)
102+
memFS := afero.NewMemMapFs()
103+
preparer := newTestPreparer(memFS)
104+
105+
writeTestSpec(t, memFS, "kernel", "%{pkg_release}")
106+
107+
comp := mockComponent(ctrl, "kernel", &projectconfig.ComponentConfig{
108+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
109+
})
110+
111+
err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "kernel"), 3)
112+
require.Error(t, err)
113+
assert.Contains(t, err.Error(), "cannot be auto-bumped")
114+
assert.Contains(t, err.Error(), "release-calculation")
115+
}
116+
117+
func TestTryBumpStaticRelease_NonStandardSucceedsWithManual(t *testing.T) {
118+
ctrl := gomock.NewController(t)
119+
memFS := afero.NewMemMapFs()
120+
preparer := newTestPreparer(memFS)
121+
122+
writeTestSpec(t, memFS, "kernel", "%{pkg_release}")
123+
124+
comp := mockComponent(ctrl, "kernel", &projectconfig.ComponentConfig{
125+
ReleaseCalculation: projectconfig.ReleaseCalculationManual,
126+
})
127+
128+
err := preparer.tryBumpStaticRelease(comp, filepath.Join(testSourcesDir, "kernel"), 3)
129+
require.NoError(t, err)
130+
}

internal/app/azldev/core/sources/release_test.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88

99
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
1010
"github.com/microsoft/azure-linux-dev-tools/internal/global/testctx"
11-
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
1211
"github.com/microsoft/azure-linux-dev-tools/internal/rpm/spec"
1312
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
1413
"github.com/stretchr/testify/assert"
@@ -96,36 +95,3 @@ func TestGetReleaseTagValue_FileNotFound(t *testing.T) {
9695
_, err := sources.GetReleaseTagValue(ctx.FS(), "/nonexistent.spec")
9796
require.Error(t, err)
9897
}
99-
100-
func TestHasUserReleaseOverlay(t *testing.T) {
101-
for _, testCase := range []struct {
102-
name string
103-
overlays []projectconfig.ComponentOverlay
104-
expected bool
105-
}{
106-
{"no overlays", nil, false},
107-
{"unrelated tag", []projectconfig.ComponentOverlay{
108-
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Version", Value: "1.0"},
109-
}, false},
110-
{"unsupported overlay type", []projectconfig.ComponentOverlay{
111-
{Type: projectconfig.ComponentOverlayAddSpecTag, Tag: "Release", Value: "1%{?dist}"},
112-
}, false},
113-
{"spec-set-tag", []projectconfig.ComponentOverlay{
114-
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Release", Value: "1%{?dist}"},
115-
}, true},
116-
{"spec-update-tag", []projectconfig.ComponentOverlay{
117-
{Type: projectconfig.ComponentOverlayUpdateSpecTag, Tag: "Release", Value: "2%{?dist}"},
118-
}, true},
119-
{"case insensitive", []projectconfig.ComponentOverlay{
120-
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "release", Value: "1%{?dist}"},
121-
}, true},
122-
{"mixed overlays", []projectconfig.ComponentOverlay{
123-
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "BuildRequires", Value: "gcc"},
124-
{Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Release", Value: "5%{?dist}"},
125-
}, true},
126-
} {
127-
t.Run(testCase.name, func(t *testing.T) {
128-
assert.Equal(t, testCase.expected, sources.HasUserReleaseOverlay(testCase.overlays))
129-
})
130-
}
131-
}

internal/projectconfig/component.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ func (g ComponentGroupConfig) WithAbsolutePaths(referenceDir string) ComponentGr
108108
return result
109109
}
110110

111+
// ReleaseCalculation controls how the Release tag is managed during rendering.
112+
type ReleaseCalculation string
113+
114+
const (
115+
// ReleaseCalculationAuto is the default. azldev auto-bumps the Release tag based on
116+
// synthetic commit history. Static integer releases are incremented; %autorelease
117+
// is handled by rpmautospec.
118+
ReleaseCalculationAuto ReleaseCalculation = "auto"
119+
120+
// ReleaseCalculationManual skips all automatic Release tag manipulation. Use this for
121+
// components that manage their own release numbering (e.g. kernel).
122+
ReleaseCalculationManual ReleaseCalculation = "manual"
123+
)
124+
111125
// Defines a component.
112126
type ComponentConfig struct {
113127
// The component's name; not actually present in serialized files.
@@ -125,6 +139,11 @@ type ComponentConfig struct {
125139
// Where to get its spec and adjacent files from.
126140
Spec SpecSource `toml:"spec,omitempty" json:"spec,omitempty" jsonschema:"title=Spec,description=Identifies where to find the spec for this component"`
127141

142+
// ReleaseCalculation controls how the Release tag is managed during rendering.
143+
// Defaults to auto (empty). Set to "manual" for components that manage their own
144+
// release numbering (e.g. kernel).
145+
ReleaseCalculation ReleaseCalculation `toml:"release-calculation,omitempty" json:"releaseCalculation,omitempty" validate:"omitempty,oneof=auto manual" jsonschema:"enum=auto,enum=manual,title=Release calculation,description=Controls how the Release tag is managed during rendering. Empty or omitted means auto."`
146+
128147
// Overlays to apply to sources after they've been acquired. May mutate the spec as well as sources.
129148
Overlays []ComponentOverlay `toml:"overlays,omitempty" json:"overlays,omitempty" table:"-" jsonschema:"title=Overlays,description=Overlays to apply to this component's spec and/or sources"`
130149

@@ -173,6 +192,7 @@ func (c *ComponentConfig) WithAbsolutePaths(referenceDir string) *ComponentConfi
173192
Name: c.Name,
174193
SourceConfigFile: c.SourceConfigFile,
175194
RenderedSpecDir: c.RenderedSpecDir,
195+
ReleaseCalculation: c.ReleaseCalculation,
176196
Spec: deep.MustCopy(c.Spec),
177197
Build: deep.MustCopy(c.Build),
178198
SourceFiles: deep.MustCopy(c.SourceFiles),

internal/projectconfig/component_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"testing"
1111

12+
"github.com/go-playground/validator/v10"
1213
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
1314
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
1415
"github.com/stretchr/testify/assert"
@@ -210,3 +211,25 @@ func TestAllowedSourceFilesHashTypes_MatchesJSONSchemaEnum(t *testing.T) {
210211
"'jsonschema' enum value %#q is not in 'AllowedSourceFilesHashTypes'", enumVal)
211212
}
212213
}
214+
215+
func TestReleaseCalculationValidation(t *testing.T) {
216+
validate := validator.New()
217+
218+
// Empty (omitted) is valid — resolved to "auto" by the component resolver.
219+
require.NoError(t, validate.Struct(&projectconfig.ComponentConfig{}))
220+
221+
// Explicit "auto" is valid.
222+
require.NoError(t, validate.Struct(&projectconfig.ComponentConfig{
223+
ReleaseCalculation: projectconfig.ReleaseCalculationAuto,
224+
}))
225+
226+
// Explicit "manual" is valid.
227+
require.NoError(t, validate.Struct(&projectconfig.ComponentConfig{
228+
ReleaseCalculation: projectconfig.ReleaseCalculationManual,
229+
}))
230+
231+
// Invalid value is rejected.
232+
require.Error(t, validate.Struct(&projectconfig.ComponentConfig{
233+
ReleaseCalculation: "manaul",
234+
}))
235+
}

scenario/__snapshots__/TestSnapshotsContainer_config_generate-schema_stdout_1.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@
106106
"title": "Spec",
107107
"description": "Identifies where to find the spec for this component"
108108
},
109+
"release-calculation": {
110+
"type": "string",
111+
"enum": [
112+
"auto",
113+
"manual"
114+
],
115+
"title": "Release calculation",
116+
"description": "Controls how the Release tag is managed during rendering. Empty or omitted means auto."
117+
},
109118
"overlays": {
110119
"items": {
111120
"$ref": "#/$defs/ComponentOverlay"

0 commit comments

Comments
 (0)