Repository: canonical/charmcraft

Issue:
Add a --name parameter to upload command (to use a different name than the specified in metadata.yaml)
With the recent transition of the old charm store to charmhub all charm uploads now need to be handled. In our case charms are available to our customers through the old charm store URLs (e.g. `cs:~anbox-charmers/ams`) and the `metadata.yaml` file includes `name: ams`. Migration to charmhub flat names is in progress but we will have customers using the charm store URLs for some more time and need to make sure we continue to support them.

The problem now is that charmcraft forces us to rename the charm in `metadata.yaml` when we want to upload it to charmhub. WIth the import `cs:~anbox-charmers/ams` became `anbox-charmers-ams` on charmhub but the charm still contains `name: anbox` in `metadata.yaml` and has to continue to do so for backward compatibility and as we plan to move it in the near future to the flat name `ams` on charmhub.

The problem is specifically in the implementation of the upload command where it derives name to upload to on charmhub from the name set in `metadata.yaml` (see https://github.com/canonical/charmcraft/blob/main/charmcraft/commands/store/__init__.py#L64) which will fails as in our case it will derive `ams` rather than `anbox-charmers-ams` and with that will upload to the wrong charm or fail if permissions to upload to the `ams` are not given.

To workaround we added a `--name` argument to the upload command in a custom charmcraft we're using for the time being which allows to upload to a specific charm on charmhub which doesn't match the name specified in `metadata.yaml`.

It would be great if we can get this solved in charmcraft proper. 


Discussion:
hloeung: @facundobatista or anyone else from the team able to take a look?

Patch:
diff --git a/charmcraft/commands/store/__init__.py b/charmcraft/commands/store/__init__.py
index 186665ca..cd187442 100644
--- a/charmcraft/commands/store/__init__.py
+++ b/charmcraft/commands/store/__init__.py
@@ -472,6 +472,11 @@ class UploadCommand(BaseCommand):
             action="append",
             help="The channel(s) to release to (this option can be indicated multiple times)",
         )
+        parser.add_argument(
+            "--name",
+            type=str,
+            help="Name of the charm or bundle on Charmhub to upload to",
+        )
         parser.add_argument(
             "--resource",
             action="append",
@@ -507,7 +512,10 @@ class UploadCommand(BaseCommand):
 
     def run(self, parsed_args):
         """Run the command."""
-        name = get_name_from_zip(parsed_args.filepath)
+        if parsed_args.name:
+            name = parsed_args.name
+        else:
+            name = get_name_from_zip(parsed_args.filepath)
         self._validate_template_is_handled(parsed_args.filepath)
         store = Store(self.config.charmhub)
         result = store.upload(name, parsed_args.filepath)


Test patch:
diff --git a/tests/commands/test_store_commands.py b/tests/commands/test_store_commands.py
index a4547242..76f1309a 100644
--- a/tests/commands/test_store_commands.py
+++ b/tests/commands/test_store_commands.py
@@ -750,7 +750,7 @@ def test_upload_call_ok(emitter, store_mock, config, tmp_path):
 
     test_charm = tmp_path / "mystuff.charm"
     _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
-    args = Namespace(filepath=test_charm, release=[])
+    args = Namespace(filepath=test_charm, release=[], name=None)
     retcode = UploadCommand(config).run(args)
     assert retcode == 0
 
@@ -770,7 +770,7 @@ def test_upload_call_error(emitter, store_mock, config, tmp_path):
 
     test_charm = tmp_path / "mystuff.charm"
     _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
-    args = Namespace(filepath=test_charm, release=[])
+    args = Namespace(filepath=test_charm, release=[], name=None)
     retcode = UploadCommand(config).run(args)
     assert retcode == 1
 
@@ -790,7 +790,7 @@ def test_upload_call_ok_including_release(emitter, store_mock, config, tmp_path)
 
     test_charm = tmp_path / "mystuff.charm"
     _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
-    args = Namespace(filepath=test_charm, release=["edge"], resource=[])
+    args = Namespace(filepath=test_charm, release=["edge"], resource=[], name=None)
     UploadCommand(config).run(args)
 
     assert store_mock.mock_calls == [
@@ -811,7 +811,7 @@ def test_upload_call_ok_including_release_multiple(emitter, store_mock, config,
 
     test_charm = tmp_path / "mystuff.charm"
     _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
-    args = Namespace(filepath=test_charm, release=["edge", "stable"], resource=[])
+    args = Namespace(filepath=test_charm, release=["edge", "stable"], resource=[], name=None)
     UploadCommand(config).run(args)
 
     assert store_mock.mock_calls == [
@@ -834,7 +834,7 @@ def test_upload_including_release_with_resources(emitter, store_mock, config, tm
     _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
     r1 = ResourceOption(name="foo", revision=3)
     r2 = ResourceOption(name="bar", revision=17)
-    args = Namespace(filepath=test_charm, release=["edge"], resource=[r1, r2])
+    args = Namespace(filepath=test_charm, release=["edge"], resource=[r1, r2], name=None)
     UploadCommand(config).run(args)
 
     assert store_mock.mock_calls == [
@@ -865,7 +865,7 @@ def test_upload_call_error_including_release(emitter, store_mock, config, tmp_pa
 
     test_charm = tmp_path / "mystuff.charm"
     _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
-    args = Namespace(filepath=test_charm, release=["edge"])
+    args = Namespace(filepath=test_charm, release=["edge"], name=None)
     UploadCommand(config).run(args)
 
     # check the upload was attempted, but not the release!
@@ -883,7 +883,7 @@ def test_upload_charm_with_init_template_todo_token(tmp_path, config):
         zf.writestr("file_ok.cfg", b"This is fine :).")
         zf.writestr("othertainted.txt", b"# TEMPLATE-TODO: need to fix.")
 
-    args = Namespace(filepath=test_charm, release=[])
+    args = Namespace(filepath=test_charm, release=[], name=None)
     expected_msg = (
         "Cannot upload the charm as it include the following files with a leftover "
         "TEMPLATE-TODO token from when the project was created using the 'init' "
@@ -893,6 +893,22 @@ def test_upload_charm_with_init_template_todo_token(tmp_path, config):
         UploadCommand(config).run(args)
 
 
+def test_upload_with_different_name_than_in_metadata(emitter, store_mock, config, tmp_path):
+    """Simple upload to a specific name different from metadata, success result."""
+    store_response = Uploaded(ok=True, status=200, revision=7, errors=[])
+    store_mock.upload.return_value = store_response
+
+    test_charm = tmp_path / "mystuff.charm"
+    _build_zip_with_yaml(test_charm, "metadata.yaml", content={"name": "mycharm"})
+    args = Namespace(filepath=test_charm, release=[], name="foo-mycharm")
+    retcode = UploadCommand(config).run(args)
+    assert retcode == 0
+
+    assert store_mock.mock_calls == [call.upload("foo-mycharm", test_charm)]
+    expected = "Revision 7 of 'foo-mycharm' created"
+    emitter.assert_message(expected)
+
+
 # -- tests for list revisions command