Repository: conda-forge/conda-smithy

Issue:
Missing list delimiter in maintainers list breaks things
This is basically a "this one thing I accidentally did broke the system once" bug.

In particular, I accidentally wrote the maintainers list of my recipe like so:

```
extra:
  recipe-maintainers:
    pkgw
```

And then bad things happened. See [here](https://github.com/conda-forge/libsakura-feedstock/pull/1), [here](https://github.com/conda-forge/staged-recipes/pull/2530).

@jakirkham fixed it all up, but the linter should catch this!


Discussion:

Resolving patch:
diff --git a/conda_smithy/cli.py b/conda_smithy/cli.py
--- a/conda_smithy/cli.py
+++ b/conda_smithy/cli.py
@@ -234,12 +234,13 @@ class RecipeLint(Subcommand):
     def __init__(self, parser):
         super(RecipeLint, self).__init__(parser, "Lint a single conda recipe.")
         scp = self.subcommand_parser
+        scp.add_argument("--conda-forge", action='store_true')
         scp.add_argument("recipe_directory", default=[os.getcwd()], nargs='*')
 
     def __call__(self, args):
         all_good = True
         for recipe in args.recipe_directory:
-            lint = lint_recipe.main(os.path.join(recipe))
+            lint = lint_recipe.main(os.path.join(recipe), conda_forge=args.conda_forge)
             if lint:
                 all_good = False
                 print('{} has some lint:\n  {}'.format(recipe, '\n  '.join(lint)))
diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py
--- a/conda_smithy/lint_recipe.py
+++ b/conda_smithy/lint_recipe.py
@@ -6,6 +6,7 @@
 import itertools
 import os
 import re
+import github
 
 import jinja2
 import ruamel.yaml
@@ -39,7 +40,7 @@ def get_section(parent, name, lints):
     return section
 
 
-def lintify(meta, recipe_dir=None):
+def lintify(meta, recipe_dir=None, conda_forge=False):
     lints = []
     major_sections = list(meta.keys())
 
@@ -52,6 +53,7 @@ def lintify(meta, recipe_dir=None):
     requirements_section = get_section(meta, 'requirements', lints)
     about_section = get_section(meta, 'about', lints)
     extra_section = get_section(meta, 'extra', lints)
+    package_section = get_section(meta, 'package', lints)
 
     # 1: Top level meta.yaml keys should have a specific order.
     section_order_sorted = sorted(major_sections,
@@ -71,11 +73,15 @@ def lintify(meta, recipe_dir=None):
             lints.append('The {} item is expected in the about section.'
                          ''.format(about_item))
 
-    # 3: The recipe should have some maintainers.
+    # 3a: The recipe should have some maintainers.
     if not extra_section.get('recipe-maintainers', []):
         lints.append('The recipe could do with some maintainers listed in '
                      'the `extra/recipe-maintainers` section.')
 
+    # 3b: Maintainers should be a list
+    if not isinstance(extra_section.get('recipe-maintainers', []), list):
+        lints.append('Recipe maintainers should be a json list.')
+
     # 4: The recipe should have some tests.
     if 'test' not in major_sections:
         test_files = ['run_test.py', 'run_test.sh', 'run_test.bat',
@@ -149,9 +155,48 @@ def lintify(meta, recipe_dir=None):
     except RuntimeError as e:
         lints.append(str(e))
 
+    # 13: Check that the recipe name is valid
+    recipe_name = package_section.get('name', '').strip()
+    if re.match('^[a-z0-9_\-.]+$', recipe_name) is None:
+        lints.append('Recipe name has invalid characters. only lowercase alpha, numeric, '
+                     'underscores, hyphens and dots allowed')
+
+    # 14: Run conda-forge specific lints
+    if conda_forge:
+        run_conda_forge_lints(meta, recipe_dir, lints)
+
     return lints
 
 
+def run_conda_forge_lints(meta, recipe_dir, lints):
+    gh = github.Github(os.environ['GH_TOKEN'])
+    package_section = get_section(meta, 'package', lints)
+    extra_section = get_section(meta, 'extra', lints)
+    recipe_dirname = os.path.basename(recipe_dir) if recipe_dir else 'recipe'
+    recipe_name = package_section.get('name', '').strip()
+    is_staged_recipes = recipe_dirname != 'recipe'
+
+    # 1: Check that the recipe does not exist in conda-forge
+    if is_staged_recipes:
+        cf = gh.get_user(os.getenv('GH_ORG', 'conda-forge'))
+        try:
+            cf.get_repo('{}-feedstock'.format(recipe_name))
+            feedstock_exists = True
+        except github.UnknownObjectException as e:
+            feedstock_exists = False
+
+        if feedstock_exists:
+            lints.append('Feedstock with the same name exists in conda-forge')
+
+    # 2: Check that the recipe maintainers exists:
+    maintainers = extra_section.get('recipe-maintainers', [])
+    for maintainer in maintainers:
+        try:
+            gh.get_user(maintainer)
+        except github.UnknownObjectException as e:
+            lints.append('Recipe maintainer "{}" does not exist'.format(maintainer))
+
+
 def selector_lines(lines):
     # Using the same pattern defined in conda-build (metadata.py),
     # we identify selectors.
@@ -168,7 +213,7 @@ def selector_lines(lines):
             yield line
 
 
-def main(recipe_dir):
+def main(recipe_dir, conda_forge=False):
     recipe_dir = os.path.abspath(recipe_dir)
     recipe_meta = os.path.join(recipe_dir, 'meta.yaml')
     if not os.path.exists(recipe_dir):
@@ -179,5 +224,5 @@ def main(recipe_dir):
     with io.open(recipe_meta, 'rt') as fh:
         content = env.from_string(''.join(fh)).render(os=os)
         meta = ruamel.yaml.load(content, ruamel.yaml.RoundTripLoader)
-    results = lintify(meta, recipe_dir)
+    results = lintify(meta, recipe_dir, conda_forge)
     return results


Test patch:
diff --git a/conda_smithy/tests/test_lint_recipe.py b/conda_smithy/tests/test_lint_recipe.py
--- a/conda_smithy/tests/test_lint_recipe.py
+++ b/conda_smithy/tests/test_lint_recipe.py
@@ -10,9 +10,13 @@
 import tempfile
 import textwrap
 import unittest
+import warnings
+import github
 
 import conda_smithy.lint_recipe as linter
 
+def is_gh_token_set():
+    return 'GH_TOKEN' in os.environ
 
 @contextmanager
 def tmp_directory():
@@ -97,6 +101,10 @@ def test_maintainers_section(self):
         lints = linter.lintify({'extra': ['recipe-maintainers']})
         self.assertIn(expected_message, lints)
 
+        lints = linter.lintify({'extra': {'recipe-maintainers': 'Luke'}})
+        expected_message = ('Recipe maintainers should be a json list.')
+        self.assertIn(expected_message, lints)
+
     def test_test_section(self):
         expected_message = 'The recipe must have some tests.'
 
@@ -215,6 +223,13 @@ def test_redundant_license(self):
                             'the word "License".')
         self.assertIn(expected_message, lints)
 
+    def test_recipe_name(self):
+        meta = {'package': {'name': 'mp++'}}
+        lints = linter.lintify(meta)
+        expected_message = ('Recipe name has invalid characters. only lowercase alpha, '
+                            'numeric, underscores, hyphens and dots allowed')
+        self.assertIn(expected_message, lints)
+
     def test_end_empty_line(self):
         bad_contents = [
             # No empty lines at the end of the file
@@ -253,6 +268,43 @@ def test_end_empty_line(self):
                 else:
                     self.assertIn(expected_message, lints)
 
+    @unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
+    def test_maintainer_exists(self):
+        lints = linter.lintify({'extra': {'recipe-maintainers': ['support']}}, conda_forge=True)
+        expected_message = ('Recipe maintainer "support" does not exist')
+        self.assertIn(expected_message, lints)
+
+        lints = linter.lintify({'extra': {'recipe-maintainers': ['isuruf']}}, conda_forge=True)
+        expected_message = ('Recipe maintainer "isuruf" does not exist')
+        self.assertNotIn(expected_message, lints)
+
+        expected_message = 'Feedstock with the same name exists in conda-forge'
+        # Check that feedstock exists if staged_recipes
+        lints = linter.lintify({'package': {'name': 'python'}}, recipe_dir='python', conda_forge=True)
+        self.assertIn(expected_message, lints)
+        lints = linter.lintify({'package': {'name': 'python'}}, recipe_dir='python', conda_forge=False)
+        self.assertNotIn(expected_message, lints)
+        # No lint if in a feedstock
+        lints = linter.lintify({'package': {'name': 'python'}}, recipe_dir='recipe', conda_forge=True)
+        self.assertNotIn(expected_message, lints)
+        lints = linter.lintify({'package': {'name': 'python'}}, recipe_dir='recipe', conda_forge=False)
+        self.assertNotIn(expected_message, lints)
+
+        # Make sure there's no feedstock named python1 before proceeding
+        gh = github.Github(os.environ['GH_TOKEN'])
+        cf = gh.get_user('conda-forge')
+        try:
+            cf.get_repo('python1-feedstock')
+            feedstock_exists = True
+        except github.UnknownObjectException as e:
+            feedstock_exists = False
+
+        if feedstock_exists:
+            warnings.warn("There's a feedstock named python1, but tests assume that there isn't")
+        else:
+            lints = linter.lintify({'package': {'name': 'python1'}}, recipe_dir="python", conda_forge=True)
+            self.assertNotIn(expected_message, lints)
+
 
 class TestCLI_recipe_lint(unittest.TestCase):
     def test_cli_fail(self):