Release Notes: - N/A
Unified · Split
| @@ -0,0 +1,93 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Add `state:needs triage` label to open GitHub issues of types Bug and Crash | ||
| 3 | if they're missing area, priority, or frequency labels. Don't touch issues | ||
| 4 | with an assignee or another `state:` label. | ||
| 5 | |||
| 6 | Requires `requests` library and a GitHub access token with "Issues (write)" | ||
| 7 | permission passed as an environment variable. Was used as a quick-and-dirty | ||
| 8 | one-off-bulk-operation script to surface older untriaged issues in the `zed` | ||
| 9 | repository. Leaving it here for reference only; there's no error handling or | ||
| 10 | guardrails, you've been warned. | ||
| 11 | """ | ||
| 12 | |||
| 13 | import itertools | ||
| 14 | import logging | ||
| 15 | import os | ||
| 16 | |||
| 17 | import requests | ||
| 18 | |||
| 19 | |||
| 20 | logging.basicConfig(level=logging.INFO) | ||
| 21 | log = logging.getLogger(__name__) | ||
| 22 | |||
| 23 | GITHUB_API_BASE_URL = "https://api.github.com" | ||
| 24 | REPO_OWNER = "zed-industries" | ||
| 25 | REPO_NAME = "zed" | ||
| 26 | GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") | ||
| 27 | HEADERS = { | ||
| 28 | "Authorization": f"token {GITHUB_TOKEN}", | ||
| 29 | "Accept": "application/vnd.github+json" | ||
| 30 | } | ||
| 31 | REQUIRED_LABELS_PREFIXES = ["area:", "priority:", "frequency:"] | ||
| 32 | NEEDS_TRIAGE_LABEL = "state:needs triage" | ||
| 33 | |||
| 34 | |||
| 35 | def get_open_issues(repo, issue_type): | ||
| 36 | """Get open issues of certain type(s) via GitHub's REST API.""" | ||
| 37 | issues = [] | ||
| 38 | issues_url = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{repo}/issues" | ||
| 39 | |||
| 40 | log.info("Start fetching open issues from the GitHub API.") | ||
| 41 | params = { | ||
| 42 | "state": "open", | ||
| 43 | "type": issue_type, | ||
| 44 | "page": 1, | ||
| 45 | "per_page": 100, # worked fine despite the docs saying 30 | ||
| 46 | } | ||
| 47 | while True: | ||
| 48 | response = requests.get(issues_url, headers=HEADERS, params=params) | ||
| 49 | response.raise_for_status() | ||
| 50 | issues.extend(response.json()) | ||
| 51 | log.info(f"Fetched the next page, total issues so far: {len(issues)}.") | ||
| 52 | |||
| 53 | # is there a next page? | ||
| 54 | link_header = response.headers.get('Link', '') | ||
| 55 | if 'rel="next"' not in link_header: | ||
| 56 | break | ||
| 57 | params['page'] += 1 | ||
| 58 | |||
| 59 | log.info("Done fetching issues.") | ||
| 60 | return issues | ||
| 61 | |||
| 62 | |||
| 63 | def is_untriaged(issue): | ||
| 64 | issue_labels = [label['name'] for label in issue['labels']] | ||
| 65 | # don't want to overwrite existing state labels | ||
| 66 | no_state_label = not any(label.startswith('state:') for label in issue_labels) | ||
| 67 | # we want at least one label for each of the required prefixes | ||
| 68 | has_all_required_labels = all( | ||
| 69 | any(label.startswith(prefix) for label in issue_labels) | ||
| 70 | for prefix in REQUIRED_LABELS_PREFIXES | ||
| 71 | ) | ||
| 72 | # let's also assume if we managed to assign an issue it's triaged enough | ||
| 73 | no_assignee = not issue['assignee'] | ||
| 74 | return no_state_label and no_assignee and not has_all_required_labels | ||
| 75 | |||
| 76 | |||
| 77 | def label_issues(issues, label): | ||
| 78 | for issue in issues: | ||
| 79 | log.debug(f"Processing issue {issue['number']}.") | ||
| 80 | api_url_add_label = f"{GITHUB_API_BASE_URL}/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue['number']}/labels" | ||
| 81 | add_response = requests.post( | ||
| 82 | api_url_add_label, headers=HEADERS, json={"labels": [label]} | ||
| 83 | ) | ||
| 84 | add_response.raise_for_status() | ||
| 85 | log.info(f"Added label '{label}' to issue {issue['title']}.") | ||
| 86 | |||
| 87 | |||
| 88 | if __name__ == "__main__": | ||
| 89 | open_bugs = get_open_issues(REPO_NAME, "Bug") | ||
| 90 | open_crashes = get_open_issues(REPO_NAME, "Crash") | ||
| 91 | untriaged_issues = filter( | ||
| 92 | is_untriaged, itertools.chain(open_bugs, open_crashes)) | ||
| 93 | label_issues(untriaged_issues, label=NEEDS_TRIAGE_LABEL) | ||