Repository: conan-io/conan

Issue:
[bug] conan upload --list errors on empty recipe list
### Environment details

* Operating System+version:
Manjaro Linux 23.0.0

* Compiler+version:
N/A

* Conan version:
Conan version 2.0.7

* Python version:
Python 3.11.3


### Steps to reproduce

1. Create a list file as follows (can be generated from something like `conan list --graph install_graph.json --graph-binaries build --format json`)
```json
{
  "Local Cache": {}
}
```
2. conan upload --remote foo --list list.json
3. Conditional check fails: https://github.com/conan-io/conan/blob/release/2.0/conan/cli/commands/upload.py#L84-L85

Note that I believe this should work since some automation workflows in CI may result in an empty list if a specific package was just cached and `--graph-binaries build` is set in the list command. I think the conditional check listed below should probably only error if the `--list` argument was not provided.

### Logs

```
$> conan upload --remote foo --list list.json
ERROR: No recipes found matching pattern 'None'
```


Discussion:
Hi @n-morales, thanks a lot for your report and taking the time to diagnose the error!

What you describe seems like a valid use case to me, if you would like to contribute a PR now that you have checked the code, we can discuss it there, but no worries if not :)
(I can't promise it would get merged if the team thinks otherwise of the error, but the discussion is at least worth having, so thanks for bringing it up!)
I think that for automation purposes, allowing an empty list makes sense. So this could be changed, and relaxed for the ``--list`` argument. It sounds that an info/warning message would be useful, I'd add it too.

So @n-morales would you like to contribute it as you as @RubenRBS suggested? Otherwise, please tell us and we will do it, no problem at all. Thanks!
@RubenRBS @memsharded I could do it -- do you have suggestions for where I should be putting a unit test for it?
Probably https://github.com/conan-io/conan/blob/release/2.0/conans/test/integration/command_v2/test_combined_pkglist_flows.py#L9 is the best, as it contains other ``upload --list`` tests. But no need to reuse the fixture with previous data, 

Patch:
diff --git a/conan/cli/commands/upload.py b/conan/cli/commands/upload.py
--- a/conan/cli/commands/upload.py
+++ b/conan/cli/commands/upload.py
@@ -1,6 +1,6 @@
 from conan.api.conan_api import ConanAPI
 from conan.api.model import ListPattern, MultiPackagesList
-from conan.api.output import cli_out_write
+from conan.api.output import cli_out_write, ConanOutput
 from conan.cli import make_abs_path
 from conan.cli.command import conan_command, OnceArgument
 from conan.cli.commands.list import print_list_json, print_serial
@@ -81,24 +81,28 @@ def upload(conan_api: ConanAPI, parser, *args):
         ref_pattern = ListPattern(args.pattern, package_id="*", only_recipe=args.only_recipe)
         package_list = conan_api.list.select(ref_pattern, package_query=args.package_query)
 
-    if not package_list.recipes:
+    if package_list.recipes:
+        if args.check:
+            conan_api.cache.check_integrity(package_list)
+        # Check if the recipes/packages are in the remote
+        conan_api.upload.check_upstream(package_list, remote, args.force)
+
+        # If only if search with "*" we ask for confirmation
+        if not args.list and not args.confirm and "*" in args.pattern:
+            _ask_confirm_upload(conan_api, package_list)
+
+        conan_api.upload.prepare(package_list, enabled_remotes, args.metadata)
+
+        if not args.dry_run:
+            conan_api.upload.upload(package_list, remote)
+            conan_api.upload.upload_backup_sources(package_list)
+    elif args.list:
+        # Don't error on no recipes for automated workflows using list,
+        # but warn to tell the user that no packages were uploaded
+        ConanOutput().warning(f"No packages were uploaded because the package list is empty.")
+    else:
         raise ConanException("No recipes found matching pattern '{}'".format(args.pattern))
 
-    if args.check:
-        conan_api.cache.check_integrity(package_list)
-    # Check if the recipes/packages are in the remote
-    conan_api.upload.check_upstream(package_list, remote, args.force)
-
-    # If only if search with "*" we ask for confirmation
-    if not args.list and not args.confirm and "*" in args.pattern:
-        _ask_confirm_upload(conan_api, package_list)
-
-    conan_api.upload.prepare(package_list, enabled_remotes, args.metadata)
-
-    if not args.dry_run:
-        conan_api.upload.upload(package_list, remote)
-        conan_api.upload.upload_backup_sources(package_list)
-
     pkglist = MultiPackagesList()
     pkglist.add(remote.name, package_list)
     return {


Test patch:
diff --git a/conans/test/integration/command/upload/upload_test.py b/conans/test/integration/command/upload/upload_test.py
--- a/conans/test/integration/command/upload/upload_test.py
+++ b/conans/test/integration/command/upload/upload_test.py
@@ -99,6 +99,12 @@ def test_pattern_upload(self):
         assert "Uploading recipe 'hello0/1.2.1@" in client.out
         assert "Uploading package 'hello0/1.2.1@" in client.out
 
+    def test_pattern_upload_no_recipes(self):
+        client = TestClient(default_server_user=True)
+        client.save({"conanfile.py": conanfile})
+        client.run("upload bogus/*@dummy/testing --confirm -r default", assert_error=True)
+        self.assertIn("No recipes found matching pattern 'bogus/*@dummy/testing'", client.out)
+
     def test_broken_sources_tgz(self):
         # https://github.com/conan-io/conan/issues/2854
         client = TestClient(default_server_user=True)
diff --git a/conans/test/integration/command_v2/test_combined_pkglist_flows.py b/conans/test/integration/command_v2/test_combined_pkglist_flows.py
--- a/conans/test/integration/command_v2/test_combined_pkglist_flows.py
+++ b/conans/test/integration/command_v2/test_combined_pkglist_flows.py
@@ -37,6 +37,19 @@ def test_list_upload_packages(self, client):
             assert f"Uploading recipe '{r}'" in client.out
         assert str(client.out).count("Uploading package") == 2
 
+    def test_list_upload_empty_list(self, client):
+        client.run(f"install --requires=zlib/1.0.0@user/channel -f json",
+                   redirect_stdout="install_graph.json")
+
+        # Generate an empty pkglist.json
+        client.run(f"list --format=json --graph=install_graph.json --graph-binaries=bogus",
+                   redirect_stdout="pkglist.json")
+
+        # No binaries should be uploaded since the pkglist is empty, but the command
+        # should not error
+        client.run("upload --list=pkglist.json -r=default")
+        assert "No packages were uploaded because the package list is empty." in client.out
+
 
 class TestGraphCreatedUpload:
     def test_create_upload(self):