Skip to content

Commit

Permalink
feat: unify staged-recipes linting
Browse files Browse the repository at this point in the history
  • Loading branch information
beckermr committed Sep 15, 2024
1 parent 8ad8071 commit 83873ee
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 112 deletions.
37 changes: 0 additions & 37 deletions .github/workflows/correct_directory.yml

This file was deleted.

38 changes: 0 additions & 38 deletions .github/workflows/do_not_edit_example.yml

This file was deleted.

36 changes: 36 additions & 0 deletions .github/workflows/lint_staged_recipes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: lint

on:
pull_request:

jobs:
lint:
name: lint
runs-on: ubuntu-latest
defaults:
run:
shell: bash -leo pipefail {0}

steps:
- name: checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

- name: install code
uses: mamba-org/setup-micromamba@f8b8a1e23a26f60a44c853292711bacfd3eac822
with:
environment-name: lint
condarc: |
show_channel_urls: true
channel_priority: strict
channels:
- conda-forge
create-args: >-
conda-smithy
pygithub
requests
- name: lint
run: |
python .github/scripts/lint_staged_recipes.py
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 changes: 0 additions & 37 deletions .github/workflows/only_edit_in_recipes.yaml

This file was deleted.

198 changes: 198 additions & 0 deletions .github/workflows/scripts/lint_staged_recipes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import argparse
from collections import defaultdict
from pathlib import Path
import pprint
import os
import sys

from conda_smithy.linter import conda_recipe_v1_linter
from conda_smithy.linter.utils import (
get_section,
)
from conda_smithy.utils import get_yaml, render_meta_yaml
import github
import requests


def _lint_recipes(gh, pr):
lints = []
hints = []
recipe_lints = defaultdict(list)
recipe_hints = defaultdict(list)
fnames = set(f.filename for f in pr.get_files())

# 1. Do not edit or delete example recipes
if "recipes/example/meta.yaml" in fnames or "recipes/example-v1/recipe.yaml" in fnames:
lints.append("Do not edit or delete example recipes in `recipes/example/` or `recipe/example-v1/`.")

# 2. Make sure the new recipe is in the right directory
if any(
(
f.startswith("recipes/example/")
or f.startswith("recipes/example-v1/")
or f.startswith("recipes/meta.y")
or f.startswith("recipes/recipe.y")
)
for f in fnames
):
lints.append(
"Please put your recipe in its own directory in the `recipes/` directory as "
"`recipe/<name of feedstock>/<your recipe file>.yaml`."
)

# 3. Only edit recipe files
if not all(f.startswith("recipes/") for f in fnames):
lints.append("Do not edit files outside of the `recipes/` directory.")

# Recipe-specific lints/hints
for fname in fnames:
if (
(not fname.startswith("recipes/"))
or fname == "recipes/example/meta.yaml"
or fname == "recipes/example-v1/recipe.yaml"
):
continue

# grab basic metadata
if fname.endswith("meta.yaml"):
with open(fname) as fh:
content = render_meta_yaml("".join(fh))
meta = get_yaml().load(content)
recipe_version = 0
else:
meta = get_yaml().load(Path(fname))
recipe_version = 1

package_section = get_section(
meta, "package", lints, recipe_version=recipe_version
)
sources_section = get_section(
meta, "source", lints, recipe_version=recipe_version
)
extra_section = get_section(
meta, "extra", lints, recipe_version=recipe_version
)
maintainers = extra_section.get("recipe-maintainers", [])

if recipe_version == 1:
recipe_name = conda_recipe_v1_linter.get_recipe_name(meta)
else:
recipe_name = package_section.get("name", "").strip()

# 4. Check for existing feedstocks in conda-forge or bioconda
if recipe_name:
cf = gh.get_user("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:
recipe_lints[fname].append("Feedstock with the same name exists in conda-forge.")
elif feedstock_exists:
recipe_hints[fname].append(
f"Feedstock with the name {existing_recipe_name} exists in conda-forge. "
f"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:
recipe_hints[fname].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"]
recipe_hints[fname].append(
f"A conda package with same name ({conda_name}) already exists."
)

# 5. Ensure all maintainers have commented that they approve of being listed
if maintainers:
# Get PR author, issue comments, and review comments
pr_author = pr.user.login
issue_comments = pr.get_issue_comments()
review_comments = 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:
recipe_lints[fname].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."
)

return lints, hints, recipe_lints, recipe_lints


def _comment_on_pr(pr, lints, hints, recipe_lints, recipe_hints):
print("lints:\n", pprint.pformat(lints))
print("hints:\n", pprint.pformat(hints))
print("recipe_lints:\n", pprint.pformat(recipe_lints))
print("recipe_hints:\n", pprint.pformat(recipe_hints))
print("::notice title=test::what does this do?")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Lint staged recipes.')
parser.add_argument('--pr-num', type=int, required=True, help='the PR number')

args = parser.parse_args()

gh = github.Github(auth=github.Auth.Token(os.getenv("GH_TOKEN")))
repo = gh.get_repo("conda-forge/staged-recipes")
pr = repo.get_pull(args.pr_num)

lints, hints, recipe_lints, recipe_hints = _lint_recipes(gh, pr)
if lints or hints or recipe_lints or recipe_hints:
_comment_on_pr(pr, lints, hints, recipe_lints, recipe_hints)
sys.exit(1)

0 comments on commit 83873ee

Please sign in to comment.