-
Notifications
You must be signed in to change notification settings - Fork 8
feat: Static release bumped during dist-git generation #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package sources | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "log/slog" | ||
| "regexp" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/global/opctx" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/rpm/spec" | ||
| ) | ||
|
|
||
| // autoreleasePattern matches the %autorelease macro invocation in a Release tag value. | ||
| // This covers both the bare form (%autorelease) and the braced form (%{autorelease}). | ||
| var autoreleasePattern = regexp.MustCompile(`%(\{autorelease\}|autorelease($|\s))`) | ||
|
|
||
| // staticReleasePattern matches a leading integer in a static Release tag value, | ||
| // followed by an optional suffix (e.g. "%{?dist}"). | ||
| var staticReleasePattern = regexp.MustCompile(`^(\d+)(.*)$`) | ||
|
|
||
| // GetReleaseTagValue reads the Release tag value from the spec file at specPath. | ||
| // It returns the raw value string as written in the spec (e.g. "1%{?dist}" or "%autorelease"). | ||
| // Returns [spec.ErrNoSuchTag] if no Release tag is found. | ||
| func GetReleaseTagValue(fs opctx.FS, specPath string) (string, error) { | ||
| specFile, err := fs.Open(specPath) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to open spec %#q:\n%w", specPath, err) | ||
| } | ||
| defer specFile.Close() | ||
|
|
||
| openedSpec, err := spec.OpenSpec(specFile) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to parse spec %#q:\n%w", specPath, err) | ||
| } | ||
|
|
||
| var releaseValue string | ||
|
|
||
| err = openedSpec.VisitTagsPackage("", func(tagLine *spec.TagLine, _ *spec.Context) error { | ||
| if strings.EqualFold(tagLine.Tag, "Release") { | ||
| releaseValue = tagLine.Value | ||
| } | ||
|
|
||
| return nil | ||
| }) | ||
Tonisal-byte marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return "", fmt.Errorf("failed to visit tags in spec %#q:\n%w", specPath, err) | ||
| } | ||
|
|
||
| if releaseValue == "" { | ||
| return "", fmt.Errorf("release tag not found in spec %#q:\n%w", specPath, spec.ErrNoSuchTag) | ||
| } | ||
|
|
||
| return releaseValue, nil | ||
| } | ||
|
|
||
| // ReleaseUsesAutorelease reports whether the given Release tag value uses the | ||
| // %autorelease macro (either bare or braced form). | ||
| func ReleaseUsesAutorelease(releaseValue string) bool { | ||
| return autoreleasePattern.MatchString(releaseValue) | ||
| } | ||
|
|
||
| // BumpStaticRelease increments the leading integer in a static Release tag value | ||
| // by the given commit count. | ||
| func BumpStaticRelease(releaseValue string, commitCount int) (string, error) { | ||
| matches := staticReleasePattern.FindStringSubmatch(releaseValue) | ||
| if matches == nil { | ||
| return "", fmt.Errorf("release value %#q does not start with an integer", releaseValue) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We know we'll find specs that don't use autorelease and don't have a standard integer followed by %{?dist}. Example: https://src.fedoraproject.org/rpms/kernel/blob/rawhide/f/kernel.spec For these, we'll want some way to avoid this being a fatal error. From Dan's "3 categories", this would be that 3rd case. For those it would be okay for us to manually manage their Release values, but we'd need a way to do so. We can use an overlay to set the |
||
| } | ||
|
|
||
| currentRelease, err := strconv.Atoi(matches[1]) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to parse release number from %#q:\n%w", releaseValue, err) | ||
| } | ||
|
|
||
| newRelease := currentRelease + commitCount | ||
| suffix := matches[2] | ||
|
|
||
| return fmt.Sprintf("%d%s", newRelease, suffix), nil | ||
| } | ||
|
|
||
| // HasUserReleaseOverlay reports whether the given overlay list contains an overlay | ||
| // that explicitly sets or updates the Release tag. This is used to determine whether | ||
| // a user has configured the component to handle a non-standard Release value | ||
| // (e.g. one using a custom macro like %{pkg_release}). | ||
| func HasUserReleaseOverlay(overlays []projectconfig.ComponentOverlay) bool { | ||
| for _, overlay := range overlays { | ||
| if !strings.EqualFold(overlay.Tag, "Release") { | ||
| continue | ||
| } | ||
|
|
||
| if overlay.Type == projectconfig.ComponentOverlaySetSpecTag || | ||
| overlay.Type == projectconfig.ComponentOverlayUpdateSpecTag { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
Tonisal-byte marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // tryBumpStaticRelease checks whether the component's spec uses %autorelease. | ||
| // If not, it bumps the static Release tag by commitCount and applies the change | ||
| // as an overlay to the spec file in-place. This ensures that components with static | ||
| // release numbers get deterministic version bumps matching the number of synthetic | ||
| // commits applied from the project repository. | ||
| // | ||
| // When the spec uses %autorelease, this function is a no-op because rpmautospec | ||
| // already resolves the release number from git history. | ||
| // | ||
| // When the Release tag uses a non-standard value (not %autorelease and not a leading | ||
| // integer, e.g. %{pkg_release}), the component must define an explicit overlay that | ||
| // sets the Release tag. If no such overlay exists, an error is returned. | ||
| func (p *sourcePreparerImpl) tryBumpStaticRelease( | ||
| component components.Component, | ||
| sourcesDirPath string, | ||
| commitCount int, | ||
| ) error { | ||
| specPath, err := p.resolveSpecPath(component, sourcesDirPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| releaseValue, err := GetReleaseTagValue(p.fs, specPath) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to read Release tag for component %#q:\n%w", | ||
| component.GetName(), err) | ||
| } | ||
|
|
||
| if ReleaseUsesAutorelease(releaseValue) { | ||
| slog.Debug("Spec uses %%autorelease; skipping static release bump", | ||
| "component", component.GetName()) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Skip static release bump if the user has defined an explicit overlay for the Release tag. | ||
| if HasUserReleaseOverlay(component.GetConfig().Overlays) { | ||
| slog.Debug("Component has an explicit Release overlay; skipping static release bump", | ||
| "component", component.GetName()) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| newRelease, err := BumpStaticRelease(releaseValue, commitCount) | ||
| if err != nil { | ||
| // The Release tag does not start with an integer (e.g. %{pkg_release}) | ||
| // and the user did not provide an explicit overlay to set it. | ||
| return fmt.Errorf( | ||
| "component %#q has a non-standard Release tag value %#q that cannot be auto-bumped; "+ | ||
| "add a \"spec-set-tag\" overlay for the Release tag in the component configuration", | ||
Tonisal-byte marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| component.GetName(), releaseValue) | ||
| } | ||
|
|
||
| slog.Info("Bumping static release", | ||
| "component", component.GetName(), | ||
| "oldRelease", releaseValue, | ||
| "newRelease", newRelease, | ||
| "commitCount", commitCount) | ||
|
|
||
| overlay := projectconfig.ComponentOverlay{ | ||
| Type: projectconfig.ComponentOverlayUpdateSpecTag, | ||
| Tag: "Release", | ||
| Value: newRelease, | ||
| } | ||
|
|
||
| if err := ApplySpecOverlayToFileInPlace(p.fs, overlay, specPath); err != nil { | ||
| return fmt.Errorf("failed to apply release bump overlay for component %#q:\n%w", | ||
| component.GetName(), err) | ||
| } | ||
Tonisal-byte marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package sources_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/global/testctx" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/rpm/spec" | ||
| "github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestReleaseUsesAutorelease(t *testing.T) { | ||
| for _, testCase := range []struct { | ||
| value string | ||
| expected bool | ||
| }{ | ||
| {"%autorelease", true}, | ||
| {"%{autorelease}", true}, | ||
| {"1", false}, | ||
| {"1%{?dist}", false}, | ||
| {"3%{?dist}.1", false}, | ||
| {"", false}, | ||
| } { | ||
| t.Run(testCase.value, func(t *testing.T) { | ||
| assert.Equal(t, testCase.expected, sources.ReleaseUsesAutorelease(testCase.value)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestBumpStaticRelease(t *testing.T) { | ||
| for _, testCase := range []struct { | ||
| name, value string | ||
| commits int | ||
| expected string | ||
| wantErr bool | ||
| }{ | ||
| {"simple integer", "1", 3, "4", false}, | ||
| {"with dist tag", "1%{?dist}", 2, "3%{?dist}", false}, | ||
| {"larger base", "10%{?dist}", 5, "15%{?dist}", false}, | ||
| {"single commit", "1%{?dist}", 1, "2%{?dist}", false}, | ||
| {"no leading int", "%{?dist}", 1, "", true}, | ||
| {"empty string", "", 1, "", true}, | ||
| } { | ||
| t.Run(testCase.name, func(t *testing.T) { | ||
| result, err := sources.BumpStaticRelease(testCase.value, testCase.commits) | ||
| if testCase.wantErr { | ||
| require.Error(t, err) | ||
| } else { | ||
| require.NoError(t, err) | ||
| assert.Equal(t, testCase.expected, result) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestGetReleaseTagValue(t *testing.T) { | ||
| makeSpec := func(release string) string { | ||
| return "Name: test-package\nVersion: 1.0.0\nRelease: " + release + "\nSummary: Test\n" | ||
| } | ||
|
|
||
| for _, testCase := range []struct { | ||
| name, specContent, expected string | ||
| wantErr bool | ||
| }{ | ||
| {"static with dist", makeSpec("1%{?dist}"), "1%{?dist}", false}, | ||
| {"autorelease", makeSpec("%autorelease"), "%autorelease", false}, | ||
| {"braced autorelease", makeSpec("%{autorelease}"), "%{autorelease}", false}, | ||
| {"no release tag", "Name: test-package\nVersion: 1.0.0\nSummary: Test\n", "", true}, | ||
| } { | ||
| t.Run(testCase.name, func(t *testing.T) { | ||
| ctx := testctx.NewCtx() | ||
| specPath := "/test.spec" | ||
|
|
||
| err := fileutils.WriteFile(ctx.FS(), specPath, []byte(testCase.specContent), 0o644) | ||
| require.NoError(t, err) | ||
|
|
||
| result, err := sources.GetReleaseTagValue(ctx.FS(), specPath) | ||
| if testCase.wantErr { | ||
| require.ErrorIs(t, err, spec.ErrNoSuchTag) | ||
| } else { | ||
| require.NoError(t, err) | ||
| assert.Equal(t, testCase.expected, result) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestGetReleaseTagValue_FileNotFound(t *testing.T) { | ||
| ctx := testctx.NewCtx() | ||
| _, err := sources.GetReleaseTagValue(ctx.FS(), "/nonexistent.spec") | ||
| require.Error(t, err) | ||
| } | ||
|
|
||
| func TestHasUserReleaseOverlay(t *testing.T) { | ||
| for _, testCase := range []struct { | ||
| name string | ||
| overlays []projectconfig.ComponentOverlay | ||
| expected bool | ||
| }{ | ||
| {"no overlays", nil, false}, | ||
| {"unrelated tag", []projectconfig.ComponentOverlay{ | ||
| {Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Version", Value: "1.0"}, | ||
| }, false}, | ||
| {"unsupported overlay type", []projectconfig.ComponentOverlay{ | ||
| {Type: projectconfig.ComponentOverlayAddSpecTag, Tag: "Release", Value: "1%{?dist}"}, | ||
| }, false}, | ||
| {"spec-set-tag", []projectconfig.ComponentOverlay{ | ||
| {Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Release", Value: "1%{?dist}"}, | ||
| }, true}, | ||
| {"spec-update-tag", []projectconfig.ComponentOverlay{ | ||
| {Type: projectconfig.ComponentOverlayUpdateSpecTag, Tag: "Release", Value: "2%{?dist}"}, | ||
| }, true}, | ||
| {"case insensitive", []projectconfig.ComponentOverlay{ | ||
| {Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "release", Value: "1%{?dist}"}, | ||
| }, true}, | ||
| {"mixed overlays", []projectconfig.ComponentOverlay{ | ||
| {Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "BuildRequires", Value: "gcc"}, | ||
| {Type: projectconfig.ComponentOverlaySetSpecTag, Tag: "Release", Value: "5%{?dist}"}, | ||
| }, true}, | ||
| } { | ||
| t.Run(testCase.name, func(t *testing.T) { | ||
| assert.Equal(t, testCase.expected, sources.HasUserReleaseOverlay(testCase.overlays)) | ||
| }) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.