Jaypore CI

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

commit f8affe4f0a3f0f61615d1e6236fdf973f853f915
parent 3abe3e251c15fb89ba2cbeca688932a42b03bb3d
Author: arjoonn <arjoonn@noreply.localhost>
Date:   Tue, 31 Jan 2023 06:54:33 +0000

Add TUI for walking through job logs (!31)

Branch auto created by JayporeCI

```jayporeci
╔ 🟢 : JayporeCI       [sha daee6208e0]
┏━ Docker
┃
┃ 🟢 : Jci             [53e256c0]   0: 9
┃ 🟢 : JciEnv          [0b07d8db]   0: 0
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Jobs
┃
┃ 🟢 : black           [d8e61a5a]   0: 0
┃ 🟢 : pylint          [6a810f5c]   0:10
┃ 🟢 : pytest          [298ca0e5]   0: 2
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Publish
┃
┃ 🟢 : DockerHubJci    [e0645215]   0:56
┃ 🟢 : DockerHubJcienv [e921564b]   1:11
┃ 🟢 : PublishDocs     [ab3329dd]   0: 7
┃ 🟢 : PublishPypi     [a41b730d]   0: 6
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```

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

Diffstat:
Mcicd/pre-push.sh | 2+-
Mdocs/source/index.rst | 14++++++++++++++
Mjaypore_ci/__main__.py | 3+++
Mjaypore_ci/executors/docker.py | 9+++++----
Ajaypore_ci/tui.css | 22++++++++++++++++++++++
Ajaypore_ci/tui.py | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpyproject.toml | 2+-
7 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/cicd/pre-push.sh b/cicd/pre-push.sh @@ -31,7 +31,7 @@ hook() { echo "JayporeCi: " JAYPORE_GITEA_TOKEN="${JAYPORE_GITEA_TOKEN:-$TOKEN}" docker run \ -d \ - --name jaypore_ci_$SHA \ + --name jayporeci__pipe__$SHA \ -e JAYPORE_GITEA_TOKEN \ -e JAYPORE_CODE_DIR=$JAYPORE_CODE_DIR \ -v /var/run/docker.sock:/var/run/docker.sock \ diff --git a/docs/source/index.rst b/docs/source/index.rst @@ -85,6 +85,20 @@ This would produce a CI report like:: - `1: 3` is the time taken by the job. +To see your pipelines on your machine you can run: + +.. code-blcok:: bash + + docker run \ + --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --workdir /app \ + bash -c 'python3 -m jaypore_ci' + + +This will open up a console where you can interact and explore the job logs. + + Examples ======== diff --git a/jaypore_ci/__main__.py b/jaypore_ci/__main__.py @@ -0,0 +1,3 @@ +from jaypore_ci.tui import Console + +Console().run() diff --git a/jaypore_ci/executors/docker.py b/jaypore_ci/executors/docker.py @@ -54,7 +54,9 @@ class Docker(Executor): if self.pipe_id is not None: self.delete_network() self.delete_all_jobs() - self.pipe_id = id(pipeline) + self.pipe_id = __check_output__( + "cat /proc/self/cgroup | grep name= | awk -F/ '{print $3}'" + ) self.pipeline = pipeline self.create_network() @@ -66,7 +68,7 @@ class Docker(Executor): """ Return a network name based on what the curent pipeline is. """ - return f"jaypore_{self.pipe_id}" if self.pipe_id is not None else None + return f"jayporeci__net__{self.pipe_id}" if self.pipe_id is not None else None def create_network(self): """ @@ -138,7 +140,7 @@ class Docker(Executor): for l in job.name.lower().replace(" ", "_") if l in "abcdefghijklmnopqrstuvwxyz_1234567890" ) - return f"{self.get_net()}_{name}" + return f"jayporeci__job__{self.pipe_id}__{name}" def run(self, job: "Job") -> str: """ @@ -146,7 +148,6 @@ class Docker(Executor): In case something goes wrong it will raise TriggerFailed """ assert self.pipe_id is not None, "Cannot run job if pipe id is not set" - self.pipe_id = id(job.pipeline) if self.pipe_id is None else self.pipe_id env_vars = [f"--env {key}={val}" for key, val in job.get_env().items()] trigger = [ "docker run -d", diff --git a/jaypore_ci/tui.css b/jaypore_ci/tui.css @@ -0,0 +1,22 @@ +Screen { + background: $surface-darken-1; +} + +#tree-view { + display: block; + scrollbar-gutter: stable; + overflow: auto; + width: auto; + height: 100%; + dock: left; +} + + + +#code-view { + overflow: auto scroll; + min-width: 100%; +} +#code { + width: auto; +} diff --git a/jaypore_ci/tui.py b/jaypore_ci/tui.py @@ -0,0 +1,110 @@ +import subprocess + +from rich.traceback import Traceback + +from textual import events +from textual.app import App, ComposeResult +from textual.containers import Container, Vertical +from textual.widgets import Tree, Footer, Header, Static + +HELP = """ +- The tree shows commit SHA values for pipelines that have run on this machine. +- Clicking on the SHA will: + - Toggle the list of jobs for that pipeline. + - Show pipeline logs +- Clicking on the job will show logs for that job +""" + + +def get_pipes_from_docker_ps(): + lines = ( + subprocess.check_output( + "docker ps -a", + shell=True, + stderr=subprocess.STDOUT, + ) + .decode() + .split("\n") + ) + + pipes = {} + PREFIX = "jayporeci__" + for line in lines: + if PREFIX not in line: + continue + kind, *details = line.split(PREFIX)[1].split("__") + cid = line.split(" ")[0] + if kind == "pipe": + if cid not in pipes: + pipes[cid] = {"sha": None, "jobs": [], "cid": None} + if pipes[cid]["sha"] is None: + pipes[cid]["sha"] = details[0][:8] + if pipes[cid]["cid"] is None: + pipes[cid]["cid"] = cid + elif kind == "job": + pipe_cid, name = details + pipe_cid = pipe_cid[:12] + if pipe_cid not in pipes: + pipes[pipe_cid] = {"sha": None, "jobs": [], "cid": None} + pipes[pipe_cid]["jobs"].append((cid[:12], name)) + return pipes + + +class Console(App): + """Textual CI Job browser app.""" + + CSS_PATH = "tui.css" + BINDINGS = [ + ("q", "quit", "Quit"), + ] + + def compose(self) -> ComposeResult: + """Compose our UI.""" + yield Header() + # Find the job tree + tree: Tree[dict] = Tree("JayporeCI", id="tree-view") + tree.root.expand() + pipes = get_pipes_from_docker_ps() + for pipe in pipes.values(): + pipe_node = tree.root.add(pipe["sha"], data=pipe) + for job in pipe["jobs"]: + job_cid, job_name = job + pipe_node.add_leaf(f"{job_cid[:4]}: {job_name}", data=job) + # --- + yield Container( + tree, + Vertical(Static(id="code", expand=True, markup=False), id="code-view"), + ) + yield Footer() + + def on_mount(self, event: events.Mount) -> None: # pylint: disable=unused-argument + self.query_one(Tree).show_root = False + self.query_one(Tree).focus() + + def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: + """Called when the user click a node in the job tree.""" + event.stop() + code_view = self.query_one("#code", Static) + data = event.node.data + cid = None + if isinstance(data, dict) and "cid" in data: + cid = name = data["cid"] + name = f"Pipeline for SHA: {data['sha']}" + elif isinstance(data, tuple): + cid, name = data + name = f"Job: {name}" + if cid is None: + code_view.update(HELP) + return + try: + logs = subprocess.check_output( + f"docker logs {cid}", + shell=True, + stderr=subprocess.STDOUT, + ).decode() + except Exception: # pylint: disable=broad-except + code_view.update(Traceback(theme="github-dark", width=None)) + self.sub_title = "ERROR" + else: + code_view.update(logs) + self.sub_title = name diff --git a/pyproject.toml b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jaypore_ci" -version = "0.2" +version = "0.2.2" description = "" authors = ["arjoonn sharma <arjoonn.94@gmail.com>"]