Repository: conda-forge/conda-smithy

Issue:
BUG: spurious failures in linter not finding maintainers / failures in `test_maintainer_exists`
Now that we're using the smithy test suite as a `downstreams:` in both conda & conda-build, it's going to be painful to have these tests spuriously failing regularly.

```
=========================== short test summary info ============================
FAILED tests/test_lint_recipe.py::TestLinter::test_maintainer_exists - AssertionError: 'Recipe maintainer "isuruf" does not exist' unexpectedly found in ['The home item is expected in the about section.', 'The license item is expected in the about section.', 'The summary item is expected in the about section.', 'The recipe must have some tests.', 'The recipe must have a `build/number` section.', 'Recipe name has invalid characters. only lowercase alpha, numeric, underscores, hyphens and dots allowed', 'Recipe maintainer "isuruf" does not exist', 'Package version is missing.']
==== 1 failed, 2563 passed, 1 skipped, 1 deselected, 51 warnings in 34.56s =====
```

We should add a retry, either on test level (there's a pytest [plugin](https://github.com/pytest-dev/pytest-rerunfailures) that will restart tests a given number of times), or on getting the org-level info from GH.

The better solution would be to do the latter, because these messages also show up in various PRs as "maintainer X not found", which is confusing.

Discussion:
beckermr: cc @jakirkham @h-vetinari 
beckermr: @conda-forge/core this one is ready for review!

Patch:
diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py
index 67dbdb90..8938fdb1 100644
--- a/conda_smithy/lint_recipe.py
+++ b/conda_smithy/lint_recipe.py
@@ -413,16 +413,44 @@ def _maintainer_exists(maintainer: str) -> bool:
         gh = _cached_gh()
         try:
             gh.get_user(maintainer)
+            is_user = True
         except github.UnknownObjectException:
-            return False
-        return True
+            is_user = False
+
+        # for w/e reason, the user endpoint returns an entry for orgs
+        # however the org endpoint does not return an entry for users
+        # so we have to check both
+        try:
+            gh.get_organization(maintainer)
+            is_org = True
+        except github.UnknownObjectException:
+            is_org = False
     else:
-        return (
-            requests.get(
-                f"https://api.github.com/users/{maintainer}"
-            ).status_code
-            == 200
+        # this API request has no token and so has a restrictive rate limit
+        # return (
+        #     requests.get(
+        #         f"https://api.github.com/users/{maintainer}"
+        #     ).status_code
+        #     == 200
+        # )
+        # so we check two public URLs instead.
+        # 1. github.com/<maintainer>?tab=repositories - this URL works for all users and all orgs
+        # 2. https://github.com/orgs/<maintainer>/teams - this URL only works for
+        #    orgs so we make sure it fails
+        # we do not allow redirects to ensure we get the correct status code
+        # for the specific URL we requested
+        req_profile = requests.get(
+            f"https://github.com/{maintainer}?tab=repositories",
+            allow_redirects=False,
+        )
+        is_user = req_profile.status_code == 200
+        req_org = requests.get(
+            f"https://github.com/orgs/{maintainer}/teams",
+            allow_redirects=False,
         )
+        is_org = req_org.status_code == 200
+
+    return is_user and not is_org
 
 
 @lru_cache(maxsize=1)
diff --git a/news/2171-fix-maintainer_exists.rst b/news/2171-fix-maintainer_exists.rst
new file mode 100644
index 00000000..f4423900
--- /dev/null
+++ b/news/2171-fix-maintainer_exists.rst
@@ -0,0 +1,24 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Fixed a bug in checking in recipe maintainers exist without a GitHub token
+  where the anonymous API would run out of requests. (#2171)
+
+**Security:**
+
+* <news item>


Test patch:
diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py
index 4534fd7d..b49f19b3 100644
--- a/tests/test_lint_recipe.py
+++ b/tests/test_lint_recipe.py
@@ -1871,6 +1871,42 @@ linter:
         expected_message = 'Recipe maintainer "isuruf" does not exist'
         self.assertNotIn(expected_message, lints)
 
+        lints, _ = linter.lintify_meta_yaml(
+            {"extra": {"recipe-maintainers": ["conda-forge"]}},
+            conda_forge=True,
+        )
+        expected_message = 'Recipe maintainer "conda-forge" does not exist'
+        self.assertIn(expected_message, lints)
+
+    def test_maintainer_exists_no_token(self):
+        gh_token = os.environ.get("GH_TOKEN", None)
+        try:
+            if "GH_TOKEN" in os.environ:
+                del os.environ["GH_TOKEN"]
+
+            lints, _ = linter.lintify_meta_yaml(
+                {"extra": {"recipe-maintainers": ["support"]}},
+                conda_forge=True,
+            )
+            expected_message = 'Recipe maintainer "support" does not exist'
+            self.assertIn(expected_message, lints)
+
+            lints, _ = linter.lintify_meta_yaml(
+                {"extra": {"recipe-maintainers": ["isuruf"]}}, conda_forge=True
+            )
+            expected_message = 'Recipe maintainer "isuruf" does not exist'
+            self.assertNotIn(expected_message, lints)
+
+            lints, _ = linter.lintify_meta_yaml(
+                {"extra": {"recipe-maintainers": ["conda-forge"]}},
+                conda_forge=True,
+            )
+            expected_message = 'Recipe maintainer "conda-forge" does not exist'
+            self.assertIn(expected_message, lints)
+        finally:
+            if gh_token is not None:
+                os.environ["GH_TOKEN"] = gh_token
+
     def test_maintainer_team_exists(self):
         lints, _ = linter.lintify_meta_yaml(
             {