diff --git a/.cookiecutter/includes/tox/passenv b/.cookiecutter/includes/tox/passenv index a913c55..344a0b1 100644 --- a/.cookiecutter/includes/tox/passenv +++ b/.cookiecutter/includes/tox/passenv @@ -1 +1,2 @@ +dev: GITHUB_ACTIONS dev: GITHUB_TOKEN diff --git a/.github/workflows/alert.yml b/.github/workflows/alert.yml index 59d1722..ed1a24a 100644 --- a/.github/workflows/alert.yml +++ b/.github/workflows/alert.yml @@ -37,7 +37,7 @@ jobs: - name: Post to Slack uses: slackapi/slack-github-action@v1.24.0 with: - channel-id: ${{ vars.SLACK_CHANNEL }} + channel-id: C062HG8E691 slack-message: ${{ env.SLACK_MESSAGE }} env: SLACK_MESSAGE: ${{ steps.slack_message.outputs.SLACK_MESSAGE }} diff --git a/src/dependabot_alerts/cli.py b/src/dependabot_alerts/cli.py index 08e9182..e2b2a12 100644 --- a/src/dependabot_alerts/cli.py +++ b/src/dependabot_alerts/cli.py @@ -1,13 +1,35 @@ +import subprocess from argparse import ArgumentParser from importlib.metadata import version +from os import environ +from dependabot_alerts.core import GitHub -def cli(_argv=None): # pylint:disable=inconsistent-return-statements + +def cli(argv=None): parser = ArgumentParser() - parser.add_argument("-v", "--version", action="store_true") + parser.add_argument( + "-v", "--version", action="version", version=version("dependabot-alerts") + ) + parser.add_argument("organization", help="GitHub organization") + + args = parser.parse_args(argv) + organization = args.organization - args = parser.parse_args(_argv) + github = GitHub(subprocess.run) + repos = github.alerts(organization) - if args.version: - print(version("dependabot-alerts")) - return 0 + if environ.get("GITHUB_ACTIONS") == "true": + print(f"*Found Dependabot alerts in {len(repos.values())} repos:*") + print() + for repo, packages in repos.items(): + for package, alerts in packages.items(): + print(f"* {repo}: {package} ({len(alerts)} alerts)") + print() + print( + "Message generated by the `alerts.yml` workflow in dependabot-alerts (https://github.com/hypothesis/dependabot-alerts/blob/main/.github/workflows/alert.yml)" + ) + else: + for repo, packages in repos.items(): + for package, alerts in packages.items(): + print(f"{repo}: {package} ({len(alerts)} alerts)") diff --git a/src/dependabot_alerts/core.py b/src/dependabot_alerts/core.py index d1b7a0d..ca79b69 100644 --- a/src/dependabot_alerts/core.py +++ b/src/dependabot_alerts/core.py @@ -1,2 +1,53 @@ +import json +from collections import defaultdict +from dataclasses import dataclass +from functools import reduce + + +def safe_get(dict_, keys): + return reduce(lambda d, k: d.get(k, {}), keys, dict_) + + +@dataclass(frozen=True) +class Alert: + repo: str + ecosystem: str + package: str + + @classmethod + def make(cls, alert_dict): + return cls( + repo=safe_get(alert_dict, ["repository", "full_name"]), + ecosystem=safe_get(alert_dict, ["dependency", "package", "ecosystem"]), + package=safe_get(alert_dict, ["dependency", "package", "name"]), + ) + + +class GitHub: + def __init__(self, run): + self._run = run + + def alerts(self, organization): + result = self._run( + [ + "gh", + "api", + "--paginate", + f"/orgs/{organization}/dependabot/alerts?state=open", + ], + check=True, + capture_output=True, + ) + alert_dicts = json.loads(result.stdout) + + alerts = defaultdict(lambda: defaultdict(list)) + + for alert_dict in alert_dicts: + alert = Alert.make(alert_dict) + alerts[alert.repo][(alert.ecosystem, alert.package)].append(alert) + + return alerts + + def hello_world(): return "Hello, world!" diff --git a/tox.ini b/tox.ini index 846481c..4aac4e8 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,7 @@ passenv = dev: DEBUG dev: SENTRY_DSN dev: NEW_RELIC_LICENSE_KEY + dev: GITHUB_ACTIONS dev: GITHUB_TOKEN deps = dev: ipython