From ace0581a069e179ee3fbf87c949ab205e27e7a52 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Fri, 13 Mar 2026 17:36:01 +0000 Subject: [PATCH 1/2] Add 'describe changed' command to check if a package needs rebuild Adds a new subcommand that checks whether a package's current version exists in the local or remote cache. Exits 0 if cached (unchanged), 1 if not (needs rebuild). Intended for CI branching decisions. Co-authored-by: Ona --- cmd/describe-changed.go | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 cmd/describe-changed.go diff --git a/cmd/describe-changed.go b/cmd/describe-changed.go new file mode 100644 index 00000000..363382bf --- /dev/null +++ b/cmd/describe-changed.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/gitpod-io/leeway/pkg/leeway" + "github.com/gitpod-io/leeway/pkg/leeway/cache" + "github.com/gitpod-io/leeway/pkg/leeway/cache/local" +) + +// describeChangedCmd checks whether a package has changed by looking up its +// current version hash in the local and remote caches. If the version exists +// in either cache the package is unchanged (exit 0); otherwise it has changed +// and needs a rebuild (exit 1). +var describeChangedCmd = &cobra.Command{ + Use: "changed ", + Short: "Checks whether a package needs to be rebuilt by consulting the cache", + Long: `Computes the version hash of a package and checks whether a build artifact +for that version already exists in the local or remote cache. + +Exits with code 0 if the package is cached (unchanged), or code 1 if it is not +(changed / needs rebuild). This is useful for CI branching decisions: + + if leeway describe changed my-component:my-package; then + echo "unchanged, skipping build" + else + echo "changed, rebuilding" + leeway build my-component:my-package + fi`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + _, pkg, _, exists := getTarget(args, false) + if !exists { + return + } + if pkg == nil { + log.Fatal("changed requires a package, not a component") + } + + version, err := pkg.Version() + if err != nil { + log.WithError(err).Fatal("cannot compute package version") + } + + // Check local cache + localCacheLoc := os.Getenv(leeway.EnvvarCacheDir) + if localCacheLoc == "" { + localCacheLoc = filepath.Join(os.TempDir(), "leeway", "cache") + } + localCache, err := local.NewFilesystemCache(localCacheLoc) + if err != nil { + log.WithError(err).Fatal("cannot set up local cache") + } + + if _, found := localCache.Location(pkg); found { + fmt.Printf("%s\t%s\tcached locally\n", pkg.FullName(), version) + os.Exit(0) + } + + // Check remote cache + remoteCache := getRemoteCacheFromEnv() + remote, err := remoteCache.ExistingPackages(context.Background(), []cache.Package{pkg}) + if err != nil { + log.WithError(err).Warn("cannot check remote cache, assuming changed") + fmt.Printf("%s\t%s\tchanged\n", pkg.FullName(), version) + os.Exit(1) + } + + if _, found := remote[pkg]; found { + fmt.Printf("%s\t%s\tcached remotely\n", pkg.FullName(), version) + os.Exit(0) + } + + fmt.Printf("%s\t%s\tchanged\n", pkg.FullName(), version) + os.Exit(1) + }, +} + +func init() { + describeCmd.AddCommand(describeChangedCmd) +} From 19c455106c63e53211ba8045e6a89d05314f7e78 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Mon, 16 Mar 2026 11:43:54 +0000 Subject: [PATCH 2/2] Use distinct exit codes: 0=cached, 1=changed, 2=error Addresses review feedback: errors now exit with code 2 so CI scripts can distinguish 'package changed' (exit 1) from 'something went wrong' (exit 2). Co-authored-by: Ona --- cmd/describe-changed.go | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/cmd/describe-changed.go b/cmd/describe-changed.go index 363382bf..4eb920b9 100644 --- a/cmd/describe-changed.go +++ b/cmd/describe-changed.go @@ -14,18 +14,31 @@ import ( "github.com/gitpod-io/leeway/pkg/leeway/cache/local" ) +const ( + // ExitChangedUnchanged indicates the package is cached and does not need a rebuild. + ExitChangedUnchanged = 0 + // ExitChangedNeedsRebuild indicates the package has changed and needs a rebuild. + ExitChangedNeedsRebuild = 1 + // ExitChangedError indicates an error occurred while checking. + ExitChangedError = 2 +) + // describeChangedCmd checks whether a package has changed by looking up its // current version hash in the local and remote caches. If the version exists // in either cache the package is unchanged (exit 0); otherwise it has changed -// and needs a rebuild (exit 1). +// and needs a rebuild (exit 1). Errors exit with code 2. var describeChangedCmd = &cobra.Command{ Use: "changed ", Short: "Checks whether a package needs to be rebuilt by consulting the cache", Long: `Computes the version hash of a package and checks whether a build artifact for that version already exists in the local or remote cache. -Exits with code 0 if the package is cached (unchanged), or code 1 if it is not -(changed / needs rebuild). This is useful for CI branching decisions: +Exit codes: + 0 - package is cached (unchanged, no rebuild needed) + 1 - package is not cached (changed, needs rebuild) + 2 - an error occurred + +This is useful for CI branching decisions: if leeway describe changed my-component:my-package; then echo "unchanged, skipping build" @@ -37,15 +50,17 @@ Exits with code 0 if the package is cached (unchanged), or code 1 if it is not Run: func(cmd *cobra.Command, args []string) { _, pkg, _, exists := getTarget(args, false) if !exists { - return + os.Exit(ExitChangedError) } if pkg == nil { - log.Fatal("changed requires a package, not a component") + log.Error("changed requires a package, not a component") + os.Exit(ExitChangedError) } version, err := pkg.Version() if err != nil { - log.WithError(err).Fatal("cannot compute package version") + log.WithError(err).Error("cannot compute package version") + os.Exit(ExitChangedError) } // Check local cache @@ -55,30 +70,30 @@ Exits with code 0 if the package is cached (unchanged), or code 1 if it is not } localCache, err := local.NewFilesystemCache(localCacheLoc) if err != nil { - log.WithError(err).Fatal("cannot set up local cache") + log.WithError(err).Error("cannot set up local cache") + os.Exit(ExitChangedError) } if _, found := localCache.Location(pkg); found { fmt.Printf("%s\t%s\tcached locally\n", pkg.FullName(), version) - os.Exit(0) + os.Exit(ExitChangedUnchanged) } // Check remote cache remoteCache := getRemoteCacheFromEnv() remote, err := remoteCache.ExistingPackages(context.Background(), []cache.Package{pkg}) if err != nil { - log.WithError(err).Warn("cannot check remote cache, assuming changed") - fmt.Printf("%s\t%s\tchanged\n", pkg.FullName(), version) - os.Exit(1) + log.WithError(err).Error("cannot check remote cache") + os.Exit(ExitChangedError) } if _, found := remote[pkg]; found { fmt.Printf("%s\t%s\tcached remotely\n", pkg.FullName(), version) - os.Exit(0) + os.Exit(ExitChangedUnchanged) } fmt.Printf("%s\t%s\tchanged\n", pkg.FullName(), version) - os.Exit(1) + os.Exit(ExitChangedNeedsRebuild) }, }