gitlab-issue-bot.py (4839B)
1 #!/usr/bin/python3 2 from datetime import datetime as dt 3 import requests 4 import json 5 import sys 6 from os.path import join, expanduser 7 8 DEBUG_MODE = False 9 # Number of days that issues can be inactive before being considered stale, 10 # depending on the issue label 11 LABELS_EXPIRY_DAYS = { 12 "Doing": 14, 13 "Current sprint": 30, 14 "Next sprint": 90, 15 } 16 17 STALE_LABEL = "Stale" 18 ISSUE_NOTE_STALE = "Labeled `Stale`, as issue hasn't been updated in a long time" 19 LOGMSG_STALE_ISSUE = "Issue \u001b[1m{}\u001b[0m with label '{}' is more than {} days old. Marking as stale..." 20 21 # API URLS 22 URL_BASE = "https://gl.haflan.dev/api/v4" 23 URL_ISSUES = URL_BASE + "/issues?state=opened&per_page=100" 24 URL_PROJECTS = URL_BASE + "/projects" 25 URL_ISSUE_NOTES = URL_BASE + "/projects/{}/issues/{}/notes?sort=desc&order_by=updated_at&per_page=1" 26 27 URL_ISSUE_PUT = URL_BASE + "/projects/{}/issues/{}" # format(project_id, issue_iid) 28 URL_NOTE_POST = URL_BASE + "/projects/{}/issues/{}/notes" # format(project_id, issue_iid) 29 30 # Load token from file 31 TOKENFILE = join(expanduser("~"), ".secrets/gitlab-token") 32 if len(sys.argv) > 2: 33 TOKENFILE = sys.argv[1] 34 35 TOKEN=open(TOKENFILE).readlines()[0].strip() 36 TOKENHEADER = {"PRIVATE-TOKEN": TOKEN } 37 38 def gitget(URL): 39 return requests.get(URL, headers=TOKENHEADER).json() 40 41 def gitput(URL, json_data): 42 return 43 44 # Not sure if this covers all kinds of activity. Check thoroughly 45 def is_stale(issue, days_before_stale): 46 latest_update_time = issue["updated_at"] 47 notes = gitget(URL_ISSUE_NOTES.format(issue["project_id"], issue["iid"])) 48 if notes: 49 latest_update_time = notes[0]["updated_at"] 50 latest_update_time = latest_update_time.split("T")[0] # only Y-m-d needed 51 dt_diff = dt.now() - dt.strptime(latest_update_time, "%Y-%m-%d") 52 return dt_diff.days > days_before_stale 53 54 def find_relevant_label(issue): 55 for label in issue["labels"]: 56 if label in LABELS_EXPIRY_DAYS: 57 return label 58 return None 59 60 def mark_as_stale(issue, old_label): 61 issue["labels"].remove(old_label) 62 # Shouldn't be possible, but just in case: 63 if STALE_LABEL not in issue["labels"]: 64 issue["labels"].append(STALE_LABEL) 65 updated_issue = { 66 "id": issue["id"], 67 "iid": issue["iid"], 68 # labels is a comma-separated list, not an array 69 "labels": "{}".format(','.join(issue["labels"])) 70 } 71 issue_url = URL_ISSUE_PUT.format(issue["project_id"], issue["iid"]) 72 note_url = URL_ISSUE_NOTES.format(issue["project_id"], issue["iid"]) 73 information_note = {"body": ISSUE_NOTE_STALE } 74 if not DEBUG_MODE: 75 requests.put(issue_url, headers=TOKENHEADER, data=updated_issue) 76 requests.post(note_url, headers=TOKENHEADER, data=information_note) 77 78 if __name__ == "__main__": 79 # Make mapping from project ids to names (for logging only) 80 project_id_name = {} 81 for project in gitget(URL_PROJECTS): 82 project_id_name[project["id"]] = project["name"] 83 # Find and check *all* issues 84 all_issues = gitget(URL_ISSUES) 85 for issue in all_issues: 86 issue_name = "{}#{}".format(project_id_name[issue["project_id"]], issue["iid"]) 87 relevant_label = find_relevant_label(issue) 88 if relevant_label and is_stale(issue, LABELS_EXPIRY_DAYS[relevant_label]): 89 print(LOGMSG_STALE_ISSUE.format(issue_name, relevant_label, 90 LABELS_EXPIRY_DAYS[relevant_label])) 91 mark_as_stale(issue, relevant_label) 92 93 """ 94 PROJECT_URL_ISSUES = URL_BASE + "/projects/{}/issues?state=opened&per_page=100" 95 96 # Like groups: No reason to go via project names either 97 project_id_name = {} 98 project_name_id = {} 99 for project in gitget(URL_PROJECTS): 100 if project["id"] not in project_id_name: 101 project_id_name[project["id"]] = project["name"] 102 project_name_id[project["name"]] = project["id"] 103 104 for pid in project_id_name: 105 #print("\u001b[1m" + project_id_name[pid] + ":\u001b[0m") 106 for issue in gitget(PROJECT_URL_ISSUES.format(pid)): 107 if has_relevant_label(issue): 108 check_expiry(issue) 109 110 """ 111 112 """ 113 GROUPS_URL = URL_BASE + "/groups" 114 # Projects are addressed by their ID, so group is not that important 115 groups = [ {"id": group["id"], "name": group["name"]} for group in gitget(GROUPS_URL)] 116 for group in groups: 117 print("\u001b[1m" + group["name"] + ":\u001b[0m ", end="") 118 print(len(gitget(GROUPS_URL + "/" + str(group["id"]) + "/issues?per_page=100"))) 119 """ 120 121 """ 122 for issue in all_open_issues: 123 print() 124 project = project_id_name[int(issue["project_id"])] 125 issue_str_id = project + "#" + str(issue["iid"]) 126 issue_string_ids[issue_str_id] = issue 127 print(issue_str_id + ": \u001b[1m" + issue["title"] + "\u001b[0m") 128 print(issue["description"]) 129 prompt = input() 130 if prompt == "exit": 131 exit() 132 """