Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ endif
## Create a release for the specified RELEASE_TAG.
release-tag: var-require-all-RELEASE_TAG-GITHUB_TOKEN
$(MAKE) release VERSION=$(RELEASE_TAG)
REPO=$(REPO) CREATE_GITHUB_RELEASE=true $(MAKE) release-publish VERSION=$(RELEASE_TAG)
REPO=$(REPO) $(MAKE) release-publish VERSION=$(RELEASE_TAG)

## Generate release notes for the specified VERSION.
release-notes: hack/bin/release var-require-all-VERSION-GITHUB_TOKEN
Expand Down
262 changes: 220 additions & 42 deletions hack/release/build.go

Large diffs are not rendered by default.

157 changes: 157 additions & 0 deletions hack/release/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"errors"
"slices"
"strings"
"testing"
)

func TestExtractGitHashFromVersion(t *testing.T) {
t.Parallel()

cases := []struct {
version string
want string
wantErr bool
}{
{
version: "v3.22.1",
wantErr: true,
},
{
version: "v3.22.0-1.0",
wantErr: true,
},
{
version: "v3.22.0-gshorthash",
wantErr: true,
},
{
version: "v3.22.0-glonghashthatisnotvalid",
wantErr: true,
},
{
version: "v3.22.1-25-g997f6be93484-extra",
wantErr: true,
},
{
version: "v3.22.1-25-g997f6be93484-dirty",
wantErr: true,
},
{
version: "v3.22.1-948-g997f6be93484",
want: "997f6be93484",
},
{
version: "v3.22.1-calient-0.dev-948-g1234567890ab",
want: "1234567890ab",
},
{
version: "v3.23.0-2.0-calient-0.dev-948-gabcdef123456",
want: "abcdef123456",
},
}

for _, tc := range cases {
t.Run(tc.version, func(t *testing.T) {
t.Parallel()
got, err := extractGitHashFromVersion(tc.version)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got none")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.want {
t.Fatalf("extractGitHashFromVersion(%q) = %q, want %q", tc.version, got, tc.want)
}
})
}
}

// Tests below must NOT be parallel since they mutate package-level vars.

func TestRunBuildCleanup(t *testing.T) {
t.Run("LIFO order and error collection", func(t *testing.T) {
buildCleanupFns = nil
defer func() { buildCleanupFns = nil }()

var order []int
buildCleanupFns = append(buildCleanupFns, func(ctx context.Context) error {
order = append(order, 1)
return errors.New("cleanup-1 failed")
})
buildCleanupFns = append(buildCleanupFns, func(ctx context.Context) error {
order = append(order, 2)
return nil
})
buildCleanupFns = append(buildCleanupFns, func(ctx context.Context) error {
order = append(order, 3)
return errors.New("cleanup-3 failed")
})

err := runBuildCleanup(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "cleanup-1 failed") {
t.Fatalf("missing cleanup-1 error: %v", err)
}
if !strings.Contains(err.Error(), "cleanup-3 failed") {
t.Fatalf("missing cleanup-3 error: %v", err)
}
if !slices.Equal(order, []int{3, 2, 1}) {
t.Fatalf("expected LIFO order [3, 2, 1], got %v", order)
}
if buildCleanupFns != nil {
t.Fatal("expected buildCleanupFns to be nil after cleanup")
}
})

t.Run("empty slice is no-op", func(t *testing.T) {
buildCleanupFns = nil
if err := runBuildCleanup(context.Background()); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})

t.Run("propagates context cancellation", func(t *testing.T) {
buildCleanupFns = nil
defer func() { buildCleanupFns = nil }()

buildCleanupFns = append(buildCleanupFns, func(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
})

ctx, cancel := context.WithCancel(context.Background())
cancel()
err := runBuildCleanup(ctx)
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected context.Canceled, got: %v", err)
}
})
}
2 changes: 1 addition & 1 deletion hack/release/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ var checkVersionFormat = func(ctx context.Context, c *cli.Command) (context.Cont
return ctx, nil
}
checkLog.Debug("Checking version format")
if isRelease, err := isReleaseVersionFormat(version); err != nil {
if isRelease, err := isValidReleaseVersion(version); err != nil {
return ctx, fmt.Errorf("checking version format: %w", err)
} else if !isRelease {
return ctx, fmt.Errorf("provided version %q is not a valid release version. \n"+
Expand Down
38 changes: 36 additions & 2 deletions hack/release/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"regexp"
"slices"
"strings"
"time"

"github.com/urfave/cli/v3"
)
Expand All @@ -33,6 +34,13 @@ var debugFlag = &cli.BoolFlag{
Sources: cli.EnvVars("DEBUG"),
}

var extensionTimeoutFlag = &cli.DurationFlag{
Name: "timeout",
Usage: "Timeout duration for extension execution",
Sources: cli.EnvVars("EXTENSION_TIMEOUT"),
Value: 5 * time.Minute,
}

// Git/GitHub related flags.
var (
githubFlagCategory = "GitHub Options"
Expand Down Expand Up @@ -73,7 +81,7 @@ var (
Category: githubFlagCategory,
Usage: "Create a GitHub release",
Sources: cli.EnvVars("CREATE_GITHUB_RELEASE"),
Value: false,
Value: true,
Action: func(ctx context.Context, c *cli.Command, b bool) error {
if b && c.String(githubTokenFlag.Name) == "" {
return fmt.Errorf("github-token is required to create GitHub releases")
Expand Down Expand Up @@ -121,7 +129,7 @@ var (
// No need to validate version for hashrelease
return nil
}
if valid, err := isReleaseVersionFormat(s); err != nil {
if valid, err := isValidReleaseVersion(s); err != nil {
return fmt.Errorf("error validating version format: %w", err)
} else if !valid {
return fmt.Errorf("version %q is not a valid release version", s)
Expand Down Expand Up @@ -314,6 +322,19 @@ var (
Sources: cli.EnvVars("CALICO_DIR"),
Action: dirFlagCheck,
}
calicoGitRepoFlag = &cli.StringFlag{
Name: "calico-repo",
Category: calicoFlagCategory,
Usage: "The git repository to clone Calico from. Used when no Calico dir for CRDs is provided (development and testing purposes only)",
Sources: cli.EnvVars("CALICO_REPO"),
Value: "projectcalico/calico",
}
calicoGitBranchFlag = &cli.StringFlag{
Name: "calico-branch",
Category: calicoFlagCategory,
Usage: "The git branch to clone Calico from. Used when no Calico dir for CRDs is provided (development and testing purposes only)",
Sources: cli.EnvVars("CALICO_BRANCH"),
}
)

// Enterprise related flags.
Expand Down Expand Up @@ -377,6 +398,19 @@ var (
Sources: cli.EnvVars("ENTERPRISE_DIR"),
Action: dirFlagCheck,
}
enterpriseGitRepoFlag = &cli.StringFlag{
Name: "enterprise-repo",
Category: enterpriseFlagCategory,
Usage: "The git repository to clone Enterprise from. Used when no Enterprise dir for CRDs is provided (development and testing purposes only)",
Sources: cli.EnvVars("ENTERPRISE_REPO"),
Value: "tigera/calico-private",
}
enterpriseGitBranchFlag = &cli.StringFlag{
Name: "enterprise-branch",
Category: enterpriseFlagCategory,
Usage: "The git branch to clone Enterprise from. Use in place of specifying the Enterprise dir for CRDs (development and testing purposes only)",
Sources: cli.EnvVars("ENTERPRISE_BRANCH", "CALICO_ENTERPRISE_BRANCH"),
}
exceptEnterpriseFlag = &cli.StringSliceFlag{
Name: "except-calico-enterprise",
Category: enterpriseFlagCategory,
Expand Down
2 changes: 1 addition & 1 deletion hack/release/prep.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ var prepAction = cli.ActionFunc(func(ctx context.Context, c *cli.Command) error
// Update registry for Enterprise
if eRegistry := c.String(enterpriseRegistryFlag.Name); eRegistry != "" {
logrus.Debugf("Updating Enterprise registry to %s", eRegistry)
if err := modifyComponentImageConfig(repoRootDir, enterpriseRegistryConfigKey, eRegistry); err != nil {
if err := modifyComponentImageConfig(repoRootDir, componentImageConfigRelPath, enterpriseRegistryConfigKey, eRegistry); err != nil {
return err
}
}
Expand Down
14 changes: 5 additions & 9 deletions hack/release/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ var publishBefore = cli.BeforeFunc(func(ctx context.Context, c *cli.Command) (co
configureLogging(c)

var err error

ctx, err = addRepoInfoToCtx(ctx, c.String(gitRepoFlag.Name))
if err != nil {
return ctx, err
}

// Run verison validations. This is a mandatory check.
// Run version validations. This is a mandatory check.
ctx, err = checkVersion(ctx, c)
if err != nil {
return ctx, err
Expand All @@ -71,12 +72,7 @@ var publishBefore = cli.BeforeFunc(func(ctx context.Context, c *cli.Command) (co
return ctx, nil
}

// If building a hashrelease, publishGithubRelease must be false
if c.Bool(hashreleaseFlag.Name) && c.Bool(createGithubReleaseFlag.Name) {
return ctx, fmt.Errorf("cannot publish GitHub release for hashrelease builds")
}

if !c.Bool(createGithubReleaseFlag.Name) {
if c.Bool(hashreleaseFlag.Name) || !c.Bool(createGithubReleaseFlag.Name) {
return ctx, nil
}

Expand Down Expand Up @@ -116,9 +112,9 @@ var publishAction = cli.ActionFunc(func(ctx context.Context, c *cli.Command) err
return publishGithubRelease(ctx, c, repoRootDir)
})

// Publish the operator images to the specified registry.
// publishImages publishes the operator images to the specified registry.
// If the images are already published, it skips publishing.
func publishImages(c *cli.Command, repoRootDir string) error {
var publishImages = func(c *cli.Command, repoRootDir string) error {
version := c.String(versionFlag.Name)
log := logrus.WithField("version", version)
// Check if images are already published
Expand Down
4 changes: 4 additions & 0 deletions hack/release/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ func calicoVersions(repo, rootDir, operatorVersion string, local bool) (map[stri
return versions, nil
}

// isValidReleaseVersion validates the operator release version format.
// It defaults to standard release format (vX.Y.Z) but can be overridden if a different format is needed.
var isValidReleaseVersion = isReleaseVersionFormat

// isReleaseVersionFormat checks if the version in the format vX.Y.Z.
func isReleaseVersionFormat(version string) (bool, error) {
releaseRegex, err := regexp.Compile(releaseFormat)
Expand Down
29 changes: 29 additions & 0 deletions hack/release/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,35 @@ func TestAddTrailingSlash(t *testing.T) {
}
}

func TestIsValidReleaseVersionOverride(t *testing.T) {
orig := isValidReleaseVersion
defer func() { isValidReleaseVersion = orig }()

t.Run("set to isReleaseVersionFormat", func(t *testing.T) {
isValidReleaseVersion = isReleaseVersionFormat
ok, err := isValidReleaseVersion("v1.2.3")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected v1.2.3 to be valid with default validator")
}
})

t.Run("can be overridden", func(t *testing.T) {
// Override to accept enterprise format (vX.Y.Z-A.B)
isValidReleaseVersion = isEnterpriseReleaseVersionFormat

ok, err := isValidReleaseVersion("v1.2.3-1.0")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatal("expected v1.2.3-1.0 to be valid with enterprise validator")
}
})
}

func TestIsPrereleaseEnterpriseVersion(t *testing.T) {
t.Parallel()

Expand Down
Loading