Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kargo-cli for openSUSE:Factory checked in at 2026-02-18 17:13:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kargo-cli (Old) and /work/SRC/openSUSE:Factory/.kargo-cli.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kargo-cli" Wed Feb 18 17:13:07 2026 rev:45 rq:1333760 version:1.9.3 Changes: -------- --- /work/SRC/openSUSE:Factory/kargo-cli/kargo-cli.changes 2026-02-03 21:30:55.511200534 +0100 +++ /work/SRC/openSUSE:Factory/.kargo-cli.new.1977/kargo-cli.changes 2026-02-18 17:13:28.353815099 +0100 @@ -1,0 +2,10 @@ +Wed Feb 18 08:09:08 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.9.3: + * CLI-related changes + - chore(backport release-1.9): fix(cli): correct promotion + response unmarshaling (#5702) + Dependencies: + * chore(backport release-1.9): fix: bump go-githubauth (#5716) + +------------------------------------------------------------------- Old: ---- kargo-cli-1.9.2.obscpio New: ---- kargo-cli-1.9.3.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kargo-cli.spec ++++++ --- /var/tmp/diff_new_pack.Oo2knL/_old 2026-02-18 17:13:29.945881350 +0100 +++ /var/tmp/diff_new_pack.Oo2knL/_new 2026-02-18 17:13:29.945881350 +0100 @@ -19,7 +19,7 @@ %define executable_name kargo Name: kargo-cli -Version: 1.9.2 +Version: 1.9.3 Release: 0 Summary: CLI for the Kubernetes Application lifecycle orchestration License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.Oo2knL/_old 2026-02-18 17:13:29.989883181 +0100 +++ /var/tmp/diff_new_pack.Oo2knL/_new 2026-02-18 17:13:29.993883348 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/akuity/kargo</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.9.2</param> + <param name="revision">v1.9.3</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.Oo2knL/_old 2026-02-18 17:13:30.017884347 +0100 +++ /var/tmp/diff_new_pack.Oo2knL/_new 2026-02-18 17:13:30.025884679 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/akuity/kargo</param> - <param name="changesrevision">141a6cd104f855d404119b78c5ae2646c66ea9e4</param></service></servicedata> + <param name="changesrevision">155c6852ffbffa2902f18e6c7add91a846e8d344</param></service></servicedata> (No newline at EOF) ++++++ kargo-cli-1.9.2.obscpio -> kargo-cli-1.9.3.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/docs/docs/40-operator-guide/40-security/30-access-controls.md new/kargo-cli-1.9.3/docs/docs/40-operator-guide/40-security/30-access-controls.md --- old/kargo-cli-1.9.2/docs/docs/40-operator-guide/40-security/30-access-controls.md 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/docs/docs/40-operator-guide/40-security/30-access-controls.md 2026-02-17 18:59:04.000000000 +0100 @@ -79,7 +79,7 @@ ServiceAccount resources may be mapped to users via the `rbac.kargo.akuity.io/claims` annotation, whose value is a string representation -of a JSON or YAML object with claim names as its keys and lists of claim values +of a JSON object with claim names as its keys and lists of claim values as its values. In the following example, the `ServiceAccount` resource is mapped to all of: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/docs/docs/50-user-guide/50-security/20-access-controls/index.md new/kargo-cli-1.9.3/docs/docs/50-user-guide/50-security/20-access-controls/index.md --- old/kargo-cli-1.9.2/docs/docs/50-user-guide/50-security/20-access-controls/index.md 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/docs/docs/50-user-guide/50-security/20-access-controls/index.md 2026-02-17 18:59:04.000000000 +0100 @@ -77,7 +77,7 @@ ServiceAccount resources may be mapped to users via the `rbac.kargo.akuity.io/claims` annotation, whose value is a string representation -of a JSON or YAML object with claim names as its keys and lists of claim values +of a JSON object with claim names as its keys and lists of claim values as its values. In the following example, the `ServiceAccount` resource is mapped to all of: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/docs/docs/50-user-guide/60-reference-docs/80-webhook-receivers/generic.md new/kargo-cli-1.9.3/docs/docs/50-user-guide/60-reference-docs/80-webhook-receivers/generic.md --- old/kargo-cli-1.9.2/docs/docs/50-user-guide/60-reference-docs/80-webhook-receivers/generic.md 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/docs/docs/50-user-guide/60-reference-docs/80-webhook-receivers/generic.md 2026-02-17 18:59:04.000000000 +0100 @@ -96,8 +96,8 @@ generic: secretRef: name: wh-secret - actions: - - actionType: Refresh + actions: + - actionType: Refresh ``` :::note @@ -126,9 +126,9 @@ generic: secretRef: name: wh-secret - actions: - - actionType: Refresh - whenExpression: "request.header("X-Event-Type") == 'push'" + actions: + - actionType: Refresh + whenExpression: "request.header("X-Event-Type") == 'push'" ``` :::note @@ -174,12 +174,12 @@ generic: secretRef: name: wh-secret - actions: - - actionType: Refresh - whenExpression: "request.header('X-Event-Type') == 'push'" - targetSelectionCriteria: - - kind: Warehouse - name: my-warehouse + actions: + - actionType: Refresh + whenExpression: "request.header('X-Event-Type') == 'push'" + targetSelectionCriteria: + - kind: Warehouse + name: my-warehouse ``` The following example depicts `targetSelectionCriteria` that selects @@ -198,12 +198,12 @@ generic: secretRef: name: wh-secret - actions: - - actionType: Refresh - whenExpression: "request.header('X-Event-Type') == 'push'" - targetSelectionCriteria: - - kind: Warehouse - name: "${{ normalizeGit(request.body.repository.name) }}" + actions: + - actionType: Refresh + whenExpression: "request.header('X-Event-Type') == 'push'" + targetSelectionCriteria: + - kind: Warehouse + name: "${{ normalizeGit(request.body.repository.name) }}" ``` ##### By Labels @@ -223,14 +223,14 @@ generic: secretRef: name: wh-secret - actions: - - actionType: Refresh - whenExpression: "request.header('X-Event-Type') == 'push'" - targetSelectionCriteria: - - kind: Warehouse - labelSelector: - matchLabels: - environment: prod + actions: + - actionType: Refresh + whenExpression: "request.header('X-Event-Type') == 'push'" + targetSelectionCriteria: + - kind: Warehouse + labelSelector: + matchLabels: + environment: prod ``` The following example depicts `targetSelectionCriteria` that selects @@ -249,16 +249,16 @@ generic: secretRef: name: wh-secret - actions: - - actionType: Refresh - whenExpression: "request.header('X-Event-Type') == 'push'" - targetSelectionCriteria: - - kind: Warehouse - labelSelector: - matchExpressions: - - key: service - operator: In - values: ["ui", "api"] + actions: + - actionType: Refresh + whenExpression: "request.header('X-Event-Type') == 'push'" + targetSelectionCriteria: + - kind: Warehouse + labelSelector: + matchExpressions: + - key: service + operator: In + values: ["ui", "api"] ``` ##### By Values in an Index diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/go.mod new/kargo-cli-1.9.3/go.mod --- old/kargo-cli-1.9.2/go.mod 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/go.mod 2026-02-17 18:59:04.000000000 +0100 @@ -50,7 +50,7 @@ github.com/google/uuid v1.6.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/jferrl/go-githubauth v1.5.0 + github.com/jferrl/go-githubauth v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/klauspost/compress v1.18.3 github.com/ktrysmt/go-bitbucket v0.9.87 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/go.sum new/kargo-cli-1.9.3/go.sum --- old/kargo-cli-1.9.2/go.sum 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/go.sum 2026-02-17 18:59:04.000000000 +0100 @@ -344,8 +344,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jferrl/go-githubauth v1.5.0 h1:0zv6YqxGwtu2pjtb1DP2vaPVhdsIlyy4AhrjWryJTY8= -github.com/jferrl/go-githubauth v1.5.0/go.mod h1:dwyfWjg9p59UvnSVevlPGGiVfVluPgezLlHBMLD5qs0= +github.com/jferrl/go-githubauth v1.5.1 h1:otHMf7Q6+Hw98fEznIUewsrhayXQqXinhNLc7uqYbco= +github.com/jferrl/go-githubauth v1.5.1/go.mod h1:/TwNj2nXg/u0wrTnz8+BjJDThDKaScqsczu7Ryj+v2s= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/cli/cmd/promote/promote.go new/kargo-cli-1.9.3/pkg/cli/cmd/promote/promote.go --- old/kargo-cli-1.9.2/pkg/cli/cmd/promote/promote.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/cli/cmd/promote/promote.go 2026-02-17 18:59:04.000000000 +0100 @@ -212,10 +212,14 @@ if err != nil { return fmt.Errorf("marshal promotion: %w", err) } - promo := &kargoapi.Promotion{} - if err = json.Unmarshal(promoJSON, promo); err != nil { + // The response is {"promotion": {...}} + var result struct { + Promotion *kargoapi.Promotion `json:"promotion"` + } + if err = json.Unmarshal(promoJSON, &result); err != nil { return fmt.Errorf("unmarshal promotion: %w", err) } + promo := result.Promotion if o.Wait { if err = o.waitForPromotion(ctx, nil, promo); err != nil { return fmt.Errorf("wait for promotion: %w", err) @@ -237,14 +241,18 @@ if err != nil { return err } - var promotions []*kargoapi.Promotion promotionsJSON, err := json.Marshal(res.Payload) if err != nil { return fmt.Errorf("marshal promotions: %w", err) } - if err = json.Unmarshal(promotionsJSON, &promotions); err != nil { + // The response is {"promotions": [...]} + var result struct { + Promotions []*kargoapi.Promotion `json:"promotions"` + } + if err = json.Unmarshal(promotionsJSON, &result); err != nil { return fmt.Errorf("unmarshal promotions: %w", err) } + promotions := result.Promotions if o.Wait { if err = o.waitForPromotions(ctx, promotions...); err != nil { return fmt.Errorf("wait for promotion: %w", err) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/credentials/github/app.go new/kargo-cli-1.9.3/pkg/credentials/github/app.go --- old/kargo-cli-1.9.2/pkg/credentials/github/app.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/credentials/github/app.go 2026-02-17 18:59:04.000000000 +0100 @@ -280,7 +280,7 @@ if len(parts) < 5 { return "" } - return strings.TrimSuffix(parts[len(parts)-1], ".git") + return parts[len(parts)-1] } // extractBaseURL extracts the base URL from a full repository URL. The base diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/credentials/github/app_test.go new/kargo-cli-1.9.3/pkg/credentials/github/app_test.go --- old/kargo-cli-1.9.2/pkg/credentials/github/app_test.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/credentials/github/app_test.go 2026-02-17 18:59:04.000000000 +0100 @@ -178,17 +178,6 @@ }, expected: true, }, - { - name: "valid with .git suffix", - credType: credentials.TypeGit, - repoURL: testRepoURL + ".git", - getDataMap: func() map[string][]byte { - dm := maps.Clone(supportedDataMap) - delete(dm, clientIDKey) - return dm - }, - expected: true, - }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -539,11 +528,6 @@ expected: "repo", }, { - name: "GitHub URL with .git suffix", - repoURL: "https://github.com/example/repo.git", - expected: "repo", - }, - { name: "GitHub Enterprise URL", repoURL: "https://github.example.com/example/repo", expected: "repo", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/approve_freight_v1alpha1.go new/kargo-cli-1.9.3/pkg/server/approve_freight_v1alpha1.go --- old/kargo-cli-1.9.2/pkg/server/approve_freight_v1alpha1.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/approve_freight_v1alpha1.go 2026-02-17 18:59:04.000000000 +0100 @@ -9,7 +9,6 @@ "connectrpc.com/connect" "github.com/gin-gonic/gin" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -92,11 +91,7 @@ if err := s.authorizeFn( ctx, "promote", - schema.GroupVersionResource{ - Group: kargoapi.GroupVersion.Group, - Version: kargoapi.GroupVersion.Version, - Resource: "stages", - }, + kargoapi.GroupVersion.WithResource("stages"), "", types.NamespacedName{ Namespace: project, @@ -197,6 +192,20 @@ ); err != nil { _ = c.Error(err) return + } + + if err := s.authorizeFn( + ctx, + "promote", + kargoapi.GroupVersion.WithResource("stages"), + "", + types.NamespacedName{ + Namespace: project, + Name: stageName, + }, + ); err != nil { + _ = c.Error(err) + return } if freight.IsApprovedFor(stageName) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/approve_freight_v1alpha1_test.go new/kargo-cli-1.9.3/pkg/server/approve_freight_v1alpha1_test.go --- old/kargo-cli-1.9.2/pkg/server/approve_freight_v1alpha1_test.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/approve_freight_v1alpha1_test.go 2026-02-17 18:59:04.000000000 +0100 @@ -10,6 +10,7 @@ "connectrpc.com/connect" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -442,10 +443,45 @@ }, }, { + name: "not authorized to approve (not authorized to promote)", + clientBuilder: fake.NewClientBuilder(). + WithObjects(testProject, testFreight, testStage). + WithStatusSubresource(testFreight), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return apierrors.NewForbidden( + kargoapi.GroupVersion.WithResource("stages").GroupResource(), + testStageName, + errors.New("not authorized"), + ) + } + }, + assertions: func(t *testing.T, w *httptest.ResponseRecorder, _ client.Client) { + require.Equal(t, http.StatusForbidden, w.Code) + }, + }, + { name: "approves Freight", clientBuilder: fake.NewClientBuilder(). WithObjects(testProject, testFreight, testStage). WithStatusSubresource(testFreight), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + } + }, assertions: func(t *testing.T, w *httptest.ResponseRecorder, c client.Client) { require.Equal(t, http.StatusOK, w.Code) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/create_or_update_resource_v1alpha1.go new/kargo-cli-1.9.3/pkg/server/create_or_update_resource_v1alpha1.go --- old/kargo-cli-1.9.2/pkg/server/create_or_update_resource_v1alpha1.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/create_or_update_resource_v1alpha1.go 2026-02-17 18:59:04.000000000 +0100 @@ -56,7 +56,7 @@ } // If we just created a Project successfully, keep track of this Project // being one that was created in the course of this API call. - if result.CreatedResourceManifest != nil && resource.GroupVersionKind() == projectGVK { + if err == nil && result.CreatedResourceManifest != nil && resource.GroupVersionKind() == projectGVK { createdProjects[resource.GetName()] = struct{}{} } // Convert to protobuf result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/create_resource_v1alpha1.go new/kargo-cli-1.9.3/pkg/server/create_resource_v1alpha1.go --- old/kargo-cli-1.9.2/pkg/server/create_resource_v1alpha1.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/create_resource_v1alpha1.go 2026-02-17 18:59:04.000000000 +0100 @@ -64,7 +64,7 @@ } // If we just created a Project successfully, keep track of this Project // being one that was created in the course of this API call. - if resource.GroupVersionKind() == projectGVK { + if err == nil && resource.GroupVersionKind() == projectGVK { createdProjects[resource.GetName()] = struct{}{} } // Convert to protobuf result @@ -158,7 +158,7 @@ } // If we just created a Project successfully, keep track of this Project // being one that was created in the course of this API call. - if resource.GroupVersionKind() == projectGVK { + if err == nil && resource.GroupVersionKind() == projectGVK { createdProjects[resource.GetName()] = struct{}{} } results = append(results, result) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/project_middleware.go new/kargo-cli-1.9.3/pkg/server/project_middleware.go --- old/kargo-cli-1.9.2/pkg/server/project_middleware.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/project_middleware.go 2026-02-17 18:59:04.000000000 +0100 @@ -18,7 +18,11 @@ return } p := &kargoapi.Project{} - if err := s.client.Get( + var cl client.Client = s.client + if s.client != nil && s.client.InternalClient() != nil { + cl = s.client.InternalClient() + } + if err := cl.Get( c.Request.Context(), client.ObjectKey{Name: project}, p, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/promote_downstream_v1alpha1.go new/kargo-cli-1.9.3/pkg/server/promote_downstream_v1alpha1.go --- old/kargo-cli-1.9.2/pkg/server/promote_downstream_v1alpha1.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/promote_downstream_v1alpha1.go 2026-02-17 18:59:04.000000000 +0100 @@ -300,6 +300,22 @@ return } + for _, downstream := range downstreams { + if err := s.authorizeFn( + ctx, + "promote", + kargoapi.GroupVersion.WithResource("stages"), + "", + types.NamespacedName{ + Namespace: downstream.Namespace, + Name: downstream.Name, + }, + ); err != nil { + _ = c.Error(err) + return + } + } + // Validate that freight is available to all downstream stages for _, downstream := range downstreams { if !downstream.IsFreightAvailable(freight) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/promote_downstream_v1alpha1_test.go new/kargo-cli-1.9.3/pkg/server/promote_downstream_v1alpha1_test.go --- old/kargo-cli-1.9.2/pkg/server/promote_downstream_v1alpha1_test.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/promote_downstream_v1alpha1_test.go 2026-02-17 18:59:04.000000000 +0100 @@ -10,6 +10,7 @@ "connectrpc.com/connect" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -791,13 +792,54 @@ }, }, { - name: "Successfully promote downstream", + name: "not authorized to promote to a downstream stage", clientBuilder: fake.NewClientBuilder().WithObjects( testProject, testStage, testDownstreamStage, testFreight, ), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return apierrors.NewForbidden( + kargoapi.GroupVersion.WithResource("stages").GroupResource(), + testDownstreamStage.Name, + errors.New("not authorized"), + ) + } + }, + body: mustJSONBody(promoteDownstreamRequest{ + Freight: testFreight.Name, + }), + assertions: func(t *testing.T, w *httptest.ResponseRecorder, _ client.Client) { + require.Equal(t, http.StatusForbidden, w.Code) + }, + }, + { + name: "successfully promotes downstream", + clientBuilder: fake.NewClientBuilder().WithObjects( + testProject, + testStage, + testDownstreamStage, + testFreight, + ), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + } + }, body: mustJSONBody(promoteDownstreamRequest{ Freight: testFreight.Name, }), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/promote_to_stage_v1alpha1.go new/kargo-cli-1.9.3/pkg/server/promote_to_stage_v1alpha1.go --- old/kargo-cli-1.9.2/pkg/server/promote_to_stage_v1alpha1.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/promote_to_stage_v1alpha1.go 2026-02-17 18:59:04.000000000 +0100 @@ -9,7 +9,6 @@ "connectrpc.com/connect" "github.com/gin-gonic/gin" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -94,26 +93,10 @@ return nil, connect.NewError(connect.CodeNotFound, err) } - if !s.isFreightAvailableFn(stage, freight) { - // nolint:staticcheck - return nil, connect.NewError( - connect.CodeInvalidArgument, - fmt.Errorf( - "Freight %q is not available to Stage %q", - freightName, - stageName, - ), - ) - } - if err = s.authorizeFn( ctx, "promote", - schema.GroupVersionResource{ - Group: kargoapi.GroupVersion.Group, - Version: kargoapi.GroupVersion.Version, - Resource: "stages", - }, + kargoapi.GroupVersion.WithResource("stages"), "", types.NamespacedName{ Namespace: project, @@ -123,6 +106,18 @@ return nil, err } + if !s.isFreightAvailableFn(stage, freight) { + // nolint:staticcheck + return nil, connect.NewError( + connect.CodeInvalidArgument, + fmt.Errorf( + "Freight %q is not available to Stage %q", + freightName, + stageName, + ), + ) + } + promotion, err := kargo.NewPromotionBuilder(s.client).Build(ctx, *stage, freight.Name) if err != nil { return nil, fmt.Errorf("build promotion: %w", err) @@ -250,6 +245,20 @@ freight = &list.Items[0] } + if err := s.authorizeFn( + ctx, + "promote", + kargoapi.GroupVersion.WithResource("stages"), + "", + types.NamespacedName{ + Namespace: project, + Name: stageName, + }, + ); err != nil { + _ = c.Error(err) + return + } + // Validate that the Freight is available to the Stage if !stage.IsFreightAvailable(freight) { _ = c.Error(libhttp.ErrorStr( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/promote_to_stage_v1alpha1_test.go new/kargo-cli-1.9.3/pkg/server/promote_to_stage_v1alpha1_test.go --- old/kargo-cli-1.9.2/pkg/server/promote_to_stage_v1alpha1_test.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/promote_to_stage_v1alpha1_test.go 2026-02-17 18:59:04.000000000 +0100 @@ -10,6 +10,7 @@ "connectrpc.com/connect" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -229,7 +230,7 @@ }, }, { - name: "Freight not available", + name: "promoting not authorized", req: &svcv1alpha1.PromoteToStageRequest{ Project: "fake-project", Stage: "fake-stage", @@ -251,12 +252,23 @@ getFreightByNameOrAliasFn: func( context.Context, client.Client, - string, string, string, + string, + string, + string, ) (*kargoapi.Freight, error) { return &kargoapi.Freight{}, nil }, isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { - return false + return true + }, + authorizeFn: func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return errors.New("not authorized") }, }, assertions: func( @@ -265,16 +277,11 @@ _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err) - var connErr *connect.Error - require.True(t, errors.As(err, &connErr)) - require.Equal(t, connect.CodeInvalidArgument, connErr.Code()) - require.Contains(t, connErr.Message(), "Freight") - require.Contains(t, connErr.Message(), "is not available to Stage") + require.Error(t, err, "not authorized") }, }, { - name: "promoting not authorized", + name: "Freight not available", req: &svcv1alpha1.PromoteToStageRequest{ Project: "fake-project", Stage: "fake-stage", @@ -296,15 +303,10 @@ getFreightByNameOrAliasFn: func( context.Context, client.Client, - string, - string, - string, + string, string, string, ) (*kargoapi.Freight, error) { return &kargoapi.Freight{}, nil }, - isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { - return true - }, authorizeFn: func( context.Context, string, @@ -312,7 +314,10 @@ string, client.ObjectKey, ) error { - return errors.New("not authorized") + return nil + }, + isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { + return false }, }, assertions: func( @@ -321,7 +326,12 @@ _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err, "not authorized") + require.Error(t, err) + var connErr *connect.Error + require.True(t, errors.As(err, &connErr)) + require.Equal(t, connect.CodeInvalidArgument, connErr.Code()) + require.Contains(t, connErr.Message(), "Freight") + require.Contains(t, connErr.Message(), "is not available to Stage") }, }, { @@ -355,9 +365,6 @@ ) (*kargoapi.Freight, error) { return &kargoapi.Freight{}, nil }, - isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { - return true - }, authorizeFn: func( context.Context, string, @@ -367,6 +374,9 @@ ) error { return nil }, + isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { + return true + }, }, assertions: func( t *testing.T, @@ -413,9 +423,6 @@ }, }, nil }, - isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { - return true - }, authorizeFn: func( context.Context, string, @@ -425,6 +432,9 @@ ) error { return nil }, + isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { + return true + }, createPromotionFn: func( context.Context, client.Object, @@ -478,9 +488,6 @@ }, }, nil }, - isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { - return true - }, authorizeFn: func( context.Context, string, @@ -490,6 +497,9 @@ ) error { return nil }, + isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { + return true + }, createPromotionFn: func( context.Context, client.Object, @@ -641,8 +651,75 @@ }, }, { + name: "promoting not authorized", + clientBuilder: fake.NewClientBuilder().WithObjects(testProject, testStage, testFreight), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return apierrors.NewForbidden( + kargoapi.GroupVersion.WithResource("stages").GroupResource(), + testStage.Name, + errors.New("not authorized"), + ) + } + }, + body: mustJSONBody(promoteToStageRequest{ + Freight: testFreight.Name, + }), + assertions: func(t *testing.T, w *httptest.ResponseRecorder, _ client.Client) { + require.Equal(t, http.StatusForbidden, w.Code) + }, + }, + { + name: "Freight not available to Stage", + clientBuilder: fake.NewClientBuilder().WithObjects( + testProject, + func() *kargoapi.Stage { + s := testStage.DeepCopy() + s.Spec.RequestedFreight[0].Sources = kargoapi.FreightSources{ + Stages: []string{"some-other-stage"}, + } + return s + }(), + testFreight, + ), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + } + }, + body: mustJSONBody(promoteToStageRequest{ + Freight: testFreight.Name, + }), + assertions: func(t *testing.T, w *httptest.ResponseRecorder, _ client.Client) { + require.Equal(t, http.StatusBadRequest, w.Code) + }, + }, + { name: "Successfully promote by freight name", clientBuilder: fake.NewClientBuilder().WithObjects(testProject, testStage, testFreight), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + } + }, body: mustJSONBody(promoteToStageRequest{ Freight: testFreight.Name, }), @@ -661,6 +738,17 @@ { name: "Successfully promote by freight alias", clientBuilder: fake.NewClientBuilder().WithObjects(testProject, testStage, testFreight), + serverSetup: func(_ *testing.T, s *server) { + s.authorizeFn = func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + } + }, body: mustJSONBody(promoteToStageRequest{ FreightAlias: "fake-alias", }), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/rest_test.go new/kargo-cli-1.9.3/pkg/server/rest_test.go --- old/kargo-cli-1.9.2/pkg/server/rest_test.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/rest_test.go 2026-02-17 18:59:04.000000000 +0100 @@ -41,7 +41,10 @@ headers map[string]string clientBuilder *fake.ClientBuilder serverConfig *config.ServerConfig - assertions func(*testing.T, *httptest.ResponseRecorder, client.Client) + // serverSetup is an optional function that can be used to perform additional + // case-specific server initialization. + serverSetup func(*testing.T, *server) + assertions func(*testing.T, *httptest.ResponseRecorder, client.Client) } func testRESTEndpoint( @@ -108,6 +111,10 @@ rbac.RolesDatabaseConfig{KargoNamespace: testKargoNamespace}, ) + if testCase.serverSetup != nil { + testCase.serverSetup(t, s) + } + u := url if testCase.url != "" { u = testCase.url diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/server/update_resource_v1alpha1.go new/kargo-cli-1.9.3/pkg/server/update_resource_v1alpha1.go --- old/kargo-cli-1.9.2/pkg/server/update_resource_v1alpha1.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/server/update_resource_v1alpha1.go 2026-02-17 18:59:04.000000000 +0100 @@ -133,7 +133,7 @@ } // If we just created a Project successfully, keep track of this Project // being one that was created in the course of this API call. - if result.CreatedResourceManifest != nil && resource.GroupVersionKind() == projectGVK { + if err == nil && result.CreatedResourceManifest != nil && resource.GroupVersionKind() == projectGVK { createdProjects[resource.GetName()] = struct{}{} } results = append(results, result) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/subscription/schemas/chart.json new/kargo-cli-1.9.3/pkg/subscription/schemas/chart.json --- old/kargo-cli-1.9.2/pkg/subscription/schemas/chart.json 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/subscription/schemas/chart.json 2026-02-17 18:59:04.000000000 +0100 @@ -8,7 +8,7 @@ "repoURL": { "type": "string", "minLength": 1, - "pattern": "^(((https?)|(oci))://)(([\\w\\d\\.\\-]+)(:([\\d]+)?)?(/.*)*$", + "pattern": "^(((https?)|(oci))://)(([\\w\\d\\.\\-]+)(:([\\d]+)?)?(/.*)*)$", "description": "RepoURL specifies the URL of a Helm chart repository. It may be a classic chart repository (using HTTP/S) OR a repository within an OCI registry. Classic chart repositories can contain differently named charts. When this field points to such a repository, the name field MUST also be used to specify the name of the desired chart within that repository. In the case of a repository within an OCI registry, the URL implicitly points to a specific chart and the name field MUST NOT be used. This field is required." }, "name": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/urls/common.go new/kargo-cli-1.9.3/pkg/urls/common.go --- old/kargo-cli-1.9.2/pkg/urls/common.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/urls/common.go 2026-02-17 18:59:04.000000000 +0100 @@ -0,0 +1,20 @@ +package urls + +import ( + "strings" + "unicode" +) + +// SanitizeURL removes leading and trailing whitespace only from a string +// presumed to represent a URL. It additionally removes non-printable runes such +// as byte order marks (BOMs) from anywhere in a string. Leading whitespace and +// non-printable runes can easily be copied and pasted without a user realizing +// and are known to interfere with URL parsing. +func SanitizeURL(url string) string { + return strings.TrimSpace(strings.Map(func(r rune) rune { + if unicode.IsPrint(r) { + return r + } + return -1 + }, url)) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/urls/common_test.go new/kargo-cli-1.9.3/pkg/urls/common_test.go --- old/kargo-cli-1.9.2/pkg/urls/common_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/urls/common_test.go 2026-02-17 18:59:04.000000000 +0100 @@ -0,0 +1,38 @@ +package urls + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSanitizeURL(t *testing.T) { + testCases := map[string]string{ + // Leading and trailing whitespace + " https://example.com/repo ": "https://example.com/repo", + "\n\thttps://example.com/repo\t\n": "https://example.com/repo", + // Non-printable runes (e.g., BOMs) + "\uFEFFhttps://example.com/repo": "https://example.com/repo", + "https://example.com/\u200Brepo": "https://example.com/repo", + "https://example.com/repo\uFEFF": "https://example.com/repo", + "\uFEFF https://example.com/\u200Brepo \uFEFF": "https://example.com/repo", + // Combination of both + "\uFEFF \n https://example.com/\trepo \u200B \n ": "https://example.com/repo", + // No changes needed + "https://example.com/repo": "https://example.com/repo", + "ftp://example.com/resource": "ftp://example.com/resource", + " ": "", + "": "", + // Internal whitespace should not be removed + "https://example.com/ myrepo": "https://example.com/ myrepo", + // Internal non-printable runes should be removed + "https://example.com/\u200Bmyrepo": "https://example.com/myrepo", + } + for in, out := range testCases { + t.Run(in, func(t *testing.T) { + require.Equal(t, out, + SanitizeURL(in), + ) + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/urls/git.go new/kargo-cli-1.9.3/pkg/urls/git.go --- old/kargo-cli-1.9.2/pkg/urls/git.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/urls/git.go 2026-02-17 18:59:04.000000000 +0100 @@ -20,7 +20,7 @@ // normalized will be returned as-is. func NormalizeGit(repo string) string { origRepo := repo - repo = strings.ToLower(repo) + repo = SanitizeURL(strings.ToLower(repo)) // HTTP/S URLs if strings.HasPrefix(repo, "http://") || strings.HasPrefix(repo, "https://") { @@ -53,7 +53,6 @@ repoURL.Path = strings.TrimSuffix(repoURL.Path, ".git") return repoURL.String() } - // URLS of the form [user@]host.xz[:path/to/repo[.git][/]] matches := scpSyntaxRegex.FindStringSubmatch(repo) if len(matches) != 2 && len(matches) != 3 { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/urls/helm.go new/kargo-cli-1.9.3/pkg/urls/helm.go --- old/kargo-cli-1.9.2/pkg/urls/helm.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/urls/helm.go 2026-02-17 18:59:04.000000000 +0100 @@ -1,6 +1,7 @@ package urls import ( + "net/url" "strings" ) @@ -8,14 +9,18 @@ // Crucially, this function removes the oci:// prefix from the URL if there is // one. func NormalizeChart(repo string) string { + ogRepo := repo + repo = SanitizeURL(repo) + // just to check validity + if _, err := url.Parse(repo); err != nil { + return ogRepo + } // Note: We lean a bit on image.NormalizeURL() because it is excellent at // normalizing the many different forms of equivalent URLs for Docker Hub // repositories. return NormalizeImage( strings.TrimPrefix( - strings.ToLower( - strings.TrimSpace(repo), - ), + strings.ToLower(repo), "oci://", ), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/pkg/urls/image.go new/kargo-cli-1.9.3/pkg/urls/image.go --- old/kargo-cli-1.9.2/pkg/urls/image.go 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/pkg/urls/image.go 2026-02-17 18:59:04.000000000 +0100 @@ -16,9 +16,11 @@ // canonical representation of a repository URL is needed. Any URL that cannot // be normalized will be returned as-is. func NormalizeImage(repoURL string) string { + ogRepoURL := repoURL + repoURL = SanitizeURL(strings.ToLower(repoURL)) parsed, err := name.ParseReference(repoURL, name.WeakValidation) if err != nil { - return repoURL + return ogRepoURL } reg := parsed.Context().Registry.Name() repo := parsed.Context().RepositoryStr() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/ui/src/features/project/pipelines/promotion/promote.tsx new/kargo-cli-1.9.3/ui/src/features/project/pipelines/promotion/promote.tsx --- old/kargo-cli-1.9.2/ui/src/features/project/pipelines/promotion/promote.tsx 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/ui/src/features/project/pipelines/promotion/promote.tsx 2026-02-17 18:59:04.000000000 +0100 @@ -8,13 +8,14 @@ import { paths } from '@ui/config/paths'; import { useExtensionsContext } from '@ui/extensions/extensions-context'; import { ModalComponentProps } from '@ui/features/common/modal/modal-context'; -import { useActionContext } from '@ui/features/project/pipelines/context/action-context'; +import { IAction, useActionContext } from '@ui/features/project/pipelines/context/action-context'; import { promoteDownstream, promoteToStage } from '@ui/gen/api/service/v1alpha1/service-KargoService_connectquery'; import { Freight, Stage } from '@ui/gen/api/v1alpha1/generated_pb'; +import { useDictionaryContext } from '../context/dictionary-context'; import { isStageControlFlow } from '../nodes/stage-meta-utils'; import { FreightDetails } from './freight-details'; @@ -30,7 +31,10 @@ const navigate = useNavigate(); const { promoteTabs } = useExtensionsContext(); - const isControlFlow = isStageControlFlow(props.stage); + const dictionaryContext = useDictionaryContext(); + + const isDownstreamPromotion = + actionContext?.action?.type === IAction.PROMOTE_DOWNSTREAM || isStageControlFlow(props.stage); const freightAlias = props.freight?.alias; const stageName = props.stage?.metadata?.name; @@ -70,7 +74,7 @@ freight: props.freight?.metadata?.name }; - if (isControlFlow) { + if (isDownstreamPromotion) { promoteDownstreamActionMutation.mutate(payload); return; } @@ -78,13 +82,19 @@ promoteActionMutation.mutate(payload); }; + let promotingTo = stageName || ''; + + if (isDownstreamPromotion) { + promotingTo = [...(dictionaryContext?.subscribersByStage?.[promotingTo] || [])].join(', '); + } + return ( <Drawer open={props.visible} onClose={props.hide} title={ <Flex align='center'> - Promote {freightAlias} to {stageName} + Promote {freightAlias} to {promotingTo} </Flex> } size='large' @@ -97,7 +107,7 @@ onClick={onPromote} loading={promoteActionMutation.isPending || promoteDownstreamActionMutation.isPending} > - Promote{isControlFlow && ' to downstream'} + Promote{isDownstreamPromotion && ' to downstream'} </Button> } > diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kargo-cli-1.9.2/ui/src/gen/subscriptions/chart.json new/kargo-cli-1.9.3/ui/src/gen/subscriptions/chart.json --- old/kargo-cli-1.9.2/ui/src/gen/subscriptions/chart.json 2026-02-02 13:44:17.000000000 +0100 +++ new/kargo-cli-1.9.3/ui/src/gen/subscriptions/chart.json 2026-02-17 18:59:04.000000000 +0100 @@ -7,7 +7,7 @@ "repoURL": { "type": "string", "minLength": 1, - "pattern": "^(((https?)|(oci))://)(([\\w\\d\\.\\-]+)(:([\\d]+)?)?(/.*)*$", + "pattern": "^(((https?)|(oci))://)(([\\w\\d\\.\\-]+)(:([\\d]+)?)?(/.*)*)$", "description": "RepoURL specifies the URL of a Helm chart repository. It may be a classic chart repository (using HTTP/S) OR a repository within an OCI registry. Classic chart repositories can contain differently named charts. When this field points to such a repository, the name field MUST also be used to specify the name of the desired chart within that repository. In the case of a repository within an OCI registry, the URL implicitly points to a specific chart and the name field MUST NOT be used. This field is required." }, "name": { ++++++ kargo-cli.obsinfo ++++++ --- /var/tmp/diff_new_pack.Oo2knL/_old 2026-02-18 17:13:34.858085764 +0100 +++ /var/tmp/diff_new_pack.Oo2knL/_new 2026-02-18 17:13:34.862085930 +0100 @@ -1,5 +1,5 @@ name: kargo-cli -version: 1.9.2 -mtime: 1770036257 -commit: 141a6cd104f855d404119b78c5ae2646c66ea9e4 +version: 1.9.3 +mtime: 1771351144 +commit: 155c6852ffbffa2902f18e6c7add91a846e8d344 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/kargo-cli/vendor.tar.gz /work/SRC/openSUSE:Factory/.kargo-cli.new.1977/vendor.tar.gz differ: char 93, line 2
