Repository: reanahub/reana-client

Issue:
upload REANA specification file to server by default
Originated in https://github.com/reanahub/reana-client/issues/620#issuecomment-1211638448 (point 1)

We do not upload the REANA specification files to the server. Having REANA specifications in the workspace will allow for easier debugs and more reproducibility (if we decide to export workflows to, for example, Zenodo).

The focus of this issue is to upload specifications by default in reana-client. This will help with transitioning to https://github.com/reanahub/reana-db/issues/162.

In addition, we can also add a validation step to reana-server to check if reana.yaml is uploaded.

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 49ea1c7..c134624 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,7 @@ Version 0.9.0 (UNRELEASED)
 - Adds support for ``.reanaignore`` during file upload. Files that match ``.reanaignore`` will not be uploaded.
 - Adds support for ``.gitignore`` during file upload. Files that match ``.gitignore`` will not be uploaded.
 - Adds ``retention-rules-list`` command to list the retention rules of a workflow.
+- Changes ``create`` and ``restart`` commands to always upload REANA specification file.
 - Changes ``delete`` CLI command to always delete workspace.
 - Changes ``delete_workflow`` API method to always delete workspace.
 - Changes ``list`` to hide deleted workflows by default.
diff --git a/reana_client/cli/workflow.py b/reana_client/cli/workflow.py
index 80f1089..4dfcb48 100644
--- a/reana_client/cli/workflow.py
+++ b/reana_client/cli/workflow.py
@@ -356,19 +356,23 @@ def workflow_create(ctx, file, name, skip_validation, access_token):  # noqa: D3
         display_message("Workflow name cannot be a valid UUIDv4", msg_type="error")
         sys.exit(1)
 
+    specification_filename = click.format_filename(file)
+
     try:
         reana_specification = load_validate_reana_spec(
-            click.format_filename(file),
+            specification_filename,
             access_token=access_token,
             skip_validation=skip_validation,
             server_capabilities=True,
         )
         logging.info("Connecting to {0}".format(get_api_url()))
         response = create_workflow(reana_specification, name, access_token)
-        click.echo(click.style(response["workflow_name"], fg="green"))
+        workflow_name = response["workflow_name"]
+
+        click.echo(click.style(workflow_name, fg="green"))
         # check if command is called from wrapper command
         if "invoked_by_subcommand" in ctx.parent.__dict__:
-            ctx.parent.workflow_name = response["workflow_name"]
+            ctx.parent.workflow_name = workflow_name
     except Exception as e:
         logging.debug(traceback.format_exc())
         logging.debug(str(e))
@@ -377,6 +381,14 @@ def workflow_create(ctx, file, name, skip_validation, access_token):  # noqa: D3
         )
         sys.exit(1)
 
+    # upload specification file by default
+    ctx.invoke(
+        upload_files,
+        workflow=workflow_name,
+        filenames=(specification_filename,),
+        access_token=access_token,
+    )
+
 
 @workflow_execution_group.command("start")
 @add_workflow_option
@@ -578,61 +590,70 @@ def workflow_restart(
         "restart": True,
     }
     if file:
+        specification_filename = click.format_filename(file)
         parsed_parameters["reana_specification"] = load_validate_reana_spec(
             click.format_filename(file)
         )
-    if workflow:
-        if parameters or options:
-            try:
-                if "reana_specification" in parsed_parameters:
-                    workflow_type = parsed_parameters["reana_specification"][
-                        "workflow"
-                    ]["type"]
-                    original_parameters = (
-                        parsed_parameters["reana_specification"]
-                        .get("inputs", {})
-                        .get("parameters", {})
-                    )
-                else:
-                    response = get_workflow_parameters(workflow, access_token)
-                    workflow_type = response["type"]
-                    original_parameters = response["parameters"]
+        # upload new specification
+        ctx.invoke(
+            upload_files,
+            workflow=workflow,
+            filenames=(specification_filename,),
+            access_token=access_token,
+        )
 
-                parsed_parameters["operational_options"] = validate_operational_options(
-                    workflow_type, parsed_parameters["operational_options"]
-                )
-                parsed_parameters["input_parameters"] = validate_input_parameters(
-                    parsed_parameters["input_parameters"], original_parameters
+    if parameters or options:
+        try:
+            if "reana_specification" in parsed_parameters:
+                workflow_type = parsed_parameters["reana_specification"]["workflow"][
+                    "type"
+                ]
+                original_parameters = (
+                    parsed_parameters["reana_specification"]
+                    .get("inputs", {})
+                    .get("parameters", {})
                 )
+            else:
+                response = get_workflow_parameters(workflow, access_token)
+                workflow_type = response["type"]
+                original_parameters = response["parameters"]
 
-            except REANAValidationError as e:
-                display_message(e.message, msg_type="error")
-                sys.exit(1)
-            except Exception as e:
-                display_message(
-                    "Could not apply given input parameters: "
-                    "{0} \n{1}".format(parameters, str(e)),
-                    msg_type="error",
-                )
-                sys.exit(1)
-        try:
-            logging.info("Connecting to {0}".format(get_api_url()))
-            response = start_workflow(workflow, access_token, parsed_parameters)
-            workflow = response["workflow_name"] + "." + str(response["run_number"])
-            current_status = get_workflow_status(workflow, access_token).get("status")
-            display_message(
-                get_workflow_status_change_msg(workflow, current_status),
-                msg_type="success",
+            parsed_parameters["operational_options"] = validate_operational_options(
+                workflow_type, parsed_parameters["operational_options"]
+            )
+            parsed_parameters["input_parameters"] = validate_input_parameters(
+                parsed_parameters["input_parameters"], original_parameters
             )
+
+        except REANAValidationError as e:
+            display_message(e.message, msg_type="error")
+            sys.exit(1)
         except Exception as e:
-            logging.debug(traceback.format_exc())
-            logging.debug(str(e))
             display_message(
-                "Cannot start workflow {}: \n{}".format(workflow, str(e)),
+                "Could not apply given input parameters: "
+                "{0} \n{1}".format(parameters, str(e)),
                 msg_type="error",
             )
             sys.exit(1)
 
+    try:
+        logging.info("Connecting to {0}".format(get_api_url()))
+        response = start_workflow(workflow, access_token, parsed_parameters)
+        workflow = response["workflow_name"] + "." + str(response["run_number"])
+        current_status = get_workflow_status(workflow, access_token).get("status")
+        display_message(
+            get_workflow_status_change_msg(workflow, current_status),
+            msg_type="success",
+        )
+    except Exception as e:
+        logging.debug(traceback.format_exc())
+        logging.debug(str(e))
+        display_message(
+            "Cannot start workflow {}: \n{}".format(workflow, str(e)),
+            msg_type="error",
+        )
+        sys.exit(1)
+
 
 @workflow_execution_group.command("status")
 @add_workflow_option


Test patch:
diff --git a/tests/test_cli_workflows.py b/tests/test_cli_workflows.py
index c3ddd24..1983141 100644
--- a/tests/test_cli_workflows.py
+++ b/tests/test_cli_workflows.py
@@ -485,7 +485,7 @@ def test_workflow_create_successful(create_yaml_workflow_schema):
         with patch(
             "reana_client.api.client.current_rs_api_client",
             make_mock_api_client("reana-server")(mock_response, mock_http_response),
-        ):
+        ), patch("reana_client.api.client.requests.post") as upload_request:
             with runner.isolated_filesystem():
                 with open("reana.yaml", "w") as f:
                     f.write(create_yaml_workflow_schema)
@@ -495,6 +495,9 @@ def test_workflow_create_successful(create_yaml_workflow_schema):
                 assert result.exit_code == 0
                 assert response["workflow_name"] in result.output
 
+                upload_request.assert_called_once()
+                assert "File /reana.yaml was successfully uploaded." in result.output
+
 
 def test_workflow_create_not_valid_name(create_yaml_workflow_schema):
     """Test workflow create when creation is successfull."""