From e9d2e9f5970c0e6f5546218af717927fd5bc918f Mon Sep 17 00:00:00 2001 From: Yang Chiu Date: Wed, 14 Aug 2024 18:54:26 +0800 Subject: [PATCH] ci: migrate github bot from zenhub to github project Signed-off-by: Yang Chiu --- github-bot/Dockerfile | 2 +- github-bot/longhorn_github_bot/__init__.py | 117 ++++++++++++++------- github-bot/longhorn_github_bot/config.py | 3 +- github-bot/requirements.txt | 2 +- 4 files changed, 81 insertions(+), 43 deletions(-) diff --git a/github-bot/Dockerfile b/github-bot/Dockerfile index db874bb..11c4c72 100644 --- a/github-bot/Dockerfile +++ b/github-bot/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.5-alpine3.12 +FROM python:3.10.14-alpine COPY requirements.txt /srv WORKDIR /srv diff --git a/github-bot/longhorn_github_bot/__init__.py b/github-bot/longhorn_github_bot/__init__.py index d7307db..918cd68 100644 --- a/github-bot/longhorn_github_bot/__init__.py +++ b/github-bot/longhorn_github_bot/__init__.py @@ -17,14 +17,14 @@ FLASK_USERNAME = generate_password_hash(config('flask_username')) GITHUB_OWNER = config('github_owner') GITHUB_REPOSITORY = config('github_repository') -ZENHUB_PIPELINE = config('zenhub_pipeline') -ZENHUB_PIPELINE_TRIAGE = "Triage" -ZENHUB_PIPELINE_ICEBOX = "Icebox" -ZENHUB_PIPELINE_BACKLOG = "Backlog" -ZENHUB_WORKSPACE_NAME = "Longhorn Main Board" +PROJECT_PIPELINE = config('project_pipeline') +PROJECT_PIPELINE_TRIAGE = "Triage" +PROJECT_PIPELINE_ICEBOX = "Icebox" +PROJECT_PIPELINE_BACKLOG = "Backlog" +PROJECT_NAME = "Longhorn Sprint" -ADD_TO_SPRINT_PIPELINES = {ZENHUB_PIPELINE_TRIAGE, ZENHUB_PIPELINE_ICEBOX, ZENHUB_PIPELINE_BACKLOG} -REMOVE_FROM_SPRINT_PIPELINES = {ZENHUB_PIPELINE_TRIAGE, ZENHUB_PIPELINE_ICEBOX} +ADD_TO_SPRINT_PIPELINES = {PROJECT_PIPELINE_TRIAGE, PROJECT_PIPELINE_ICEBOX, PROJECT_PIPELINE_BACKLOG} +REMOVE_FROM_SPRINT_PIPELINES = {PROJECT_PIPELINE_TRIAGE, PROJECT_PIPELINE_ICEBOX} # From https://docs.python.org/3/howto/logging.html#logging-to-a-file numeric_level = getattr(logging, FLASK_LOGLEVEL.upper(), None) @@ -34,9 +34,8 @@ g = Github(config('github_token')) -gql_url = "https://api.zenhub.com/public/graphql" -gql_headers = {"Authorization": f"Bearer {config('zenhub_graphql_token')}"} - +gql_url = "https://api.github.com/graphql" +gql_headers = {"Authorization": f"Bearer {config('github_token')}"} @auth.verify_password def verify_password(username, password): @@ -44,39 +43,54 @@ def verify_password(username, password): return username -@app.route('/zenhub', methods=['POST']) +@app.route('/project', methods=['POST']) @auth.login_required -def zenhub(): - form = request.form - organization = form.get('organization') - repo = form.get('repo') - if organization != GITHUB_OWNER or repo != GITHUB_REPOSITORY: - app.logger.debug("webhook event targets %s/%s, ignoring", organization, repo) - else: - webhook_type = request.form.get('type') - if webhook_type == 'issue_transfer': - issue_transfer(form) - else: - app.logger.debug('unhandled webhook type %s, ignoring', webhook_type) - +def project(): + json = request.get_json() + print(json) + + organization = json['organization']['login'] + if organization != GITHUB_OWNER: + app.logger.debug(f"webhook event targets {organization}, ignoring") + return { + 'message': 'webhook handled successfully' + }, 200 + + if 'action' not in json: + app.logger.debug(f"unhandled webhook type, ignoring") + return { + 'message': 'webhook handled successfully' + }, 200 + + webhook_type = json['action'] + if webhook_type != 'edited': + app.logger.debug(f"unhandled webhook type {webhook_type}, ignoring") + return { + 'message': 'webhook handled successfully' + }, 200 + + changed_field = json['changes']['field_value']['field_name'] + if changed_field != 'Status': + app.logger.debug(f"unhandled changed field {changed_field}, ignoring") + return { + 'message': 'webhook handled successfully' + }, 200 + + issue_transfer(json) return { 'message': 'webhook handled successfully' }, 200 -def issue_transfer(form): - issue_number = form.get('issue_number') - try: - issue_number = int(issue_number) - except ValueError: - app.logger.warning('could not parse issue_number %s as int', issue_number) - return +def issue_transfer(json): - to_pipeline = form.get('to_pipeline_name') - from_pipeline = form.get('from_pipeline_name') + issue_node_id = json['projects_v2_item']['content_node_id'] - if to_pipeline == ZENHUB_PIPELINE: - add_checklist(issue_number) + to_pipeline = json['changes']['field_value']['to']['name'] + from_pipeline = json['changes']['field_value']['to']['name'] + + if to_pipeline == PROJECT_PIPELINE: + add_checklist(issue_node_id) if from_pipeline in ADD_TO_SPRINT_PIPELINES and to_pipeline not in ADD_TO_SPRINT_PIPELINES: add_to_sprint(issue_number) @@ -87,8 +101,10 @@ def issue_transfer(form): return -def add_checklist(issue_number): - repo = g.get_repo('{}/{}'.format(GITHUB_OWNER, GITHUB_REPOSITORY)) +def add_checklist(issue_node_id): + repo = g.get_repo(f"{GITHUB_OWNER}/{GITHUB_REPOSITORY}") + print(f"get issue with number with node id: {issue_node_id}") + issue_number = query_issue_number(issue_node_id) issue = repo.get_issue(issue_number) comments = issue.get_comments() found = False @@ -103,13 +119,13 @@ def add_checklist(issue_number): def add_to_sprint(issue_number): - active_sprint_id, repository_gh_id = query_active_sprint_id_and_repository_gh_id(ZENHUB_WORKSPACE_NAME) + active_sprint_id, repository_gh_id = query_active_sprint_id_and_repository_gh_id(PROJECT_NAME) issue_id = query_issue_id(repository_gh_id, issue_number) add_issue_to_sprint(issue_id, active_sprint_id) def remove_from_sprint(issue_number): - active_sprint_id, repository_gh_id = query_active_sprint_id_and_repository_gh_id(ZENHUB_WORKSPACE_NAME) + active_sprint_id, repository_gh_id = query_active_sprint_id_and_repository_gh_id(PROJECT_NAME) issue_id = query_issue_id(repository_gh_id, issue_number) remove_issue_from_sprint(issue_id, active_sprint_id) @@ -149,6 +165,29 @@ def query_active_sprint_id_and_repository_gh_id(workspace_name): repository_gh_id = response.json()["data"]["viewer"]["searchWorkspaces"]["nodes"][0]["defaultRepository"]["ghId"] return active_sprint_id, repository_gh_id +def query_issue_number(issue_node_id): + query = """ +query($issueId: ID!) { + node(id: $issueId) { + ... on Issue { + number + } + } +} + """ + variables = { + "issueId": issue_node_id + } + response = requests.post(url=gql_url, + headers=gql_headers, + json={ + "query": query, + "variables": variables + }) + app.logger.debug("query_issue_number response %s %s", response.status_code, response.json()) + issue_number = response.json()["data"]["node"]["number"] + print(f"issue_number = {issue_number}") + return issue_number def query_issue_id(repository_gh_id, issue_number): query = """ diff --git a/github-bot/longhorn_github_bot/config.py b/github-bot/longhorn_github_bot/config.py index 40ea9af..9eda43a 100644 --- a/github-bot/longhorn_github_bot/config.py +++ b/github-bot/longhorn_github_bot/config.py @@ -12,8 +12,7 @@ class BotConfig(RequiredConfigMixin): required_config.add_option('github_repository', parser=str, default='longhorn', doc='Set the name of the target ' 'GitHub repository.') required_config.add_option('github_token', parser=str, doc='Set the token of the GitHub machine user.') - required_config.add_option('zenhub_graphql_token', parser=str, doc='Set the token of the Zenhub GraphQL API') - required_config.add_option('zenhub_pipeline', parser=str, default='Review', doc='Set the target ZenHub pipeline to ' + required_config.add_option('project_pipeline', parser=str, default='Review', doc='Set the target project pipeline to ' 'handle events for.') diff --git a/github-bot/requirements.txt b/github-bot/requirements.txt index 9ecd7a1..559df82 100644 --- a/github-bot/requirements.txt +++ b/github-bot/requirements.txt @@ -5,4 +5,4 @@ werkzeug==2.0.2 itsdangerous==2.0.1 Flask-HTTPAuth==4.1.0 Gunicorn==20.0.4 -PyGitHub==1.51 +PyGitHub==2.3.0