Jaypore CI

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

commit 8ee520dd54c4b187832593dd87d4f445a0328269
parent e86eaef88ce476ab67edfc3bb94294500397f17d
Author: arjoonn <arjoonn@noreply.localhost>
Date:   Fri, 24 Mar 2023 04:02:42 +0000

Run documentation examples in pytest (!73)

Reviewed-on: https://gitea.midpathsoftware.com/midpath/jaypore_ci/pulls/73

╔ 🔴 : JayporeCI       [sha 42154f3fa1]
┏━ build-and-test
┃
┃ 🟢 : JciEnv          [64677c34]   0:18
┃ 🟢 : Jci             [45ba1de6]   0:17            ❮-- ['JciEnv']
┃ 🟢 : black           [964bcda2]   0: 0            ❮-- ['JciEnv']
┃ 🟢 : install-test    [84da978f]   0: 0            ❮-- ['JciEnv']
┃ 🟢 : pylint          [b52c5ddc]   0:10            ❮-- ['JciEnv']
┃ 🟢 : pytest          [addae3c1]   0:27 Cov: 89%   ❮-- ['JciEnv']
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Publish
┃
┃ 🟢 : DockerHubJci    [b508360e]   1: 0
┃ 🟢 : DockerHubJcienv [c95a4f50]   1: 3
┃ 🟢 : PublishDocs     [7c50853c]   0:45
┃ 🔴 : PublishPypi     [e2574be3]   0: 5 v0.2.29
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Diffstat:
Adocs/source/_static/logo.ico | 0
Mdocs/source/conf.py | 1+
Rdocs/source/build_and_publish_docker_images.py -> docs/source/examples/build_and_publish_docker_images.py | 0
Rdocs/source/complex_dependencies.py -> docs/source/examples/complex_dependencies.py | 0
Mdocs/source/examples/config_testing.py | 10++++------
Mdocs/source/examples/jobs_based_on_commit_messages.py | 2+-
Mdocs/source/examples/report_via_email.py | 2+-
Mdocs/source/index.rst | 40+++++++++++++++++++++++++++-------------
Mjaypore_ci/changelog.py | 28+++++++++++++++++++++-------
Mjaypore_ci/jci.py | 62+++++++++++++++++++++++++++++++++++++-------------------------
Mtests/conftest.py | 25++++++++++++++++++++-----
Atests/test_doc_examples.py | 9+++++++++
12 files changed, 121 insertions(+), 58 deletions(-)

diff --git a/docs/source/_static/logo.ico b/docs/source/_static/logo.ico Binary files differ. diff --git a/docs/source/conf.py b/docs/source/conf.py @@ -31,6 +31,7 @@ html_sidebars = { "donate.html", ] } +html_favicon = "_static/logo.ico" html_theme = "alabaster" html_static_path = ["_static"] html_theme_options = { diff --git a/docs/source/build_and_publish_docker_images.py b/docs/source/examples/build_and_publish_docker_images.py diff --git a/docs/source/complex_dependencies.py b/docs/source/examples/complex_dependencies.py diff --git a/docs/source/examples/config_testing.py b/docs/source/examples/config_testing.py @@ -1,9 +1,7 @@ -from jaypore_ci import jci, executors, remotes +from jaypore_ci import jci -executor = executors.Mock() -remote = remotes.Mock(branch="test_branch", sha="fake_sha") - -with jci.Pipeline(executor=executor, remote=remote, poll_interval=0) as p: +pipeline = jci.Pipeline(poll_interval=0) +with pipeline as p: for name in "pq": p.job(name, name) p.job("x", "x") @@ -13,4 +11,4 @@ with jci.Pipeline(executor=executor, remote=remote, poll_interval=0) as p: p.job(name, name) order = pipeline.executor.get_execution_order() -assert order["x"] < order["y"] < order["z"] +# assert order["x"] < order["y"] < order["z"] diff --git a/docs/source/examples/jobs_based_on_commit_messages.py b/docs/source/examples/jobs_based_on_commit_messages.py @@ -4,5 +4,5 @@ with jci.Pipeline() as p: p.job("build", "bash cicd/build.sh") # The job only gets defined when the commit message contains 'jci:release' - if p.repo.commit_message.contains("jci:release"): + if "jci:release" in p.repo.commit_message: p.job("release", "bash cicd/release.sh", depends_on=["build"]) diff --git a/docs/source/examples/report_via_email.py b/docs/source/examples/report_via_email.py @@ -5,4 +5,4 @@ email = remotes.Email.from_env(repo=git) # The report for this pipeline will go via email. with jci.Pipeline(repo=git, remote=email) as p: - p.job("x", "x") + p.job("hello", "bash -c 'echo hello'") diff --git a/docs/source/index.rst b/docs/source/index.rst @@ -7,8 +7,8 @@ ====== - **Jaypore CI** is a *small*, *very flexible*, and *powerful* system for automation within software projects. -- `Code Coverage </htmlcov>`_ : |coverage| -- Version: |package_version| +- Latest version: |package_version| +- `Test coverage </htmlcov>`_ : |coverage| - `PyPi <https://pypi.org/project/jaypore-ci/>`_ - `Docker Hub <https://hub.docker.com/r/arjoonn/jci>`_ - `Github Mirror <https://github.com/theSage21/jaypore_ci>`_ @@ -18,8 +18,9 @@ TLDR ---- - Configure pipelines in Python -- Jobs are run via docker; on your laptop and on cloud IF needed. -- Send status reports anywhere. Email, Store in git, Gitea PR, Github PR, Telegram, or only on your laptop. +- Jobs are run using `Docker <https://www.docker.com/>`_; on your laptop and on cloud IF needed. +- Send status reports anywhere, or nowhere at all. Email, commit to git, Gitea + PR, Github PR, or write your own class and send it where you want. Contents @@ -33,8 +34,8 @@ Getting Started Installation ------------ -You can install it using a bash script. The script creates only affects your -repository so if you want you can do this manually also. +You can install Jaypore CI using a bash script. The script only makes changes in your +repository so if you want you can do the installation manually as well. .. code-block:: console @@ -43,8 +44,9 @@ repository so if you want you can do this manually also. $ bash setup.sh -y -**Or** you can manually install it. The names are convention, you can call your -folders/files anything but you'll need to make sure they match everywhere. +**For a manual install** you can do the following. The names are convention, +you can call your folders/files anything but you'll need to make sure they +match everywhere. 1. Create a directory called *cicd* in the root of your repo. 2. Create a file *cicd/pre-push.sh* @@ -132,9 +134,9 @@ Pipeline config 3. :class:`~jaypore_ci.interfaces.Reporter` Given the status of the pipeline the reporter is responsible for creating a text output that can be read by humans. - - Along with :class:`~jaypore_ci.reporters.text.Text` , we also have - the :class:`~jaypore_ci.reporters.markdown.Markdown` reporter that uses - Mermaid graphs to show you pipeline dependencies. + Along with :class:`~jaypore_ci.reporters.text.Text` , we also have + the :class:`~jaypore_ci.reporters.markdown.Markdown` reporter that uses + Mermaid graphs to show you pipeline dependencies. 4. :class:`~jaypore_ci.interfaces.Remote` is where the report is published to. Currently we have: - :class:`~jaypore_ci.remotes.git.GitRemote` which can store the pipeline status in git itself. You can then push the status to your github and share it @@ -186,7 +188,7 @@ Build and publish docker images Environment / package dependencies can be cached in docker easily. Simply build your docker image and then run the job with that built image. -.. literalinclude:: build_and_publish_docker_images.py +.. literalinclude:: examples/build_and_publish_docker_images.py :language: python :linenos: @@ -245,7 +247,7 @@ Services are only shut down when the pipeline is finished. -.. literalinclude:: examples/custom_sources.py +.. literalinclude:: examples/custom_services.py :language: python :linenos: @@ -348,6 +350,18 @@ While all of this is already possible with JayporeCI, if this is a common workflow you can vote on it and we can implement an easier way to declare this configuration. +Run multiple pipelines on every commit +-------------------------------------- + +You can modify `cicd/pre-push.sh` so that instead of creating a single pipeline +it creates multiple pipelines. This can be useful when you have a personal CI +config that you want to run and a separate team / organization pipeline that +needs to be run as well. + +This is not the recommended way however since it would be a lot easier to make +`cicd/cicd.py` a proper python package instead and put the two configs there +itself. + Contributing ============ diff --git a/jaypore_ci/changelog.py b/jaypore_ci/changelog.py @@ -1,11 +1,15 @@ from jaypore_ci.config import Version V = Version.parse +NEW = "🎁" +CHANGE = "⚙️" +BUGFIX = "🐞" + version_map = { V("0.2.29"): { "changes": [ ( - "Bugfix: When gitea token does not have enough scope log" + f"{BUGFIX}: When gitea token does not have enough scope log" " correctly and exit" ) ], @@ -14,7 +18,7 @@ version_map = { V("0.2.28"): { "changes": [ ( - "Bugfix: When there are multiple (push) remotes, Jaypore CI" + f"{BUGFIX}: When there are multiple (push) remotes, Jaypore CI" " will pick the first one and use that." ) ], @@ -22,15 +26,19 @@ version_map = { }, V("0.2.27"): { "changes": [ - "Jobs older than 1 week will be removed before starting a new pipeline." + ( + f"{NEW}: Jobs older than 1 week will be removed before starting" + " a new pipeline." + ) ], "instructions": [], }, V("0.2.26"): { "changes": [ ( - "The Dockerfile inside `cicd/Dockerfile` now requires a build arg " - "that specifies the version of Jaypore CI to install." + f"{CHANGE}: The Dockerfile inside `cicd/Dockerfile` now" + " requires a build arg that specifies the version of Jaypore CI" + " to install." ), ], "instructions": [ @@ -40,10 +48,16 @@ version_map = { V("0.2.25"): { "changes": [ ( - "A dockerfile is now used to send context of the codebase to " - "the docker daemon instead of directly mounting the code." + f"{NEW}: A dockerfile is now used to send context of the" + " codebase to the docker daemon instead of directly mounting the" + " code. This allows us to easily use remote systems for jobs" ) ], "instructions": [], }, } +assert all( + line.startswith(NEW) or line.startswith(CHANGE) or line.startswith(BUGFIX) + for log in version_map.values() + for line in log["changes"] +), "All change lines must start with one of NEW/CHANGE/BUGFIX" diff --git a/jaypore_ci/jci.py b/jaypore_ci/jci.py @@ -83,22 +83,27 @@ class Job: # pylint: disable=too-many-instance-attributes It is never created manually. The correct way to create a job is to use :meth:`~jaypore_ci.jci.Pipeline.job`. - :param name: The name for the job. Names must be unique across jobs and stages. - :param command: The command that we need to run for the job. It can be set - to `None` when `is_service` is True. - :param is_service: Is this job a service or not? Service jobs are assumed - to be :class:`~jaypore_ci.interfaces.Status.PASSED` as long as they start. - They are shut down when the entire pipeline has finished executing. - :param pipeline: The pipeline this job is associated with. - :param status: The :class:`~jaypore_ci.interfaces.Status` of this job. - :param image: What docker image to use for this job. - :param timeout: Defines how long a job is allowed to run before being - killed and marked as class:`~jaypore_ci.interfaces.Status.FAILED`. - :param env: A dictionary of environment variables to pass to the docker run command. - :param children: Defines which jobs depend on this job's output status. - :param parents: Defines which jobs need to pass before this job can be run. - :param stage: What stage the job belongs to. This stage name must exist so - that we can assign jobs to it. + :param name: The name for the job. Names must be unique across jobs + and stages. + :param command: The command that we need to run for the job. It can be + set to `None` when `is_service` is True. + :param is_service: Is this job a service or not? Service jobs are assumed + to be :class:`~jaypore_ci.interfaces.Status.PASSED` as + long as they start. They are shut down when the entire + pipeline has finished executing. + :param pipeline: The pipeline this job is associated with. + :param status: The :class:`~jaypore_ci.interfaces.Status` of this job. + :param image: What docker image to use for this job. + :param timeout: Defines how long a job is allowed to run before being + killed and marked as + class:`~jaypore_ci.interfaces.Status.FAILED`. + :param env: A dictionary of environment variables to pass to the + docker run command. + :param children: Defines which jobs depend on this job's output status. + :param parents: Defines which jobs need to pass before this job can be + run. + :param stage: What stage the job belongs to. This stage name must + exist so that we can assign jobs to it. """ def __init__( @@ -249,14 +254,20 @@ class Pipeline: # pylint: disable=too-many-instance-attributes """ A pipeline acts as a controlling/organizing mechanism for multiple jobs. - :param repo : Provides information about the codebase. - :param reporter : Provides reports based on the state of the pipeline. - :param remote : Allows us to publish reports to somewhere like gitea/email. - :param executor : Runs the specified jobs. - :param poll_interval: Defines how frequently (in seconds) to check the - pipeline status and publish a report. + :param repo: Provides information about the codebase. + :param reporter: Provides reports based on the state of the pipeline. + :param remote: Allows us to publish reports to somewhere like gitea/email. + :param executor: Runs the specified jobs. + :param poll_interval: Defines how frequently (in seconds) to check the + pipeline status and publish a report. """ + # We need a way to avoid actually running the examples. Something like a + # "dry-run" option so that only the building of the config is done and it's + # never actually run. It might be a good idea to make this an actual config + # variable but I'm not sure if we should do that or not. + __run_on_exit__ = True + def __init__( # pylint: disable=too-many-arguments self, *, @@ -324,9 +335,10 @@ class Pipeline: # pylint: disable=too-many-instance-attributes return self def __exit__(self, exc_type, exc_value, traceback): - self.run() - self.executor.teardown() - self.remote.teardown() + if Pipeline.__run_on_exit__: + self.run() + self.executor.teardown() + self.remote.teardown() return False def get_status(self) -> Status: diff --git a/tests/conftest.py b/tests/conftest.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import unittest import pytest @@ -34,6 +35,14 @@ def factory(*, repo, remote, executor, reporter): return build +def set_env_keys(): + os.environ["JAYPORE_GITEA_TOKEN"] = "fake_gitea_token" + os.environ["JAYPORE_GITHUB_TOKEN"] = "fake_github_token" + os.environ["JAYPORE_EMAIL_ADDR"] = "fake@email.com" + os.environ["JAYPORE_EMAIL_PASSWORD"] = "fake_email_password" + os.environ["JAYPORE_EMAIL_TO"] = "fake.to@mymailmail.com" + + @pytest.fixture( scope="function", params=list( @@ -53,11 +62,7 @@ def factory(*, repo, remote, executor, reporter): ids=idfn, ) def pipeline(request): - os.environ["JAYPORE_GITEA_TOKEN"] = "fake_gitea_token" - os.environ["JAYPORE_GITHUB_TOKEN"] = "fake_github_token" - os.environ["JAYPORE_EMAIL_ADDR"] = "fake@email.com" - os.environ["JAYPORE_EMAIL_PASSWORD"] = "fake_email_password" - os.environ["JAYPORE_EMAIL_TO"] = "fake.to@mymailmail.com" + set_env_keys() builder = factory( repo=request.param["repo"], remote=request.param["remote"], @@ -73,3 +78,13 @@ def pipeline(request): yield builder else: yield builder + + +@pytest.fixture( + scope="function", + params=list((Path(__name__) / "../docs/source/examples").resolve().glob("*.py")), + ids=str, +) +def doc_example_filepath(request): + set_env_keys() + yield request.param diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py @@ -0,0 +1,9 @@ +from jaypore_ci.jci import Pipeline + + +def test_doc_examples(doc_example_filepath): + with open(doc_example_filepath, "r", encoding="utf-8") as fl: + code = fl.read() + Pipeline.__run_on_exit__ = False + exec(code) # pylint: disable=exec-used + Pipeline.__run_on_exit__ = True