From ac344f20d290e0ba180b620708d925812ce149b0 Mon Sep 17 00:00:00 2001 From: Raj Date: Fri, 20 Mar 2026 13:02:24 -0700 Subject: [PATCH 1/3] Add --all-tags/-a flag to image push --- .../ContainerCommands/Image/ImagePush.swift | 97 ++++++++++++++++++- .../Client/ClientImage.swift | 42 ++++++++ .../ContainerAPIService/Client/Flags.swift | 11 +++ .../Client/ImageServiceXPCKeys.swift | 3 + .../Server/ImagesService.swift | 86 ++++++++++++++++ .../Server/ImagesServiceHarness.swift | 31 ++++-- .../Images/TestCLIImagesCommand.swift | 43 ++++++++ 7 files changed, 302 insertions(+), 11 deletions(-) diff --git a/Sources/ContainerCommands/Image/ImagePush.swift b/Sources/ContainerCommands/Image/ImagePush.swift index 60e8bf803..94ab14c55 100644 --- a/Sources/ContainerCommands/Image/ImagePush.swift +++ b/Sources/ContainerCommands/Image/ImagePush.swift @@ -17,7 +17,9 @@ import ArgumentParser import ContainerAPIClient import Containerization +import ContainerizationError import ContainerizationOCI +import Foundation import TerminalProgress extension Application { @@ -33,8 +35,11 @@ extension Application { @OptionGroup var progressFlags: Flags.Progress + @OptionGroup + var imageUploadFlags: Flags.ImageUpload + @Option( - name: .shortAndLong, + name: .long, help: "Limit the push to the specified architecture" ) var arch: String? @@ -47,6 +52,9 @@ extension Application { @Option(help: "Limit the push to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch) [environment: CONTAINER_DEFAULT_PLATFORM]") var platform: String? + @Flag(name: .shortAndLong, help: "Push all tags of an image") + var allTags: Bool = false + @OptionGroup public var logOptions: Flags.Logging @@ -54,10 +62,30 @@ extension Application { public init() {} + public func validate() throws { + if allTags { + let ref = try Reference.parse(reference) + if ref.tag != nil { + throw ContainerizationError(.invalidArgument, message: "tag can't be used with --all-tags/-a") + } + if ref.digest != nil { + throw ContainerizationError(.invalidArgument, message: "digest can't be used with --all-tags/-a") + } + } + } + public func run() async throws { let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log) - let scheme = try RequestScheme(registry.scheme) + + if allTags { + try await pushAllTags(platform: p, scheme: scheme) + } else { + try await pushSingle(platform: p, scheme: scheme) + } + } + + private func pushSingle(platform: Platform?, scheme: RequestScheme) async throws { let image = try await ClientImage.get(reference: reference) var progressConfig: ProgressConfig @@ -78,8 +106,71 @@ extension Application { progress.finish() } progress.start() - _ = try await image.push(platform: p, scheme: scheme, progressUpdate: progress.handler) + try await image.push(platform: platform, scheme: scheme, progressUpdate: progress.handler) + progress.finish() + } + + private func pushAllTags(platform: Platform?, scheme: RequestScheme) async throws { + if self.platform != nil || arch != nil || os != nil { + log.warning("--platform/--arch/--os with --all-tags filters each tag push to the specified platform; tags without matching manifests may fail") + } + + // Enumerate matching tags for display before pushing. + let allImages = try await ClientImage.list() + let normalized = try ClientImage.normalizeReference(reference) + let parsedRef = try Reference.parse(normalized) + let repoName: String + if let resolved = parsedRef.resolvedDomain { + repoName = "\(resolved)/\(parsedRef.path)" + } else { + repoName = parsedRef.name + } + + let matchingTags = allImages.filter { img in + guard !Utility.isInfraImage(name: img.reference) else { return false } + guard let ref = try? Reference.parse(img.reference) else { return false } + let resolvedName: String + if let resolved = ref.resolvedDomain { + resolvedName = "\(resolved)/\(ref.path)" + } else { + resolvedName = ref.name + } + return resolvedName == repoName + } + + let displayRepo = try ClientImage.denormalizeReference(normalized) + let displayName = try Reference.parse(displayRepo).name + print("The push refers to repository [\(displayName)]") + + var progressConfig: ProgressConfig + switch self.progressFlags.progress { + case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) + case .ansi: + progressConfig = try ProgressConfig( + description: "Pushing \(matchingTags.count) tags", + showPercent: false, + showItems: false, + showSpeed: false, + ignoreSmallSize: true + ) + } + + let progress = ProgressBar(config: progressConfig) + defer { + progress.finish() + } + progress.start() + try await ClientImage.pushAllTags( + reference: reference, platform: platform, scheme: scheme, + maxConcurrentUploads: imageUploadFlags.maxConcurrentUploads, progressUpdate: progress.handler) progress.finish() + + let formatter = ByteCountFormatter() + for img in matchingTags { + let tag = (try? Reference.parse(img.reference))?.tag ?? "" + let size = formatter.string(fromByteCount: img.descriptor.size) + print("\(tag): digest: \(img.descriptor.digest) size: \(size)") + } } } } diff --git a/Sources/Services/ContainerAPIService/Client/ClientImage.swift b/Sources/Services/ContainerAPIService/Client/ClientImage.swift index 42b242c8b..b3c1eb222 100644 --- a/Sources/Services/ContainerAPIService/Client/ClientImage.swift +++ b/Sources/Services/ContainerAPIService/Client/ClientImage.swift @@ -283,6 +283,48 @@ extension ClientImage { return image } + public static func pushAllTags( + reference: String, platform: Platform? = nil, scheme: RequestScheme = .auto, maxConcurrentUploads: Int = 3, progressUpdate: ProgressUpdateHandler? = nil + ) async throws { + guard maxConcurrentUploads > 0 else { + throw ContainerizationError(.invalidArgument, message: "maximum number of concurrent uploads must be greater than 0, got \(maxConcurrentUploads)") + } + + // Normalize the reference, then extract the repository name without the tag. + let normalized = try Self.normalizeReference(reference) + let parsedRef = try Reference.parse(normalized) + + let repositoryName: String + if let resolved = parsedRef.resolvedDomain { + repositoryName = "\(resolved)/\(parsedRef.path)" + } else { + repositoryName = parsedRef.name + } + + guard let host = parsedRef.domain else { + throw ContainerizationError(.invalidArgument, message: "could not extract host from reference \(normalized)") + } + + let client = newXPCClient() + let request = newRequest(.imagePush) + + request.set(key: .imageRepository, value: repositoryName) + request.set(key: .allTags, value: true) + try request.set(platform: platform) + + let insecure = try scheme.schemeFor(host: host) == .http + request.set(key: .insecureFlag, value: insecure) + request.set(key: .maxConcurrentUploads, value: Int64(maxConcurrentUploads)) + + var progressUpdateClient: ProgressUpdateClient? + if let progressUpdate { + progressUpdateClient = await ProgressUpdateClient(for: progressUpdate, request: request) + } + + _ = try await client.send(request) + await progressUpdateClient?.finish() + } + public static func delete(reference: String, garbageCollect: Bool = false) async throws { let client = newXPCClient() let request = newRequest(.imageDelete) diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index 88de209f9..6a7d97960 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -353,4 +353,15 @@ public struct Flags { @Option(name: .long, help: "Maximum number of concurrent downloads (default: 3)") public var maxConcurrentDownloads: Int = 3 } + + public struct ImageUpload: ParsableArguments { + public init() {} + + public init(maxConcurrentUploads: Int) { + self.maxConcurrentUploads = maxConcurrentUploads + } + + @Option(name: .long, help: ArgumentHelp("Maximum number of concurrent uploads with --all-tags", valueName: "count")) + public var maxConcurrentUploads: Int = 3 + } } diff --git a/Sources/Services/ContainerImagesService/Client/ImageServiceXPCKeys.swift b/Sources/Services/ContainerImagesService/Client/ImageServiceXPCKeys.swift index c087f99f1..d40402a52 100644 --- a/Sources/Services/ContainerImagesService/Client/ImageServiceXPCKeys.swift +++ b/Sources/Services/ContainerImagesService/Client/ImageServiceXPCKeys.swift @@ -36,6 +36,9 @@ public enum ImagesServiceXPCKeys: String { case insecureFlag case garbageCollect case maxConcurrentDownloads + case maxConcurrentUploads + case allTags + case imageRepository case forceLoad case rejectedMembers diff --git a/Sources/Services/ContainerImagesService/Server/ImagesService.swift b/Sources/Services/ContainerImagesService/Server/ImagesService.swift index 2fb06ed1c..5eddeebb4 100644 --- a/Sources/Services/ContainerImagesService/Server/ImagesService.swift +++ b/Sources/Services/ContainerImagesService/Server/ImagesService.swift @@ -136,6 +136,92 @@ public actor ImagesService { } } + public func pushAllTags(repositoryName: String, platform: Platform?, insecure: Bool, maxConcurrentUploads: Int, progressUpdate: ProgressUpdateHandler?) async throws { + self.log.debug( + "ImagesService: enter", + metadata: [ + "func": "\(#function)", + "repositoryName": "\(repositoryName)", + "platform": "\(String(describing: platform))", + "insecure": "\(insecure)", + "maxConcurrentUploads": "\(maxConcurrentUploads)", + ] + ) + defer { + self.log.debug( + "ImagesService: exit", + metadata: [ + "func": "\(#function)", + "repositoryName": "\(repositoryName)", + "platform": "\(String(describing: platform))", + ] + ) + } + + let allImages = try await imageStore.list() + let matchingImages = allImages.filter { image in + guard !Utility.isInfraImage(name: image.reference) else { return false } + guard let ref = try? Reference.parse(image.reference) else { return false } + let resolvedName: String + if let resolved = ref.resolvedDomain { + resolvedName = "\(resolved)/\(ref.path)" + } else { + resolvedName = ref.name + } + return resolvedName == repositoryName + } + + guard !matchingImages.isEmpty else { + throw ContainerizationError(.notFound, message: "no tags found for repository \(repositoryName)") + } + + let maxConcurrent = maxConcurrentUploads > 0 ? maxConcurrentUploads : 3 + + try await Self.withAuthentication(ref: repositoryName) { auth in + let progress = ContainerizationProgressAdapter.handler(from: progressUpdate) + var iterator = matchingImages.makeIterator() + var failures: [(reference: String, message: String)] = [] + + await withTaskGroup(of: (String, String?).self) { group in + for _ in 0.. ImageDescription { self.log.debug( "ImagesService: enter", diff --git a/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift b/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift index e88b2368f..630f8cac8 100644 --- a/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift +++ b/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift @@ -62,22 +62,37 @@ public struct ImagesServiceHarness: Sendable { @Sendable public func push(_ message: XPCMessage) async throws -> XPCMessage { - let ref = message.string(key: .imageReference) - guard let ref else { - throw ContainerizationError( - .invalidArgument, - message: "missing image reference" - ) - } let platformData = message.dataNoCopy(key: .ociPlatform) var platform: Platform? = nil if let platformData { platform = try JSONDecoder().decode(ContainerizationOCI.Platform.self, from: platformData) } let insecure = message.bool(key: .insecureFlag) + let allTags = message.bool(key: .allTags) let progressUpdateService = ProgressUpdateService(message: message) - try await service.push(reference: ref, platform: platform, insecure: insecure, progressUpdate: progressUpdateService?.handler) + if allTags { + let repository = message.string(key: .imageRepository) + guard let repository else { + throw ContainerizationError( + .invalidArgument, + message: "missing image repository" + ) + } + let maxConcurrentUploads = message.int64(key: .maxConcurrentUploads) + try await service.pushAllTags( + repositoryName: repository, platform: platform, insecure: insecure, + maxConcurrentUploads: Int(maxConcurrentUploads), progressUpdate: progressUpdateService?.handler) + } else { + let ref = message.string(key: .imageReference) + guard let ref else { + throw ContainerizationError( + .invalidArgument, + message: "missing image reference" + ) + } + try await service.push(reference: ref, platform: platform, insecure: insecure, progressUpdate: progressUpdateService?.handler) + } let reply = message.reply() return reply diff --git a/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift b/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift index a2b71c20e..70c65d956 100644 --- a/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift +++ b/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift @@ -311,6 +311,49 @@ class TestCLIImagesCommand: CLITest { "Expected validation error message in output") } + @Test func testAllTagsRejectsTaggedReference() throws { + let (_, _, error, status) = try run(arguments: [ + "image", + "push", + "--all-tags", + "alpine:latest", + ]) + + #expect(status != 0, "Expected --all-tags with a tag to fail") + #expect( + error.contains("tag can't be used with --all-tags/-a"), + "Expected tag validation error message in output") + } + + @Test func testAllTagsRejectsDigestReference() throws { + let (_, _, error, status) = try run(arguments: [ + "image", + "push", + "--all-tags", + "alpine@sha256:0000000000000000000000000000000000000000000000000000000000000000", + ]) + + #expect(status != 0, "Expected --all-tags with a digest to fail") + #expect( + error.contains("digest can't be used with --all-tags/-a"), + "Expected digest validation error message in output") + } + + @Test func testMaxConcurrentUploadsValidation() throws { + let (_, _, error, status) = try run(arguments: [ + "image", + "push", + "--all-tags", + "--max-concurrent-uploads", "0", + "alpine", + ]) + + #expect(status != 0, "Expected command to fail with maxConcurrentUploads=0") + #expect( + error.contains("maximum number of concurrent uploads must be greater than 0"), + "Expected validation error message in output") + } + @Test func testImageLoadRejectsInvalidMembersWithoutForce() throws { do { // 0. Generate unique malicious filename for this test run From 7df60be81713bab8144b3dc5b8bfa06868513687 Mon Sep 17 00:00:00 2001 From: Raj Date: Fri, 20 Mar 2026 13:14:45 -0700 Subject: [PATCH 2/3] Update command ref doc --- docs/command-reference.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/command-reference.md b/docs/command-reference.md index 7aea2a329..a9e379a43 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -522,18 +522,20 @@ Pushes an image to a registry. The flags mirror those for `image pull` with the **Usage** ```bash -container image push [--scheme ] [--progress ] [--arch ] [--os ] [--platform ] [--debug] +container image push [--scheme ] [--progress ] [--arch ] [--os ] [--platform ] [--all-tags] [--max-concurrent-uploads ] [--debug] ``` **Arguments** -* ``: Image reference to push +* ``: Image reference to push. When using `--all-tags`, this should be a repository name without a tag. **Options** * `--scheme `: Scheme to use when connecting to the container registry. One of (http, https, auto) (default: auto) * `--progress `: Progress type (format: none|ansi) (default: ansi) -* `-a, --arch `: Limit the push to the specified architecture +* `-a, --all-tags`: Push all tags of an image +* `--max-concurrent-uploads `: Maximum number of concurrent uploads with --all-tags (default: 3) +* `--arch `: Limit the push to the specified architecture * `--os `: Limit the push to the specified OS * `--platform `: Limit the push to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch) From 06777c34cbe98ea504064a57833802df08a27a63 Mon Sep 17 00:00:00 2001 From: Raj Date: Mon, 23 Mar 2026 15:00:15 -0700 Subject: [PATCH 3/3] return pushed images from service, drop -a short flag --- .../ContainerCommands/Image/ImagePush.swift | 36 ++++--------------- .../Client/ClientImage.swift | 10 ++++-- .../Server/ImagesService.swift | 6 +++- .../Server/ImagesServiceHarness.swift | 7 +++- .../Images/TestCLIImagesCommand.swift | 4 +-- docs/command-reference.md | 4 +-- 6 files changed, 30 insertions(+), 37 deletions(-) diff --git a/Sources/ContainerCommands/Image/ImagePush.swift b/Sources/ContainerCommands/Image/ImagePush.swift index 94ab14c55..9e58b165a 100644 --- a/Sources/ContainerCommands/Image/ImagePush.swift +++ b/Sources/ContainerCommands/Image/ImagePush.swift @@ -39,7 +39,7 @@ extension Application { var imageUploadFlags: Flags.ImageUpload @Option( - name: .long, + name: .shortAndLong, help: "Limit the push to the specified architecture" ) var arch: String? @@ -52,7 +52,7 @@ extension Application { @Option(help: "Limit the push to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch) [environment: CONTAINER_DEFAULT_PLATFORM]") var platform: String? - @Flag(name: .shortAndLong, help: "Push all tags of an image") + @Flag(name: .long, help: "Push all tags of an image") var allTags: Bool = false @OptionGroup @@ -66,10 +66,10 @@ extension Application { if allTags { let ref = try Reference.parse(reference) if ref.tag != nil { - throw ContainerizationError(.invalidArgument, message: "tag can't be used with --all-tags/-a") + throw ContainerizationError(.invalidArgument, message: "tag can't be used with --all-tags") } if ref.digest != nil { - throw ContainerizationError(.invalidArgument, message: "digest can't be used with --all-tags/-a") + throw ContainerizationError(.invalidArgument, message: "digest can't be used with --all-tags") } } } @@ -115,29 +115,7 @@ extension Application { log.warning("--platform/--arch/--os with --all-tags filters each tag push to the specified platform; tags without matching manifests may fail") } - // Enumerate matching tags for display before pushing. - let allImages = try await ClientImage.list() let normalized = try ClientImage.normalizeReference(reference) - let parsedRef = try Reference.parse(normalized) - let repoName: String - if let resolved = parsedRef.resolvedDomain { - repoName = "\(resolved)/\(parsedRef.path)" - } else { - repoName = parsedRef.name - } - - let matchingTags = allImages.filter { img in - guard !Utility.isInfraImage(name: img.reference) else { return false } - guard let ref = try? Reference.parse(img.reference) else { return false } - let resolvedName: String - if let resolved = ref.resolvedDomain { - resolvedName = "\(resolved)/\(ref.path)" - } else { - resolvedName = ref.name - } - return resolvedName == repoName - } - let displayRepo = try ClientImage.denormalizeReference(normalized) let displayName = try Reference.parse(displayRepo).name print("The push refers to repository [\(displayName)]") @@ -147,7 +125,7 @@ extension Application { case .none: progressConfig = try ProgressConfig(disableProgressUpdates: true) case .ansi: progressConfig = try ProgressConfig( - description: "Pushing \(matchingTags.count) tags", + description: "Pushing tags", showPercent: false, showItems: false, showSpeed: false, @@ -160,13 +138,13 @@ extension Application { progress.finish() } progress.start() - try await ClientImage.pushAllTags( + let pushed = try await ClientImage.pushAllTags( reference: reference, platform: platform, scheme: scheme, maxConcurrentUploads: imageUploadFlags.maxConcurrentUploads, progressUpdate: progress.handler) progress.finish() let formatter = ByteCountFormatter() - for img in matchingTags { + for img in pushed { let tag = (try? Reference.parse(img.reference))?.tag ?? "" let size = formatter.string(fromByteCount: img.descriptor.size) print("\(tag): digest: \(img.descriptor.digest) size: \(size)") diff --git a/Sources/Services/ContainerAPIService/Client/ClientImage.swift b/Sources/Services/ContainerAPIService/Client/ClientImage.swift index b3c1eb222..322a93ca3 100644 --- a/Sources/Services/ContainerAPIService/Client/ClientImage.swift +++ b/Sources/Services/ContainerAPIService/Client/ClientImage.swift @@ -283,9 +283,10 @@ extension ClientImage { return image } + @discardableResult public static func pushAllTags( reference: String, platform: Platform? = nil, scheme: RequestScheme = .auto, maxConcurrentUploads: Int = 3, progressUpdate: ProgressUpdateHandler? = nil - ) async throws { + ) async throws -> [ClientImage] { guard maxConcurrentUploads > 0 else { throw ContainerizationError(.invalidArgument, message: "maximum number of concurrent uploads must be greater than 0, got \(maxConcurrentUploads)") } @@ -321,8 +322,13 @@ extension ClientImage { progressUpdateClient = await ProgressUpdateClient(for: progressUpdate, request: request) } - _ = try await client.send(request) + let response = try await client.send(request) await progressUpdateClient?.finish() + + let imageDescriptions = try response.imageDescriptions() + return imageDescriptions.map { desc in + ClientImage(description: desc) + } } public static func delete(reference: String, garbageCollect: Bool = false) async throws { diff --git a/Sources/Services/ContainerImagesService/Server/ImagesService.swift b/Sources/Services/ContainerImagesService/Server/ImagesService.swift index 5eddeebb4..f96309fc4 100644 --- a/Sources/Services/ContainerImagesService/Server/ImagesService.swift +++ b/Sources/Services/ContainerImagesService/Server/ImagesService.swift @@ -136,7 +136,9 @@ public actor ImagesService { } } - public func pushAllTags(repositoryName: String, platform: Platform?, insecure: Bool, maxConcurrentUploads: Int, progressUpdate: ProgressUpdateHandler?) async throws { + public func pushAllTags(repositoryName: String, platform: Platform?, insecure: Bool, maxConcurrentUploads: Int, progressUpdate: ProgressUpdateHandler?) async throws + -> [ImageDescription] + { self.log.debug( "ImagesService: enter", metadata: [ @@ -220,6 +222,8 @@ public actor ImagesService { throw ContainerizationError(.internalError, message: "failed to push one or more tags:\n\(details)") } } + + return matchingImages.map { $0.description.fromCZ } } public func tag(old: String, new: String) async throws -> ImageDescription { diff --git a/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift b/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift index 630f8cac8..b7a589934 100644 --- a/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift +++ b/Sources/Services/ContainerImagesService/Server/ImagesServiceHarness.swift @@ -80,9 +80,14 @@ public struct ImagesServiceHarness: Sendable { ) } let maxConcurrentUploads = message.int64(key: .maxConcurrentUploads) - try await service.pushAllTags( + let pushed = try await service.pushAllTags( repositoryName: repository, platform: platform, insecure: insecure, maxConcurrentUploads: Int(maxConcurrentUploads), progressUpdate: progressUpdateService?.handler) + + let reply = message.reply() + let imageData = try JSONEncoder().encode(pushed) + reply.set(key: .imageDescriptions, value: imageData) + return reply } else { let ref = message.string(key: .imageReference) guard let ref else { diff --git a/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift b/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift index 70c65d956..8fba3a1be 100644 --- a/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift +++ b/Tests/CLITests/Subcommands/Images/TestCLIImagesCommand.swift @@ -321,7 +321,7 @@ class TestCLIImagesCommand: CLITest { #expect(status != 0, "Expected --all-tags with a tag to fail") #expect( - error.contains("tag can't be used with --all-tags/-a"), + error.contains("tag can't be used with --all-tags"), "Expected tag validation error message in output") } @@ -335,7 +335,7 @@ class TestCLIImagesCommand: CLITest { #expect(status != 0, "Expected --all-tags with a digest to fail") #expect( - error.contains("digest can't be used with --all-tags/-a"), + error.contains("digest can't be used with --all-tags"), "Expected digest validation error message in output") } diff --git a/docs/command-reference.md b/docs/command-reference.md index a9e379a43..992a3a98a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -533,9 +533,9 @@ container image push [--scheme ] [--progress ] [--arch ] [-- * `--scheme `: Scheme to use when connecting to the container registry. One of (http, https, auto) (default: auto) * `--progress `: Progress type (format: none|ansi) (default: ansi) -* `-a, --all-tags`: Push all tags of an image +* `--all-tags`: Push all tags of an image * `--max-concurrent-uploads `: Maximum number of concurrent uploads with --all-tags (default: 3) -* `--arch `: Limit the push to the specified architecture +* `-a, --arch `: Limit the push to the specified architecture * `--os `: Limit the push to the specified OS * `--platform `: Limit the push to the specified platform (format: os/arch[/variant], takes precedence over --os and --arch)