Repository: conda/conda-build

Issue:
anaconda_upload option automatically uses the --force switch
conda-build 2.1.0 introduced a change where 'anaconda upload' is now forced. https://github.com/conda/conda-build/blob/master/conda_build/build.py#L1605. I don't see any kind of config setting that can alter the behavior. That line was changed in tag 2.1.0.

This seems a bit dangerous since it will blow away the package that's already there. We're normally pretty careful not to accidentally rebuild a package with the same version/build-string, but, sometimes it happens inadvertently. We drive conda build through a higher-level build process which is necessary to compile our internal libraries and handles much of the cross-company dependency management. After the compilation, conda build is driven and environment variables are set based on that outer build system's notion of the version. It's easy to forget to bump the version and accidentally rebuild with an existing version/build-string. 

The old behavior seems better: the upload fails because there's an existing instance of the package with the same version/build-string, but, there are instructions for how to upload manually if you really intended to do that.

Can we add a new config option to disable the --force flag on anaconda upload?


Discussion:
Yes, I'm happy to add this flag.  I need to keep the behavior the same, at least for the 2.1.x series, but I'm quite happy to give you an easy way to change it.

Resolving patch:
diff --git a/conda_build/build.py b/conda_build/build.py
--- a/conda_build/build.py
+++ b/conda_build/build.py
@@ -1587,14 +1587,11 @@ def handle_anaconda_upload(paths, config):
     upload = False
     # this is the default, for no explicit argument.
     # remember that anaconda_upload takes defaults from condarc
-    if config.anaconda_upload is None:
-        pass
-    elif config.token or config.user:
+    if config.token or config.user:
         upload = True
     # rc file has uploading explicitly turned off
-    elif config.anaconda_upload is False:
+    elif not config.anaconda_upload:
         print("# Automatic uploading is disabled")
-        upload = False
     else:
         upload = True
 
@@ -1626,7 +1623,9 @@ def handle_anaconda_upload(paths, config):
 
     if config.token:
         cmd.extend(['--token', config.token])
-    cmd.extend(['upload', '--force'])
+    cmd.append('upload')
+    if config.force_upload:
+        cmd.append('--force')
     if config.user:
         cmd.extend(['--user', config.user])
     for package in paths:
diff --git a/conda_build/cli/main_build.py b/conda_build/cli/main_build.py
--- a/conda_build/cli/main_build.py
+++ b/conda_build/cli/main_build.py
@@ -135,6 +135,13 @@ def parse_args(args):
         '--user',
         help="User/organization to upload packages to on anaconda.org or pypi"
     )
+    p.add_argument(
+        '--no-force-upload',
+        help="Disable force upload to anaconda.org, preventing overwriting any existing packages",
+        dest='force_upload',
+        default=True,
+        action='store_false',
+    )
     pypi_grp = p.add_argument_group("PyPI upload parameters (twine)")
     pypi_grp.add_argument(
         '--password',
diff --git a/conda_build/config.py b/conda_build/config.py
--- a/conda_build/config.py
+++ b/conda_build/config.py
@@ -91,6 +91,7 @@ def env(lang, default):
         Setting = namedtuple("ConfigSetting", "name, default")
         values = [Setting('activate', True),
                   Setting('anaconda_upload', binstar_upload),
+                  Setting('force_upload', cc_conda_build.get('force_upload', True)),
                   Setting('channel_urls', []),
                   Setting('dirty', False),
                   Setting('include_recipe', True),


Test patch:
diff --git a/tests/test_api_build.py b/tests/test_api_build.py
--- a/tests/test_api_build.py
+++ b/tests/test_api_build.py
@@ -14,7 +14,8 @@
 # for version
 import conda
 
-from conda_build.conda_interface import PY3, url_path
+from conda_build.conda_interface import PY3, url_path, cc_conda_build
+import conda_build
 
 from binstar_client.commands import remove, show
 from binstar_client.errors import NotFound
@@ -920,3 +921,16 @@ def test_no_locking(test_config):
     recipe = os.path.join(metadata_dir, 'source_git_jinja2')
     api.update_index(os.path.join(test_config.croot, test_config.subdir), config=test_config)
     api.build(recipe, config=test_config, locking=False)
+
+
+def test_no_force_upload_condarc_setting(mocker, testing_workdir, test_metadata):
+    test_metadata.config.anaconda_upload = True
+    del test_metadata.meta['test']
+    api.output_yaml(test_metadata, 'meta.yaml')
+    call = mocker.patch.object(conda_build.build.subprocess, 'call')
+    cc_conda_build['force_upload'] = False
+    pkg = api.build(testing_workdir)
+    assert call.called_once_with(['anaconda', 'upload', pkg])
+    del cc_conda_build['force_upload']
+    pkg = api.build(testing_workdir)
+    assert call.called_once_with(['anaconda', 'upload', '--force', pkg])
diff --git a/tests/test_cli.py b/tests/test_cli.py
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,14 +11,14 @@
 import pytest
 
 import conda
-from conda_build.conda_interface import download
+from conda_build.conda_interface import download, reset_context
 from conda_build.tarcheck import TarCheck
 
 from conda_build import api
 from conda_build.utils import (get_site_packages, on_win, get_build_folders, package_has_file,
                                check_call_env, tar_xf)
 from conda_build.conda_interface import TemporaryDirectory
-from conda_build.environ import get_py_ver
+import conda_build
 from .utils import metadata_dir, put_bad_conda_on_path
 
 import conda_build.cli.main_build as main_build
@@ -441,3 +441,23 @@ def test_purge_all(test_metadata):
     main_build.execute(args)
     assert not get_build_folders(test_metadata.config.croot)
     assert not os.path.isfile(fn)
+
+
+def test_no_force_upload(mocker, testing_workdir, test_metadata):
+    with open(os.path.join(testing_workdir, '.condarc'), 'w') as f:
+        f.write('anaconda_upload: True\n')
+        f.write('conda_build:\n')
+        f.write('    force_upload: False\n')
+    del test_metadata.meta['test']
+    api.output_yaml(test_metadata, 'meta.yaml')
+    args = ['--no-force-upload', testing_workdir]
+    call = mocker.patch.object(conda_build.build.subprocess, 'call')
+    reset_context(testing_workdir)
+    main_build.execute(args)
+    pkg = api.get_output_file_path(test_metadata)
+    assert call.called_once_with(['anaconda', 'upload', pkg])
+    args = [testing_workdir]
+    with open(os.path.join(testing_workdir, '.condarc'), 'w') as f:
+        f.write('anaconda_upload: True\n')
+    main_build.execute(args)
+    assert call.called_once_with(['anaconda', 'upload', '--force', pkg])