Jaypore CI

> Jaypore CI: Minimal, Offline, Local CI system.
Log | Files | Refs | README

commit 3abe3e251c15fb89ba2cbeca688932a42b03bb3d
parent 75b028a4a87d3165e733918c1a4d93e8bf463efd
Author: arjoonn <arjoonn@noreply.localhost>
Date:   Tue, 31 Jan 2023 02:19:55 +0000

Add support for github remote (!30)

Branch auto created by JayporeCI

```jayporeci
╔ 🟢 : JayporeCI       [sha f2f319af5a]
┏━ Docker
┃
┃ 🟢 : Jci             [bba52dc0]   1:49
┃ 🟢 : JciEnv          [0378dfd5]   1:41
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Jobs
┃
┃ 🟢 : black           [b11e858b]   0: 0
┃ 🟢 : pylint          [09e63926]   0: 7
┃ 🟢 : pytest          [9b152c06]   0: 2
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Publish
┃
┃ 🟢 : DockerHubJci    [236fbde6]  31:23
┃ 🟢 : DockerHubJcienv [d387fd54]  26:48
┃ 🟢 : PublishDocs     [f0df7c7e]   0: 6
┃ 🟢 : PublishPypi     [6ba561b6]   0: 7
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```

Co-authored-by: arjoonn sharma <arjoonn@midpathsoftware.com>
Reviewed-on: https://gitea.midpathsoftware.com/midpath/jaypore_ci/pulls/30

Diffstat:
Mjaypore_ci/jci.py | 12++++++++----
Mjaypore_ci/remotes/__init__.py | 1+
Ajaypore_ci/remotes/github.py | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpyproject.toml | 2+-
4 files changed, 194 insertions(+), 5 deletions(-)

diff --git a/jaypore_ci/jci.py b/jaypore_ci/jci.py @@ -93,9 +93,12 @@ class Job: # pylint: disable=too-many-instance-attributes Status.TIMEOUT: "warning", Status.SKIPPED: "warning", }[self.pipeline.get_status()] - self.pipeline.remote.publish( - self.pipeline.reporter.render(self.pipeline), status - ) + report = self.pipeline.reporter.render(self.pipeline) + try: + self.pipeline.remote.publish(report, status) + except Exception as e: # pylint: disable=broad-except + self.logging().exeception(e) + return report def trigger(self): """ @@ -372,7 +375,8 @@ class Pipeline: # pylint: disable=too-many-instance-attributes break self.logging().error("Pipeline passed") if job is not None: - job.update_report() + report = job.update_report() + self.logging().info("Report:", report=report) @contextmanager def stage(self, name, **kwargs): diff --git a/jaypore_ci/remotes/__init__.py b/jaypore_ci/remotes/__init__.py @@ -1,2 +1,3 @@ from .mock import Mock from .gitea import Gitea +from .github import Github diff --git a/jaypore_ci/remotes/github.py b/jaypore_ci/remotes/github.py @@ -0,0 +1,184 @@ +""" +A github remote git host. + +This is used to report pipeline status to the remote. +""" +import os +import subprocess +from pathlib import Path +from urllib.parse import urlparse + +import requests + +from jaypore_ci.interfaces import Remote +from jaypore_ci.logging import logger + + +class Github(Remote): # pylint: disable=too-many-instance-attributes + """ + The remote implementation for github. + """ + + def __headers__(self): + return { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json", + "X-Github-Api-Version": "2022-11-28", + } + + @classmethod + def from_env(cls): + """ + Creates a remote instance from the environment. + It will: + + - Find the remote location using `git remote`. + - Find the current branch + - Create a new pull request for that branch + - Allow posting updates using the gitea token provided + """ + remote = ( + subprocess.check_output( + "git remote -v | grep push | awk '{print $2}'", shell=True + ) + .decode() + .strip() + ) + assert "https://github.com" in remote, "Only https remotes supported" + assert ".git" in remote + remote = urlparse(remote) + branch = ( + subprocess.check_output( + r"git branch | grep \* | awk '{print $2}'", shell=True + ) + .decode() + .strip() + ) + os.environ["JAYPORE_COMMIT_BRANCH"] = branch + sha = subprocess.check_output("git rev-parse HEAD", shell=True).decode().strip() + os.environ["JAYPORE_COMMIT_SHA"] = sha + owner = Path(remote.path).parts[1] + repo = Path(remote.path).parts[2].replace(".git", "") + token = os.environ["JAYPORE_GITHUB_TOKEN"] + return cls( + root="https://api.github.com", + owner=owner, + repo=repo, + branch=branch, + token=token, + sha=sha, + ) + + def __init__( + self, *, root, owner, repo, token, **kwargs + ): # pylint: disable=too-many-arguments + super().__init__(**kwargs) + # --- customer + self.root = root + self.api = root + self.owner = owner + self.repo = repo + self.token = token + self.timeout = 10 + self.base_branch = "main" + + def logging(self): + """ + Return's a logging instance with information about gitea bound to it. + """ + return logger.bind( + root=self.root, owner=self.owner, repo=self.repo, branch=self.branch + ) + + def get_pr_id(self): + """ + Returns the pull request ID for the current branch. + """ + r = requests.post( + f"{self.api}/repos/{self.owner}/{self.repo}/pulls", + headers=self.__headers__(), + timeout=self.timeout, + json={ + "base": self.base_branch, + "body": "Branch auto created by JayporeCI", + "head": self.branch, + "title": self.branch, + }, + ) + self.logging().debug("Create PR", status_code=r.status_code) + if r.status_code == 201: + return r.json()["number"] + r = requests.get( + f"{self.api}/repos/{self.owner}/{self.repo}/pulls", + headers=self.__headers__(), + timeout=self.timeout, + json={"base": self.base_branch, "head": self.branch, "draft": True}, + ) + self.logging().debug("Get PR", status_code=r.status_code) + if r.status_code == 200: + if len(r.json()) == 1: + return r.json()[0]["number"] + self.logging().debug( + "Failed github api", + api=self.api, + owner=self.owner, + repo=self.repo, + token=self.token, + branch=self.branch, + status=r.status_code, + response=r.text, + ) + raise Exception(r) + + def publish(self, report: str, status: str): + """ + Will publish the report to the remote. + + :param report: Report to write to remote. + :param status: One of ["pending", "success", "error", "failure"] + This is the dot/tick next to each commit in gitea. + """ + assert status in ("pending", "success", "error", "failure") + issue_id = self.get_pr_id() + # Get existing PR body + r = requests.get( + f"{self.api}/repos/{self.owner}/{self.repo}/pulls/{issue_id}", + timeout=self.timeout, + headers=self.__headers__(), + ) + self.logging().debug("Get existing body", status_code=r.status_code) + assert r.status_code == 200 + body = r.json()["body"] + body = (line for line in body.split("\n")) + prefix = [] + for line in body: + if "```jayporeci" in line: + prefix = prefix[:-1] + break + prefix.append(line) + while prefix and prefix[-1].strip() == "": + prefix = prefix[:-1] + prefix.append("") + # Post new body with report + report = "\n".join(prefix) + "\n" + report + r = requests.patch( + f"{self.api}/repos/{self.owner}/{self.repo}/pulls/{issue_id}", + json={"body": report}, + timeout=self.timeout, + headers=self.__headers__(), + ) + self.logging().debug("Published new report", status_code=r.status_code) + # Set commit status + r = requests.post( + f"{self.api}/repos/{self.owner}/{self.repo}/statuses/{self.sha}", + json={ + "context": "JayporeCi", + "description": f"Pipeline status is: {status}", + "state": status, + }, + timeout=self.timeout, + headers=self.__headers__(), + ) + self.logging().debug( + "Published new status", status=status, status_code=r.status_code + ) diff --git a/pyproject.toml b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jaypore_ci" -version = "0.1.14" +version = "0.2" description = "" authors = ["arjoonn sharma <arjoonn.94@gmail.com>"]