Repository: conda-forge/conda-smithy

Issue:
Not all hints shown when running `conda smithy lint recipe` locally
I'm often linting locally before pushing to avoid creating noisy linter comments upon pushing to a PR, however, it seems the new warning about missing pip build dependencies doesn't show up when doing so. Running the linter locally (conda-smithy 3.39.0) for https://github.com/conda-forge/cython-feedstock/pull/143 says "recipe is in fine form", while the actual linter [complains](https://github.com/conda-forge/cython-feedstock/pull/143#issuecomment-2316665990).

Other hints (like the SPDX identifier) _do_ get shown, so it's not about hint vs. error. This is on a windows machine, in case that matters. 

Discussion:

Patch:
diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py
index 825ba192..5030a4a4 100644
--- a/conda_smithy/lint_recipe.py
+++ b/conda_smithy/lint_recipe.py
@@ -9,7 +9,6 @@ from pathlib import Path
 from textwrap import indent
 from typing import Any, List, Optional, Tuple
 
-import github
 import jsonschema
 import requests
 from conda_build.metadata import (
@@ -371,6 +370,14 @@ def lintify_meta_yaml(
     return lints, hints
 
 
+def _maintainer_exists(maintainer: str) -> bool:
+    """Check if a maintainer exists on GitHub."""
+    return (
+        requests.get(f"https://api.github.com/users/{maintainer}").status_code
+        == 200
+    )
+
+
 def run_conda_forge_specific(
     meta,
     recipe_dir,
@@ -378,8 +385,6 @@ def run_conda_forge_specific(
     hints,
     recipe_version: int = 0,
 ):
-    gh = github.Github(os.environ["GH_TOKEN"])
-
     # Retrieve sections from meta
     package_section = get_section(
         meta, "package", lints, recipe_version=recipe_version
@@ -387,9 +392,6 @@ def run_conda_forge_specific(
     extra_section = get_section(
         meta, "extra", lints, recipe_version=recipe_version
     )
-    sources_section = get_section(
-        meta, "source", lints, recipe_version=recipe_version
-    )
     requirements_section = get_section(
         meta, "requirements", lints, recipe_version=recipe_version
     )
@@ -408,87 +410,18 @@ def run_conda_forge_specific(
     is_staged_recipes = recipe_dirname != "recipe"
 
     # 1: Check that the recipe does not exist in conda-forge or bioconda
-    if is_staged_recipes and recipe_name:
-        cf = gh.get_user(os.getenv("GH_ORG", "conda-forge"))
-
-        for name in set(
-            [
-                recipe_name,
-                recipe_name.replace("-", "_"),
-                recipe_name.replace("_", "-"),
-            ]
-        ):
-            try:
-                if cf.get_repo(f"{name}-feedstock"):
-                    existing_recipe_name = name
-                    feedstock_exists = True
-                    break
-                else:
-                    feedstock_exists = False
-            except github.UnknownObjectException:
-                feedstock_exists = False
-
-        if feedstock_exists and existing_recipe_name == recipe_name:
-            lints.append("Feedstock with the same name exists in conda-forge.")
-        elif feedstock_exists:
-            hints.append(
-                f"Feedstock with the name {existing_recipe_name} exists in conda-forge. Is it the same as this package ({recipe_name})?"
-            )
-
-        bio = gh.get_user("bioconda").get_repo("bioconda-recipes")
-        try:
-            bio.get_dir_contents(f"recipes/{recipe_name}")
-        except github.UnknownObjectException:
-            pass
-        else:
-            hints.append(
-                "Recipe with the same name exists in bioconda: "
-                "please discuss with @conda-forge/bioconda-recipes."
-            )
-
-        url = None
-        if recipe_version == 1:
-            for source_url in sources_section:
-                if source_url.startswith("https://pypi.io/packages/source/"):
-                    url = source_url
-        else:
-            for source_section in sources_section:
-                if str(source_section.get("url")).startswith(
-                    "https://pypi.io/packages/source/"
-                ):
-                    url = source_section["url"]
-        if url:
-            # get pypi name from  urls like "https://pypi.io/packages/source/b/build/build-0.4.0.tar.gz"
-            pypi_name = url.split("/")[6]
-            mapping_request = requests.get(
-                "https://raw.githubusercontent.com/regro/cf-graph-countyfair/master/mappings/pypi/name_mapping.yaml"
-            )
-            if mapping_request.status_code == 200:
-                mapping_raw_yaml = mapping_request.content
-                mapping = get_yaml().load(mapping_raw_yaml)
-                for pkg in mapping:
-                    if pkg.get("pypi_name", "") == pypi_name:
-                        conda_name = pkg["conda_name"]
-                        hints.append(
-                            f"A conda package with same name ({conda_name}) already exists."
-                        )
+    # moved to staged-recipes directly
 
     # 2: Check that the recipe maintainers exists:
     for maintainer in maintainers:
         if "/" in maintainer:
             # It's a team. Checking for existence is expensive. Skip for now
             continue
-        try:
-            gh.get_user(maintainer)
-        except github.UnknownObjectException:
+        if not _maintainer_exists(maintainer):
             lints.append(f'Recipe maintainer "{maintainer}" does not exist')
 
     # 3: if the recipe dir is inside the example dir
-    if recipe_dir is not None and "recipes/example/" in recipe_dir:
-        lints.append(
-            "Please move the recipe out of the example dir and "
-            "into its own dir."
-        )
+    # moved to staged-recipes directly
 
     # 4: Do not delete example recipe
     # removed in favor of direct check in staged-recipes CI
@@ -522,35 +455,7 @@ def run_conda_forge_specific(
             hints.append(specific_hints[dep])
 
     # 6: Check if all listed maintainers have commented:
-    pr_number = os.environ.get("STAGED_RECIPES_PR_NUMBER")
-
-    if is_staged_recipes and maintainers and pr_number:
-        # Get PR details using GitHub API
-        current_pr = gh.get_repo("conda-forge/staged-recipes").get_pull(
-            int(pr_number)
-        )
-
-        # Get PR author, issue comments, and review comments
-        pr_author = current_pr.user.login
-        issue_comments = current_pr.get_issue_comments()
-        review_comments = current_pr.get_reviews()
-
-        # Combine commenters from both issue comments and review comments
-        commenters = {comment.user.login for comment in issue_comments}
-        commenters.update({review.user.login for review in review_comments})
-
-        # Check if all maintainers have either commented or are the PR author
-        non_participating_maintainers = set()
-        for maintainer in maintainers:
-            if maintainer not in commenters and maintainer != pr_author:
-                non_participating_maintainers.add(maintainer)
-
-        # Add a lint message if there are any non-participating maintainers
-        if non_participating_maintainers:
-            lints.append(
-                f"The following maintainers have not yet confirmed that they are willing to be listed here: "
-                f"{', '.join(non_participating_maintainers)}. Please ask them to comment on this PR if they are."
-            )
+    # moved to staged recipes directly
 
     # 7: Ensure that the recipe has some .ci_support files
     if not is_staged_recipes and recipe_dir is not None:
diff --git a/news/gh-token-linting.rst b/news/gh-token-linting.rst
new file mode 100644
index 00000000..40ccde16
--- /dev/null
+++ b/news/gh-token-linting.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* Moved staged-recipes specific lints/hints that required a GitHub token to the `staged-recipes` repository. (#)
+
+**Fixed:**
+
+* <news item>
+
+**Security:**
+
+* <news item>


Test patch:
diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py
index 4759b1cc..b0b5491d 100644
--- a/tests/test_lint_recipe.py
+++ b/tests/test_lint_recipe.py
@@ -5,12 +5,10 @@ import subprocess
 import tempfile
 import textwrap
 import unittest
-import warnings
 from collections import OrderedDict
 from contextlib import contextmanager
 from pathlib import Path
 
-import github
 import pytest
 
 import conda_smithy.lint_recipe as linter
@@ -20,10 +18,6 @@ from conda_smithy.utils import get_yaml
 _thisdir = os.path.abspath(os.path.dirname(__file__))
 
 
-def is_gh_token_set():
-    return "GH_TOKEN" in os.environ
-
-
 @contextmanager
 def get_recipe_in_dir(recipe_name: str) -> Path:
     base_dir = Path(__file__).parent
@@ -1819,7 +1813,6 @@ noarch_platforms:
         )
         assert not lints
 
-    @unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
     def test_maintainer_exists(self):
         lints, _ = linter.lintify_meta_yaml(
             {"extra": {"recipe-maintainers": ["support"]}}, conda_forge=True
@@ -1833,168 +1826,6 @@ noarch_platforms:
         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_meta_yaml(
-            {"package": {"name": "python"}},
-            recipe_dir="python",
-            conda_forge=True,
-        )
-        self.assertIn(expected_message, lints)
-        lints, _ = linter.lintify_meta_yaml(
-            {"package": {"name": "python"}},
-            recipe_dir="python",
-            conda_forge=False,
-        )
-        self.assertNotIn(expected_message, lints)
-        # No lint if in a feedstock
-        lints, _ = linter.lintify_meta_yaml(
-            {"package": {"name": "python"}},
-            recipe_dir="recipe",
-            conda_forge=True,
-        )
-        self.assertNotIn(expected_message, lints)
-        lints, _ = linter.lintify_meta_yaml(
-            {"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:
-            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_meta_yaml(
-                {"package": {"name": "python1"}},
-                recipe_dir="python",
-                conda_forge=True,
-            )
-            self.assertNotIn(expected_message, lints)
-
-        # Test bioconda recipe checking
-        expected_message = (
-            "Recipe with the same name exists in bioconda: "
-            "please discuss with @conda-forge/bioconda-recipes."
-        )
-        bio = gh.get_user("bioconda").get_repo("bioconda-recipes")
-        r = "samtools"
-        try:
-            bio.get_dir_contents(f"recipe/{r}")
-        except github.UnknownObjectException:
-            warnings.warn(
-                f"There's no bioconda recipe named {r}, but tests assume that there is"
-            )
-        else:
-            # Check that feedstock exists if staged_recipes
-            lints, _ = linter.lintify_meta_yaml(
-                {"package": {"name": r}}, recipe_dir=r, conda_forge=True
-            )
-            self.assertIn(expected_message, lints)
-            lints, _ = linter.lintify_meta_yaml(
-                {"package": {"name": r}}, recipe_dir=r, conda_forge=False
-            )
-            self.assertNotIn(expected_message, lints)
-            # No lint if in a feedstock
-            lints, _ = linter.lintify_meta_yaml(
-                {"package": {"name": r}}, recipe_dir="recipe", conda_forge=True
-            )
-            self.assertNotIn(expected_message, lints)
-            lints, _ = linter.lintify_meta_yaml(
-                {"package": {"name": r}},
-                recipe_dir="recipe",
-                conda_forge=False,
-            )
-            self.assertNotIn(expected_message, lints)
-            # No lint if the name isn't specified
-            lints, _ = linter.lintify_meta_yaml(
-                {}, recipe_dir=r, conda_forge=True
-            )
-            self.assertNotIn(expected_message, lints)
-
-        r = "this-will-never-exist"
-        try:
-            bio.get_dir_contents(f"recipes/{r}")
-        except github.UnknownObjectException:
-            lints, _ = linter.lintify_meta_yaml(
-                {"package": {"name": r}}, recipe_dir=r, conda_forge=True
-            )
-            self.assertNotIn(expected_message, lints)
-        else:
-            warnings.warn(
-                f"There's a bioconda recipe named {r}, but tests assume that there isn't"
-            )
-
-        expected_message = (
-            "A conda package with same name (numpy) already exists."
-        )
-        lints, hints = linter.lintify_meta_yaml(
-            {
-                "package": {"name": "this-will-never-exist"},
-                "source": {
-                    "url": "https://pypi.io/packages/source/n/numpy/numpy-1.26.4.tar.gz"
-                },
-            },
-            recipe_dir="recipes/foo",
-            conda_forge=True,
-        )
-        self.assertIn(expected_message, hints)
-
-        # check that this doesn't choke
-        lints, hints = linter.lintify_meta_yaml(
-            {
-                "package": {"name": "this-will-never-exist"},
-                "source": {
-                    "url": [
-                        "https://pypi.io/packages/source/n/numpy/numpy-1.26.4.tar.gz"
-                    ]
-                },
-            },
-            recipe_dir="recipes/foo",
-            conda_forge=True,
-        )
-
-    @unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
-    def test_maintainer_participation(self):
-        # Mocking PR and maintainer data
-        os.environ["STAGED_RECIPES_PR_NUMBER"] = "1"  # Example PR number
-        maintainers = ["pelson", "isuruf"]
-
-        try:
-            # Running the linter function
-            lints, _ = linter.lintify_meta_yaml(
-                {"extra": {"recipe-maintainers": maintainers}},
-                recipe_dir="python",
-                conda_forge=True,
-            )
-
-            # Expected message if a maintainer has not participated
-            expected_message = (
-                "The following maintainers have not yet confirmed that they are willing to be listed here: "
-                "isuruf. Please ask them to comment on this PR if they are."
-            )
-            self.assertIn(expected_message, lints)
-
-            expected_message = (
-                "The following maintainers have not yet confirmed that they are willing to be listed here: "
-                "pelson, isuruf. Please ask them to comment on this PR if they are."
-            )
-            self.assertNotIn(expected_message, lints)
-        finally:
-            del os.environ["STAGED_RECIPES_PR_NUMBER"]
-
     def test_bad_subheader(self):
         expected_message = (
             "The {} section contained an unexpected "
@@ -2086,25 +1917,6 @@ noarch_platforms:
         lints, hints = linter.lintify_meta_yaml(meta, recipe_version=1)
         assert any(lint.startswith(expected_message) for lint in lints)
 
-    @unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
-    def test_examples(self):
-        msg = (
-            "Please move the recipe out of the example dir and into its "
-            "own dir."
-        )
-        lints, hints = linter.lintify_meta_yaml(
-            {"extra": {"recipe-maintainers": ["support"]}},
-            recipe_dir="recipes/example/",
-            conda_forge=True,
-        )
-        self.assertIn(msg, lints)
-        lints = linter.lintify_meta_yaml(
-            {"extra": {"recipe-maintainers": ["support"]}},
-            recipe_dir="python",
-            conda_forge=True,
-        )
-        self.assertNotIn(msg, lints)
-
     def test_multiple_sources(self):
         lints = linter.main(
             os.path.join(_thisdir, "recipes", "multiple_sources")
@@ -2268,7 +2080,6 @@ noarch_platforms:
         )
         assert len(hints) < 100
 
-    @unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
     def test_mpl_base_hint(self):
         meta = {
             "requirements": {
@@ -2279,7 +2090,6 @@ noarch_platforms:
         expected = "Recipes should usually depend on `matplotlib-base`"
         self.assertTrue(any(hint.startswith(expected) for hint in hints))
 
-    @unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
     def test_mpl_base_hint_outputs(self):
         meta = {
             "outputs": [
@@ -2513,7 +2323,6 @@ class TestCliRecipeLint(unittest.TestCase):
             assert_jinja('{% set version= "0.27.3"%}', is_good=False)
 
 
-@unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
 def test_lint_no_builds():
     expected_message = "The feedstock has no `.ci_support` files and "
 
@@ -2542,7 +2351,6 @@ def test_lint_no_builds():
         assert not any(lint.startswith(expected_message) for lint in lints)
 
 
-@unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
 def test_lint_duplicate_cfyml():
     expected_message = (
         "The ``conda-forge.yml`` file is not allowed to have duplicate keys."
@@ -2771,7 +2579,6 @@ def test_v1_package_name_version():
         assert lint_2 in lints
 
 
-@unittest.skipUnless(is_gh_token_set(), "GH_TOKEN not set")
 @pytest.mark.parametrize("remove_top_level", [True, False])
 @pytest.mark.parametrize(
     "outputs_to_add, outputs_expected_hints",