Repository: conda-forge/conda-smithy

Issue:
[Feature request] Add linter for staged-recipes, checking if maintainers have agreed
This is a continuation of https://matrix.to/#/!SOyumkgPRWoXfQYIFH:matrix.org/$169940345027597QzKsX:matrix.org?via=matrix.org&via=gitter.im&via=cadair.com

In short: in staged-recipes, we make sure before merging that all maintainers listed in the feedstock have agreed to be listed there. This is a tickbox in the PR description so, in theory submitters should check themselves but that is not always the case.

This issue is to discuss, if it makes sense to have a linter check, which checks if all of the maintainers listed have interacted with the PR and then displays a message, if that is not the case.

The difficulty is, that conda-smithy currently lints the recipe without knowing the PR the recipe is in which makes it impossible to see who has interacted with the PR. 
Hence, the two steps for implementing this are

- [x] Pass PR number from webservices to conda-smithy https://github.com/conda-forge/conda-forge-webservices/pull/565
- [x] Add the function to conda-smithy. Draft is here: https://github.com/conda-forge/conda-smithy/pull/1792

The relevant function in conda-smithy starts here: https://github.com/conda-forge/conda-smithy/blob/992fb6deca7294afa581fb7de5a1a9b30e3322bc/conda_smithy/lint_recipe.py#L808
The webservices linter is called here: https://github.com/conda-forge/conda-forge-webservices/blob/main/conda_forge_webservices/linting.py


Discussion:

Resolving patch:
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
@@ -807,12 +807,17 @@ def check_pins_build_and_requirements(top_level):
 
 def run_conda_forge_specific(meta, recipe_dir, lints, hints):
     gh = github.Github(os.environ["GH_TOKEN"])
+
+    # Retrieve sections from meta
     package_section = get_section(meta, "package", lints)
     extra_section = get_section(meta, "extra", lints)
     sources_section = get_section(meta, "source", lints)
     requirements_section = get_section(meta, "requirements", lints)
     outputs_section = get_section(meta, "outputs", lints)
 
+    # Fetch list of recipe maintainers
+    maintainers = extra_section.get("recipe-maintainers", [])
+
     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"
@@ -882,7 +887,6 @@ def run_conda_forge_specific(meta, recipe_dir, lints, hints):
                         )
 
     # 2: Check that the recipe maintainers exists:
-    maintainers = extra_section.get("recipe-maintainers", [])
     for maintainer in maintainers:
         if "/" in maintainer:
             # It's a team. Checking for existence is expensive. Skip for now
@@ -944,6 +948,37 @@ def run_conda_forge_specific(meta, recipe_dir, lints, hints):
         if dep in specific_hints and specific_hints[dep] not in hints:
             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."
+            )
+
 
 def is_selector_line(line, allow_platforms=False):
     # Using the same pattern defined in conda-build (metadata.py),


Test patch:
diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py
--- a/tests/test_lint_recipe.py
+++ b/tests/test_lint_recipe.py
@@ -1103,6 +1103,35 @@ def test_maintainer_exists(self):
             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(
+                {"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 "