From 6ef0ea1457a6b4c803b455e400eeaca4afab376d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 3 Apr 2018 17:09:37 +0200 Subject: [PATCH] Add proof of concept for maintenance ping system --- .../maintenancePing/maintenancePing.py | 164 ++++++++++++++++++ .../maintenancePing/maintenance_ping_body | 19 ++ .../maintenancePing/unmaintained_warning | 5 + 3 files changed, 188 insertions(+) create mode 100755 app/scripts/maintenancePing/maintenancePing.py create mode 100644 app/scripts/maintenancePing/maintenance_ping_body create mode 100644 app/scripts/maintenancePing/unmaintained_warning diff --git a/app/scripts/maintenancePing/maintenancePing.py b/app/scripts/maintenancePing/maintenancePing.py new file mode 100755 index 0000000..2aece40 --- /dev/null +++ b/app/scripts/maintenancePing/maintenancePing.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +import datetime +import requests +import json +from functools import partial + +LOGIN=open("./login").read().strip() +TOKEN=open("./token").read().strip() +MAINTENANCE_PING_BODY=open("./maintenance_ping_body").read().strip() +UNMAINTAINED_WARNING=open("./unmaintained_warning").read().strip() + +def get_github(uri): + with requests.Session() as s: + s.headers.update({"Authorization": "token {}".format(TOKEN)}) + r = s.get("https://api.github.com" + uri) + + #assert r.status_code == 200, "Couldn't get {uri} . Reponse : {text}".format(uri=uri, text=r.text) + j = json.loads(r.text) + return j + +def github_date_to_days_ago(date): + now = datetime.datetime.now() + date = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") + return (now - date).days + +def get_issues(repo): + return get_github("/repos/{repo}/issues".format(repo=repo)) + +def get_active_maintenance_ping(issues): + + for issue in issues: + if issue["title"].startswith("[Maintenance ping]"): + if issue["state"] == "open": + issue["created_days_ago"] = github_date_to_days_ago(issue["created_at"]) + return issue + + return None + + +def get_old_maintenance_ping(issues): + + for issue in issues: + if issue["title"].startswith("[Maintenance ping]"): + if issue["state"] == "closed": + issue["updated_days_ago"] = github_date_to_days_ago(issue["updated_at"]) + return issue + + return None + + +def get_commit_days_ago(repo, branch): + + ref = get_github("/repos/{repo}/git/refs/heads/{branch}".format(repo=repo, branch=branch)) + if not "object" in ref: + return 99999 + + sha = ref["object"]["sha"] + github_date = get_github("/repos/{repo}/commits/{sha}".format(repo=repo, sha=sha))["commit"]["author"]["date"] + + return github_date_to_days_ago(github_date) + + +def create_maintenance_ping(repo): + api_url = "https://api.github.com/repos/{repo}/issues".format(repo=repo) + + issue = { "title": "[Maintenance ping] Is this app still maintained ?", + "body": MAINTENANCE_PING_BODY + } + + with requests.Session() as s: + s.headers.update({"Authorization": "token {}".format(TOKEN)}) + s.post(api_url, json.dumps(issue)) + +def warn_unmaintained_if_needed(repo, issue): + + issue_id = issue["number"] + comments = get_github("/repos/{repo}/issues/{id}/comments".format(repo=repo, id=issue_id)) + existing_warning = [ c for c in comments if c["user"]["login"] == LOGIN + and c["body"].startswith(UNMAINTAINED_WARNING[:20]) ] + # Nothing to do if there's already a warning about unmaintenained status... + if existing_warning: + return + + # Otherwise, post a comment + api_url = "https://api.github.com/repos/{repo}/issues/{id}/comments" \ + .format(repo=repo, id=issue_id) + comment = { "body": UNMAINTAINED_WARNING } + with requests.Session() as s: + s.headers.update({"Authorization": "token {}".format(TOKEN)}) + s.post(api_url, json.dumps(comment)) + + +def get_status_and_todo(repo): + + # (Get issues of repo) + issues = get_issues(repo) + + # Is a maintenance ping already opened ? + active_maintenance_ping = get_active_maintenance_ping(issues) + + if active_maintenance_ping: + # since more than 15 days ? + if active_maintenance_ping["created_days_ago"] > 15: + # yes - > unmaintained ! + # -> post a comment if not already done + return ("unmaintained", partial(warn_unmaintained_if_needed, + issue=active_maintenance_ping)) + else: + # no - > maintained ! (but status being questionned) + return ("maintained?", None) + + # Commit in master or testing in last 12 months ? + if get_commit_days_ago(repo, "master") < 12*30 \ + or get_commit_days_ago(repo, "testing") < 12*30: + # ok, maintained + return ("maintained", None) + + # Maintainenance status is now being questionned... + # Was there a (now closed) maintenance ping in the last 6 months ? + old_maintenance_ping = get_old_maintenance_ping(issues) + if old_maintenance_ping and old_maintenance_ping["update_days_ago"] < 6*30: + # Yes - > ok, maintained + return ("maintained", None) + else: + # No - > Gotta create a maintenance ping ! (but still considered maintained) + return ("maintained?", create_maintenance_ping) + +def get_apps_to_check(): + + official="https://raw.githubusercontent.com/YunoHost/apps/master/official.json" + community="https://raw.githubusercontent.com/YunoHost/apps/master/community.json" + + raw_apps = [] + raw_apps += json.loads(requests.get(official).text).values() + raw_apps += json.loads(requests.get(community).text).values() + + return [ app for app in raw_apps \ + if app["state"] in ["validated", "working", "inprogress"] ] + + +def main(): + + monitored_apps = get_apps_to_check() + + status = {} + todo = {} + + # For each monitored app : + for app in monitored_apps: + + app = app["url"].replace("https://github.com/","") + + try: + status[app], todo[app] = get_status_and_todo(app) + except: + continue + + #print("maintained?") + #print(len([ app for app in monitored_apps if status[app] == "maintained?"])) + #print("maintained") + #print(len([ app for app in monitored_apps if status[app] == "maintained"])) + +main() diff --git a/app/scripts/maintenancePing/maintenance_ping_body b/app/scripts/maintenancePing/maintenance_ping_body new file mode 100644 index 0000000..d90a37e --- /dev/null +++ b/app/scripts/maintenancePing/maintenance_ping_body @@ -0,0 +1,19 @@ +Hello ! + +This is a friendly automatic notice from the Yunohost Apps team : our tool noticed that this app is listed in the community/official app lists - but this app appears to be inactive. + +Hence, **this issue was created automatically to check if this app is still actively maintained.** + +## You are the current maintainer ? :construction_worker_man: :construction_worker_woman: + +#### You still actively maintain this app ? :tada: + +Please close this issue to signify that you still actively maintain this app. Nothing else, and thank you for your work :heart: ! + +#### You don't intend to maintain this app anymore ? :cry: + +Either don't do anything, or add a comment to explicitly state that you do not intend / have time / ... to maintain this app (but thanks for your work so far ! :kissing_heart:). After 15 days, if this issue is still opened, the app will be considered *unmaintained*. + +## You wish to become the new maintainer of this app ? :smile: + +You are welcome and free to comment in this thread that you wish to become the new maintainer, amd/or to create the corresponding pull request to fix this issue ! (If you do not already have commit rights on this repo, we can then arrange things with the rest of the app team. :wink:) Once this issue is closed, the app will then be flagged again as *maintained* ! diff --git a/app/scripts/maintenancePing/unmaintained_warning b/app/scripts/maintenancePing/unmaintained_warning new file mode 100644 index 0000000..98f2c58 --- /dev/null +++ b/app/scripts/maintenancePing/unmaintained_warning @@ -0,0 +1,5 @@ +:warning: **Warning**: according to the previous instructions, since this issue is opened since more than 15 days, this app is now flagged as "unmaintained". + +Feel free to volunteer as a maintainer and close this issue to reflag it as "maintained". + +*(This is an automatic message)*