From 61869d4612600686fa7b0dbb82cf9ae417db6ef5 Mon Sep 17 00:00:00 2001 From: "hiroto.toyoda" Date: Fri, 28 Aug 2026 02:46:09 +0900 Subject: [PATCH 1/3] fix(publish): warn when push falls back from OCI 1.1 to OCI 1.0 Signed-off-by: hiroto.toyoda --- internal/oci/push.go | 17 ++++++++++------- pkg/compose/publish.go | 5 ++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/oci/push.go b/internal/oci/push.go index 1e2d0f2e95b..883378f4408 100644 --- a/internal/oci/push.go +++ b/internal/oci/push.go @@ -94,12 +94,14 @@ func DescriptorForEnvFile(path string, content []byte) v1.Descriptor { } } -func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, error) { +// PushManifest pushes the manifest for a Compose OCI artifact and returns +// the OCI version actually used. +func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, api.OCIVersion, error) { // Check if we need an extra empty layer for the manifest config if ociVersion == api.OCIVersion1_1 || ociVersion == "" { err := push(ctx, resolver, named, v1.DescriptorEmptyJSON) if err != nil { - return v1.Descriptor{}, err + return v1.Descriptor{}, "", err } } // prepare to push the manifest by pushing the layers @@ -107,13 +109,14 @@ func PushManifest(ctx context.Context, resolver remotes.Resolver, named referenc for i := range layers { layerDescriptors[i] = layers[i] if err := push(ctx, resolver, named, layers[i]); err != nil { - return v1.Descriptor{}, err + return v1.Descriptor{}, "", err } } if ociVersion != "" { // if a version was explicitly specified, use it - return createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion) + descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion) + return descriptor, ociVersion, err } // try to push in the OCI 1.1 format but fallback to OCI 1.0 on 4xx errors @@ -122,10 +125,10 @@ func PushManifest(ctx context.Context, resolver remotes.Resolver, named referenc descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1) var pushErr pusherrors.ErrUnexpectedStatus if errors.As(err, &pushErr) && isNonAuthClientError(pushErr.StatusCode) { - // TODO(milas): show a warning here (won't work with logrus) - return createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0) + descriptor, err = createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0) + return descriptor, api.OCIVersion1_0, err } - return descriptor, err + return descriptor, api.OCIVersion1_1, err } func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor v1.Descriptor) error { diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index f0fb5c2313b..8293f8f45ed 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -119,7 +119,7 @@ func (s *composeService) pushComposeArtifact(ctx context.Context, project *types resolver := oci.NewResolver(s.configFile(), desktop.ProxyTransportFor(ctx, s.apiClient()), insecureRegistries...) - descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion) + descriptor, usedOCIVersion, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion) if err != nil { s.events.On(api.Resource{ ID: repository, @@ -128,6 +128,9 @@ func (s *composeService) pushComposeArtifact(ctx context.Context, project *types }) return err } + if options.OCIVersion == "" && usedOCIVersion == api.OCIVersion1_0 { + logrus.Warn("registry does not support OCI 1.1 artifacts; falling back to OCI 1.0 format") + } if options.Application { return pushApplicationIndex(ctx, resolver, named, descriptor, project) From 6ddf065317ae3399eba18a0a017e5d7c0fedf353 Mon Sep 17 00:00:00 2001 From: "hiroto.toyoda" Date: Thu, 10 Sep 2026 10:43:21 -0700 Subject: [PATCH 2/3] fix(publish): report OCI fallback as a bool, not the OCI version Signed-off-by: hiroto.toyoda --- internal/oci/push.go | 21 +++++++++++---------- pkg/compose/publish.go | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/oci/push.go b/internal/oci/push.go index 883378f4408..89a6daf9e57 100644 --- a/internal/oci/push.go +++ b/internal/oci/push.go @@ -94,14 +94,15 @@ func DescriptorForEnvFile(path string, content []byte) v1.Descriptor { } } -// PushManifest pushes the manifest for a Compose OCI artifact and returns -// the OCI version actually used. -func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, api.OCIVersion, error) { +// PushManifest pushes the manifest for a Compose OCI artifact. The returned +// bool reports whether the push fell back from OCI 1.1 to OCI 1.0; it is +// only meaningful when err == nil. +func PushManifest(ctx context.Context, resolver remotes.Resolver, named reference.Named, layers []v1.Descriptor, ociVersion api.OCIVersion) (v1.Descriptor, bool, error) { // Check if we need an extra empty layer for the manifest config if ociVersion == api.OCIVersion1_1 || ociVersion == "" { err := push(ctx, resolver, named, v1.DescriptorEmptyJSON) if err != nil { - return v1.Descriptor{}, "", err + return v1.Descriptor{}, false, err } } // prepare to push the manifest by pushing the layers @@ -109,26 +110,26 @@ func PushManifest(ctx context.Context, resolver remotes.Resolver, named referenc for i := range layers { layerDescriptors[i] = layers[i] if err := push(ctx, resolver, named, layers[i]); err != nil { - return v1.Descriptor{}, "", err + return v1.Descriptor{}, false, err } } if ociVersion != "" { // if a version was explicitly specified, use it descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, ociVersion) - return descriptor, ociVersion, err + return descriptor, false, err } // try to push in the OCI 1.1 format but fallback to OCI 1.0 on 4xx errors - // (other than auth) since it's most likely the result of the registry not - // having support + // (other than auth) since it's most likely the result of the registry + // rejecting the OCI 1.1 request descriptor, err := createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_1) var pushErr pusherrors.ErrUnexpectedStatus if errors.As(err, &pushErr) && isNonAuthClientError(pushErr.StatusCode) { descriptor, err = createAndPushManifest(ctx, resolver, named, layerDescriptors, api.OCIVersion1_0) - return descriptor, api.OCIVersion1_0, err + return descriptor, true, err } - return descriptor, api.OCIVersion1_1, err + return descriptor, false, err } func push(ctx context.Context, resolver remotes.Resolver, ref reference.Named, descriptor v1.Descriptor) error { diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 8293f8f45ed..5a7a80d5b24 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -119,7 +119,7 @@ func (s *composeService) pushComposeArtifact(ctx context.Context, project *types resolver := oci.NewResolver(s.configFile(), desktop.ProxyTransportFor(ctx, s.apiClient()), insecureRegistries...) - descriptor, usedOCIVersion, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion) + descriptor, didFallback, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion) if err != nil { s.events.On(api.Resource{ ID: repository, @@ -128,8 +128,8 @@ func (s *composeService) pushComposeArtifact(ctx context.Context, project *types }) return err } - if options.OCIVersion == "" && usedOCIVersion == api.OCIVersion1_0 { - logrus.Warn("registry does not support OCI 1.1 artifacts; falling back to OCI 1.0 format") + if didFallback { + logrus.Warn("registry rejected the OCI 1.1 artifact push; falling back to OCI 1.0 format") } if options.Application { From 6852e02ef8cfa57ff47ed2801ccc457e5f185008 Mon Sep 17 00:00:00 2001 From: "hiroto.toyoda" Date: Fri, 11 Sep 2026 14:38:22 -0700 Subject: [PATCH 3/3] fix(publish): fold OCI fallback warning into the published event Signed-off-by: hiroto.toyoda --- pkg/compose/publish.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 35655424422..6d4380fe938 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -91,26 +91,32 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re fmt.Println(string(indent)) } } + didFallback := false if !s.dryRun { - err = s.pushComposeArtifact(ctx, project, repository, layers, options) + didFallback, err = s.pushComposeArtifact(ctx, project, repository, layers, options) if err != nil { return err } } + text, status := "published", api.Done + if didFallback { + text, status = "published (registry rejected OCI 1.1; fell back to OCI 1.0)", api.Warning + } s.events.On(api.Resource{ ID: repository, - Text: "published", - Status: api.Done, + Text: text, + Status: status, }) return nil } // pushComposeArtifact pushes the compose artifact manifest to the repository, -// and the application image index when publishing a full application -func (s *composeService) pushComposeArtifact(ctx context.Context, project *types.Project, repository string, layers []v1.Descriptor, options api.PublishOptions) error { +// and the application image index when publishing a full application. The +// returned bool reports whether the push fell back from OCI 1.1 to OCI 1.0. +func (s *composeService) pushComposeArtifact(ctx context.Context, project *types.Project, repository string, layers []v1.Descriptor, options api.PublishOptions) (bool, error) { named, err := reference.ParseDockerRef(repository) if err != nil { - return err + return false, err } var insecureRegistries []string @@ -127,16 +133,13 @@ func (s *composeService) pushComposeArtifact(ctx context.Context, project *types Text: "publishing", Status: api.Error, }) - return err - } - if didFallback { - logrus.Warn("registry rejected the OCI 1.1 artifact push; falling back to OCI 1.0 format") + return false, err } if options.Application { - return pushApplicationIndex(ctx, resolver, named, descriptor, project) + return didFallback, pushApplicationIndex(ctx, resolver, named, descriptor, project) } - return nil + return didFallback, nil } // pushApplicationIndex pushes an image index referencing every service image,