bot/code_review_bot/tasks/base.py (92 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod import structlog import yaml logger = structlog.get_logger(__name__) class BaseTask: artifacts = [] route = None valid_states = ("completed", "failed") skipped_states = () extra_reviewers_groups = [] def __init__(self, task_id, task_status): self.id = task_id assert "task" in task_status, f"No task data for {self.id}" assert "status" in task_status, f"No status data for {self.id}" self.task = task_status["task"] self.status = task_status["status"] @property def run_id(self): return self.status["runs"][-1]["runId"] @property def name(self): """Short name used to identify the task""" return self.task["metadata"].get("name", "unknown") @property def display_name(self): """ Longer name used to describe the task to humans By default fallback to short name """ return self.name @property def state(self): return self.status["state"] @classmethod def build_from_route(cls, index_service, queue_service): """ Build the task instance from a configured Taskcluster route """ assert cls.route is not None, f"Missing route on {cls}" # Load its task id try: index = index_service.findTask(cls.route) task_id = index["taskId"] logger.info("Loaded task from route", cls=cls, task_id=task_id) except Exception as e: logger.warn("Failed loading task from route", route=cls.route, error=str(e)) return # Load the task & status description try: task_status = queue_service.status(task_id) task_status["task"] = queue_service.task(task_id) except Exception as e: logger.warn("Task not found", task=task_id, error=str(e)) return # Build the instance return cls(task_id, task_status) def load_artifact(self, queue_service, artifact_name): url = queue_service.buildUrl("getArtifact", self.id, self.run_id, artifact_name) # Allows HTTP_30x redirections retrieving the artifact response = queue_service.session.get(url, stream=True, allow_redirects=True) try: response.raise_for_status() except Exception as e: logger.warn( "Failed to read artifact", task_id=self.id, run_id=self.run_id, artifact=artifact_name, error=e, ) return None, True # Load artifact's data, either as JSON or YAML if artifact_name.endswith(".json"): content = response.json() elif artifact_name.endswith(".yml") or artifact_name.endswith(".yaml"): content = yaml.load_stream(response.text) else: # Json responses are automatically parsed into Python structures content = response.content or None return content, False def load_artifacts(self, queue_service): # Process only the supported final states # as some tasks do not always have relevant output if self.state in self.skipped_states: logger.info("Skipping task", state=self.state, id=self.id, name=self.name) return elif self.state not in self.valid_states: logger.warning( "Invalid task state", state=self.state, id=self.id, name=self.name ) return # Load relevant artifacts out = {} for artifact_name in self.artifacts: logger.info("Load artifact", task_id=self.id, artifact=artifact_name) content, skip = self.load_artifact(queue_service, artifact_name) if skip: continue out[artifact_name] = content return out class AnalysisTask(BaseTask, ABC): """ An analysis CI task running on Taskcluster """ def build_help_message(self, files): """ An optional help message aimed at developers to reproduce the issues detection A list of relative paths with issues is specified to build a precise message By default it's empty (None) """ def build_patches(self, artifacts): """ Some analyzers can provide a patch applicable by developers These patches are stored as Taskcluster artifacts and reported to developers Output is a list of tuple (patch name as str, patch content as str) """ return [] @abstractmethod def parse_issues(self, artifacts, revision): """ Given list of artifacts, return a list of Issue objects. """ class NoticeTask(BaseTask, ABC): """ A task that simply displays information. """ @abstractmethod def build_notice(self, artifacts, revision): """ Return multiline string containing information to display. """