Skip to content

Commit 75131e9

Browse files
authored
feat: Static release bumped during dist-git generation (#54)
Allow azldev to automatically update the static release number for specs that do not use %autorelease. The new release number is calculated by taking the original release number and adding the number of synthetic commits (from the project repo). This behavior can only be observed when creating synthetic dist-git using the --with-git flag on either prepare-sources or build commands.
1 parent 9350edd commit 75131e9

File tree

11 files changed

+381
-27
lines changed

11 files changed

+381
-27
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
[project]
2+
default-author-email = "azurelinux@microsoft.com"
3+
14
[tools.imageCustomizer]
25
containerTag = "mcr.microsoft.com/azurelinux/imagecustomizer:1"

internal/app/azldev/cmds/component/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func BuildComponent(
241241

242242
var preparerOpts []sources.PreparerOption
243243
if options.WithGitRepo {
244-
preparerOpts = append(preparerOpts, sources.WithGitRepo())
244+
preparerOpts = append(preparerOpts, sources.WithGitRepo(env.Config().Project.DefaultAuthorEmail))
245245
}
246246

247247
sourcePreparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)

internal/app/azldev/cmds/component/preparesources.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package component
66
import (
77
"errors"
88
"fmt"
9+
"log/slog"
910

1011
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
1112
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
@@ -117,9 +118,14 @@ func PrepareComponentSources(env *azldev.Env, options *PrepareSourcesOptions) er
117118
return err
118119
}
119120

121+
if options.SkipOverlays && options.WithGitRepo {
122+
slog.Warn("--with-git has no effect when --skip-overlays is set; " +
123+
"synthetic history requires overlays to be applied")
124+
}
125+
120126
var preparerOpts []sources.PreparerOption
121127
if options.WithGitRepo {
122-
preparerOpts = append(preparerOpts, sources.WithGitRepo())
128+
preparerOpts = append(preparerOpts, sources.WithGitRepo(env.Config().Project.DefaultAuthorEmail))
123129
}
124130

125131
preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package sources
5+
6+
import (
7+
"fmt"
8+
"log/slog"
9+
"regexp"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
14+
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
15+
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
16+
"github.com/microsoft/azure-linux-dev-tools/internal/rpm/spec"
17+
)
18+
19+
// autoreleasePattern matches the %autorelease macro invocation in a Release tag value.
20+
// This covers both the bare form (%autorelease) and the braced form (%{autorelease}).
21+
var autoreleasePattern = regexp.MustCompile(`%(\{autorelease\}|autorelease($|\s))`)
22+
23+
// staticReleasePattern matches a leading integer in a static Release tag value,
24+
// followed by an optional suffix (e.g. "%{?dist}").
25+
var staticReleasePattern = regexp.MustCompile(`^(\d+)(.*)$`)
26+
27+
// GetReleaseTagValue reads the Release tag value from the spec file at specPath.
28+
// It returns the raw value string as written in the spec (e.g. "1%{?dist}" or "%autorelease").
29+
// Returns [spec.ErrNoSuchTag] if no Release tag is found.
30+
func GetReleaseTagValue(fs opctx.FS, specPath string) (string, error) {
31+
specFile, err := fs.Open(specPath)
32+
if err != nil {
33+
return "", fmt.Errorf("failed to open spec %#q:\n%w", specPath, err)
34+
}
35+
defer specFile.Close()
36+
37+
openedSpec, err := spec.OpenSpec(specFile)
38+
if err != nil {
39+
return "", fmt.Errorf("failed to parse spec %#q:\n%w", specPath, err)
40+
}
41+
42+
var releaseValue string
43+
44+
err = openedSpec.VisitTagsPackage("", func(tagLine *spec.TagLine, _ *spec.Context) error {
45+
if strings.EqualFold(tagLine.Tag, "Release") {
46+
releaseValue = tagLine.Value
47+
}
48+
49+
return nil
50+
})
51+
if err != nil {
52+
return "", fmt.Errorf("failed to visit tags in spec %#q:\n%w", specPath, err)
53+
}
54+
55+
if releaseValue == "" {
56+
return "", fmt.Errorf("release tag not found in spec %#q:\n%w", specPath, spec.ErrNoSuchTag)
57+
}
58+
59+
return releaseValue, nil
60+
}
61+
62+
// ReleaseUsesAutorelease reports whether the given Release tag value uses the
63+
// %autorelease macro (either bare or braced form).
64+
func ReleaseUsesAutorelease(releaseValue string) bool {
65+
return autoreleasePattern.MatchString(releaseValue)
66+
}
67+
68+
// BumpStaticRelease increments the leading integer in a static Release tag value
69+
// by the given commit count.
70+
func BumpStaticRelease(releaseValue string, commitCount int) (string, error) {
71+
matches := staticReleasePattern.FindStringSubmatch(releaseValue)
72+
if matches == nil {
73+
return "", fmt.Errorf("release value %#q does not start with an integer", releaseValue)
74+
}
75+
76+
currentRelease, err := strconv.Atoi(matches[1])
77+
if err != nil {
78+
return "", fmt.Errorf("failed to parse release number from %#q:\n%w", releaseValue, err)
79+
}
80+
81+
newRelease := currentRelease + commitCount
82+
suffix := matches[2]
83+
84+
return fmt.Sprintf("%d%s", newRelease, suffix), nil
85+
}
86+
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+
106+
// tryBumpStaticRelease checks whether the component's spec uses %autorelease.
107+
// If not, it bumps the static Release tag by commitCount and applies the change
108+
// as an overlay to the spec file in-place. This ensures that components with static
109+
// release numbers get deterministic version bumps matching the number of synthetic
110+
// commits applied from the project repository.
111+
//
112+
// When the spec uses %autorelease, this function is a no-op because rpmautospec
113+
// already resolves the release number from git history.
114+
//
115+
// 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.
118+
func (p *sourcePreparerImpl) tryBumpStaticRelease(
119+
component components.Component,
120+
sourcesDirPath string,
121+
commitCount int,
122+
) error {
123+
specPath, err := p.resolveSpecPath(component, sourcesDirPath)
124+
if err != nil {
125+
return err
126+
}
127+
128+
releaseValue, err := GetReleaseTagValue(p.fs, specPath)
129+
if err != nil {
130+
return fmt.Errorf("failed to read Release tag for component %#q:\n%w",
131+
component.GetName(), err)
132+
}
133+
134+
if ReleaseUsesAutorelease(releaseValue) {
135+
slog.Debug("Spec uses %%autorelease; skipping static release bump",
136+
"component", component.GetName())
137+
138+
return nil
139+
}
140+
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+
149+
newRelease, err := BumpStaticRelease(releaseValue, commitCount)
150+
if err != nil {
151+
// 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.
153+
return fmt.Errorf(
154+
"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",
156+
component.GetName(), releaseValue, err)
157+
}
158+
159+
slog.Info("Bumping static release",
160+
"component", component.GetName(),
161+
"oldRelease", releaseValue,
162+
"newRelease", newRelease,
163+
"commitCount", commitCount)
164+
165+
overlay := projectconfig.ComponentOverlay{
166+
Type: projectconfig.ComponentOverlayUpdateSpecTag,
167+
Tag: "Release",
168+
Value: newRelease,
169+
}
170+
171+
if err := ApplySpecOverlayToFileInPlace(p.fs, overlay, specPath); err != nil {
172+
return fmt.Errorf("failed to apply release bump overlay for component %#q:\n%w",
173+
component.GetName(), err)
174+
}
175+
176+
return nil
177+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package sources_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
10+
"github.com/microsoft/azure-linux-dev-tools/internal/global/testctx"
11+
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
12+
"github.com/microsoft/azure-linux-dev-tools/internal/rpm/spec"
13+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestReleaseUsesAutorelease(t *testing.T) {
19+
for _, testCase := range []struct {
20+
value string
21+
expected bool
22+
}{
23+
{"%autorelease", true},
24+
{"%{autorelease}", true},
25+
{"1", false},
26+
{"1%{?dist}", false},
27+
{"3%{?dist}.1", false},
28+
{"", false},
29+
} {
30+
t.Run(testCase.value, func(t *testing.T) {
31+
assert.Equal(t, testCase.expected, sources.ReleaseUsesAutorelease(testCase.value))
32+
})
33+
}
34+
}
35+
36+
func TestBumpStaticRelease(t *testing.T) {
37+
for _, testCase := range []struct {
38+
name, value string
39+
commits int
40+
expected string
41+
wantErr bool
42+
}{
43+
{"simple integer", "1", 3, "4", false},
44+
{"with dist tag", "1%{?dist}", 2, "3%{?dist}", false},
45+
{"larger base", "10%{?dist}", 5, "15%{?dist}", false},
46+
{"single commit", "1%{?dist}", 1, "2%{?dist}", false},
47+
{"no leading int", "%{?dist}", 1, "", true},
48+
{"empty string", "", 1, "", true},
49+
} {
50+
t.Run(testCase.name, func(t *testing.T) {
51+
result, err := sources.BumpStaticRelease(testCase.value, testCase.commits)
52+
if testCase.wantErr {
53+
require.Error(t, err)
54+
} else {
55+
require.NoError(t, err)
56+
assert.Equal(t, testCase.expected, result)
57+
}
58+
})
59+
}
60+
}
61+
62+
func TestGetReleaseTagValue(t *testing.T) {
63+
makeSpec := func(release string) string {
64+
return "Name: test-package\nVersion: 1.0.0\nRelease: " + release + "\nSummary: Test\n"
65+
}
66+
67+
for _, testCase := range []struct {
68+
name, specContent, expected string
69+
wantErr bool
70+
}{
71+
{"static with dist", makeSpec("1%{?dist}"), "1%{?dist}", false},
72+
{"autorelease", makeSpec("%autorelease"), "%autorelease", false},
73+
{"braced autorelease", makeSpec("%{autorelease}"), "%{autorelease}", false},
74+
{"no release tag", "Name: test-package\nVersion: 1.0.0\nSummary: Test\n", "", true},
75+
} {
76+
t.Run(testCase.name, func(t *testing.T) {
77+
ctx := testctx.NewCtx()
78+
specPath := "/test.spec"
79+
80+
err := fileutils.WriteFile(ctx.FS(), specPath, []byte(testCase.specContent), 0o644)
81+
require.NoError(t, err)
82+
83+
result, err := sources.GetReleaseTagValue(ctx.FS(), specPath)
84+
if testCase.wantErr {
85+
require.ErrorIs(t, err, spec.ErrNoSuchTag)
86+
} else {
87+
require.NoError(t, err)
88+
assert.Equal(t, testCase.expected, result)
89+
}
90+
})
91+
}
92+
}
93+
94+
func TestGetReleaseTagValue_FileNotFound(t *testing.T) {
95+
ctx := testctx.NewCtx()
96+
_, err := sources.GetReleaseTagValue(ctx.FS(), "/nonexistent.spec")
97+
require.Error(t, err)
98+
}
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+
}

0 commit comments

Comments
 (0)