jenkinsctl 0.0.8__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jenkinsctl might be problematic. Click here for more details.

Files changed (36) hide show
  1. {jenkinsctl-0.0.8 → jenkinsctl-1.0.0}/PKG-INFO +28 -15
  2. {jenkinsctl-0.0.8 → jenkinsctl-1.0.0}/README.md +3 -1
  3. jenkinsctl-1.0.0/jenkinsctl/cli.py +158 -0
  4. jenkinsctl-1.0.0/jenkinsctl/commands/__init__.py +0 -0
  5. jenkinsctl-1.0.0/jenkinsctl/commands/build.py +46 -0
  6. jenkinsctl-1.0.0/jenkinsctl/commands/config.py +15 -0
  7. jenkinsctl-1.0.0/jenkinsctl/commands/enable_completion.py +22 -0
  8. jenkinsctl-1.0.0/jenkinsctl/commands/jobs.py +18 -0
  9. jenkinsctl-1.0.0/jenkinsctl/commands/json.py +14 -0
  10. jenkinsctl-1.0.0/jenkinsctl/commands/list.py +62 -0
  11. jenkinsctl-1.0.0/jenkinsctl/commands/logs.py +12 -0
  12. jenkinsctl-1.0.0/jenkinsctl/commands/rebuild.py +14 -0
  13. jenkinsctl-1.0.0/jenkinsctl/configs/__init__.py +0 -0
  14. jenkinsctl-1.0.0/jenkinsctl/configs/logging_config.py +19 -0
  15. jenkinsctl-1.0.0/jenkinsctl/configs/session.py +21 -0
  16. jenkinsctl-1.0.0/jenkinsctl/jenkins/cli_helper.py +25 -0
  17. jenkinsctl-1.0.0/jenkinsctl/jenkins/commons.py +9 -0
  18. jenkinsctl-1.0.0/jenkinsctl/jenkins/console_util.py +31 -0
  19. jenkinsctl-1.0.0/jenkinsctl/jenkins/job.py +69 -0
  20. jenkinsctl-1.0.0/jenkinsctl/jenkins/utils.py +37 -0
  21. jenkinsctl-1.0.0/pyproject.toml +36 -0
  22. jenkinsctl-0.0.8/jenkinsctl/commons.py +0 -8
  23. jenkinsctl-0.0.8/jenkinsctl/jbuild.py +0 -113
  24. jenkinsctl-0.0.8/jenkinsctl/jenkins.py +0 -53
  25. jenkinsctl-0.0.8/jenkinsctl/jget_config.py +0 -41
  26. jenkinsctl-0.0.8/jenkinsctl.egg-info/PKG-INFO +0 -114
  27. jenkinsctl-0.0.8/jenkinsctl.egg-info/SOURCES.txt +0 -15
  28. jenkinsctl-0.0.8/jenkinsctl.egg-info/dependency_links.txt +0 -1
  29. jenkinsctl-0.0.8/jenkinsctl.egg-info/entry_points.txt +0 -2
  30. jenkinsctl-0.0.8/jenkinsctl.egg-info/requires.txt +0 -4
  31. jenkinsctl-0.0.8/jenkinsctl.egg-info/top_level.txt +0 -2
  32. jenkinsctl-0.0.8/pyproject.toml +0 -32
  33. jenkinsctl-0.0.8/setup.cfg +0 -4
  34. {jenkinsctl-0.0.8 → jenkinsctl-1.0.0}/LICENSE +0 -0
  35. {jenkinsctl-0.0.8 → jenkinsctl-1.0.0}/jenkinsctl/__init__.py +0 -0
  36. {jenkinsctl-0.0.8/jenkinsctl → jenkinsctl-1.0.0/jenkinsctl/configs}/config.py +0 -0
@@ -1,24 +1,35 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jenkinsctl
3
- Version: 0.0.8
4
- Summary: Build Jenkins jobs effortlessly using a single command. 🚀
5
- Author-email: Aman Shaw <amanshaw4511@protonmail.com>
6
- Project-URL: Homepage, https://github.com/amanshaw4511/jenkinsctl
7
- Project-URL: Repository, https://github.com/amanshaw4511/jenkinsctl
8
- Project-URL: Issues, https://github.com/amanshaw4511/jenkinsctl/issues
3
+ Version: 1.0.0
4
+ Summary: A command-line tool to interact with Jenkins jobs 🚀
5
+ Home-page: https://github.com/amanshaw4511/jenkinsctl
6
+ License: GPL-3.0-or-later
9
7
  Keywords: jenkins,jenkin
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
8
+ Author: Aman Shaw
9
+ Author-email: amanshaw4511@protonmail.com
10
+ Maintainer: Aman Shaw
11
+ Maintainer-email: amanshaw4511@protonmail.com
12
+ Requires-Python: >=3.8,<4.0
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
14
  Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.8
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Requires-Dist: click (>=8.1.7,<9.0.0)
22
+ Requires-Dist: click-completion (>=0.5.2,<0.6.0)
23
+ Requires-Dist: dynaconf (>=3.2.6,<4.0.0)
24
+ Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
25
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
26
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
27
+ Requires-Dist: rich (>=13.8.1,<14.0.0)
28
+ Project-URL: Repository, https://github.com/amanshaw4511/jenkinsctl
14
29
  Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: api4jenkins
17
- Requires-Dist: dynaconf
18
- Requires-Dist: pyyaml
19
- Requires-Dist: argcomplete
20
30
 
21
- # jenkinsctl
31
+ ![jenkinsctl](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Jenkins_logo.svg/226px-Jenkins_logo.svg.png?20120629215426)
32
+ # jenkinsctl [![PyPI version](https://badge.fury.io/py/jenkinsctl.svg?)](https://badge.fury.io/py/jenkinsctl) [![Downloads](https://static.pepy.tech/badge/jenkinsctl/week?)](https://pepy.tech/project/jenkinsctl) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
22
33
  Build Jenkins jobs effortlessly using a single command. 🚀
23
34
 
24
35
  ## Installation 📦
@@ -37,6 +48,7 @@ export JENKINS_API_KEY=21df49caf41726094323b803a6de363eae
37
48
  ```
38
49
  Adjust the values to match your Jenkins server's URL, your username, and the corresponding API key. This configuration is essential for jenkinsctl to interact with Jenkins and execute tasks efficiently.
39
50
 
51
+ How to Get the API Token: https://www.baeldung.com/ops/jenkins-api-token
40
52
  ## Usage 🤖
41
53
  ```sh
42
54
  $ jenkinsctl --help
@@ -112,3 +124,4 @@ To generate a YAML configuration file from a specific build (e.g. 2nd build) of
112
124
  ```sh
113
125
  jenkinsctl config my_job 2 > my_job.yaml
114
126
  ```
127
+
@@ -1,4 +1,5 @@
1
- # jenkinsctl
1
+ ![jenkinsctl](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Jenkins_logo.svg/226px-Jenkins_logo.svg.png?20120629215426)
2
+ # jenkinsctl [![PyPI version](https://badge.fury.io/py/jenkinsctl.svg?)](https://badge.fury.io/py/jenkinsctl) [![Downloads](https://static.pepy.tech/badge/jenkinsctl/week?)](https://pepy.tech/project/jenkinsctl) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
2
3
  Build Jenkins jobs effortlessly using a single command. 🚀
3
4
 
4
5
  ## Installation 📦
@@ -17,6 +18,7 @@ export JENKINS_API_KEY=21df49caf41726094323b803a6de363eae
17
18
  ```
18
19
  Adjust the values to match your Jenkins server's URL, your username, and the corresponding API key. This configuration is essential for jenkinsctl to interact with Jenkins and execute tasks efficiently.
19
20
 
21
+ How to Get the API Token: https://www.baeldung.com/ops/jenkins-api-token
20
22
  ## Usage 🤖
21
23
  ```sh
22
24
  $ jenkinsctl --help
@@ -0,0 +1,158 @@
1
+ import logging
2
+ from io import TextIOWrapper
3
+ from typing import Optional
4
+
5
+ import click
6
+ import click_completion
7
+
8
+ from jenkinsctl.commands.build import build_handler
9
+ from jenkinsctl.commands.config import config_handler
10
+ from jenkinsctl.commands.enable_completion import handle_enable_completion
11
+ from jenkinsctl.commands.jobs import jobs_handler
12
+ from jenkinsctl.commands.json import json_handler
13
+ from jenkinsctl.commands.list import list_handler
14
+ from jenkinsctl.commands.logs import logs_handler
15
+ from jenkinsctl.commands.rebuild import rebuild_handler
16
+ from jenkinsctl.configs.logging_config import setup_logging
17
+ from jenkinsctl.jenkins.cli_helper import error_handler_and_session
18
+
19
+ click_completion.init()
20
+
21
+
22
+ @click.group()
23
+ @click.option('-v', '--verbose', is_flag=True, help="Enable verbose output")
24
+ @click.pass_context
25
+ def cli(ctx: click.Context, verbose: bool) -> None:
26
+ """A command-line tool to interact with Jenkins jobs"""
27
+ log_level: int = logging.DEBUG if verbose else logging.INFO
28
+ ctx.logger = setup_logging(log_level)
29
+
30
+
31
+ @cli.command("list")
32
+ @click.argument("job_name")
33
+ @click.option("-n", "--number", default=5, type=int, help="No of builds to list (default: 5)")
34
+ def list_command(job_name: str, number: int) -> None:
35
+ """
36
+ List all builds of a Jenkins job
37
+
38
+ \b
39
+ JOB_NAME: Name of the Jenkins job
40
+ """
41
+ with error_handler_and_session() as session:
42
+ list_handler(session, job_name, number)
43
+
44
+
45
+ @cli.command("logs")
46
+ @click.argument("job_name")
47
+ @click.argument("build_no", required=False, type=int)
48
+ def logs_command(job_name: str, build_no: Optional[int]) -> None:
49
+ """
50
+ Print logs of a Jenkins build
51
+
52
+ \b
53
+ JOB_NAME: Name of the Jenkins job
54
+ BUILD_NO: Build number (default: last build)
55
+ """
56
+ with error_handler_and_session() as session:
57
+ logs_handler(session, job_name, build_no)
58
+
59
+
60
+ @cli.command("json")
61
+ @click.argument("job_name")
62
+ @click.argument("build_no", required=False, type=int)
63
+ def json_command(job_name: str, build_no: Optional[int]) -> None:
64
+ """
65
+ Fetch and print the JSON API response of a Jenkins build
66
+
67
+ \b
68
+ JOB_NAME: Name of the Jenkins job
69
+ BUILD_NO: Build number (default: last build)
70
+ """
71
+ with error_handler_and_session() as session:
72
+ json_handler(session, job_name, build_no)
73
+
74
+
75
+ @cli.command("config")
76
+ @click.argument("job_name")
77
+ @click.argument("build_no", required=False, type=int)
78
+ def config_command(job_name: str, build_no: Optional[int]) -> None:
79
+ """
80
+ Get the configuration of a specific build in YAML format
81
+
82
+ \b
83
+ JOB_NAME: Name of the Jenkins job
84
+ BUILD_NO: Build number (default: last build)
85
+ """
86
+ with error_handler_and_session() as session:
87
+ config_handler(session, job_name, build_no)
88
+
89
+
90
+ @cli.command("rebuild")
91
+ @click.argument("job_name")
92
+ @click.argument("build_no", required=False, type=int)
93
+ def rebuild_command(job_name: str, build_no: Optional[int]) -> None:
94
+ """
95
+ Rebuild a specific Jenkins job
96
+
97
+ \b
98
+ JOB_NAME: Name of the Jenkins job
99
+ BUILD_NO: Build number (default: last build)
100
+ """
101
+ with error_handler_and_session() as session:
102
+ rebuild_handler(session, job_name, build_no)
103
+
104
+
105
+ @cli.command("build")
106
+ @click.option("-p", "--param", multiple=True,
107
+ help="Override parameters in the YAML configuration (e.g., --param key=value)")
108
+ @click.option("-f", "--file", type=click.File('r'), required=True, help="YAML configuration file for the Jenkins job")
109
+ def build_command(file: TextIOWrapper, param: tuple[str]) -> None:
110
+ """
111
+ Trigger a new Jenkins build
112
+ """
113
+ params = [p for p in param]
114
+ with error_handler_and_session() as session:
115
+ build_handler(session, file, params)
116
+
117
+
118
+ @cli.command("enable-completion")
119
+ @click.argument('shell', required=False)
120
+ def enable_completion(shell: str):
121
+ """
122
+ Enable shell autocompletion for jenkinsctl.
123
+
124
+ This command allows you to set up autocompletion for the jenkinsctl command-line tool,
125
+ making it easier to use by providing suggestions as you type.
126
+
127
+ \b
128
+ SHELL: Specify a shell for which to enable autocompletion.
129
+ Supported options include:
130
+ - bash
131
+ - zsh
132
+ - fish
133
+
134
+ If no shell is specified, it will attempt to autodetect your shell.
135
+ """
136
+ if shell is None:
137
+ shell = click_completion.core.get_auto_shell() # Detect the current shell
138
+
139
+ handle_enable_completion(shell)
140
+
141
+
142
+ @cli.command("jobs")
143
+ @click.argument("folder_name", default="")
144
+ def jobs_command(folder_name: str) -> None:
145
+ """
146
+ List all jobs in a Jenkins folder.
147
+
148
+ \b
149
+ FOLDER_NAME: Path of the folder to list jobs (e.g., projectA/subProjectX).
150
+ If no folder is specified, it lists jobs in the root folder.
151
+ """
152
+ with error_handler_and_session() as session:
153
+ jobs_handler(session, folder_name)
154
+
155
+
156
+ # Entry point
157
+ if __name__ == '__main__':
158
+ cli()
File without changes
@@ -0,0 +1,46 @@
1
+ import logging
2
+ import sys
3
+ from io import TextIOWrapper
4
+ from typing import List
5
+
6
+ import yaml
7
+
8
+ from jenkinsctl.configs.session import Session
9
+ from jenkinsctl.jenkins.console_util import json_preety
10
+ from jenkinsctl.jenkins.job import build_job
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ def get_config_from_yaml(file):
16
+ config_data = None
17
+ try:
18
+ with open(file.name, "r") as config_file:
19
+ config_data = yaml.safe_load(config_file) # Use safe_load for YAML
20
+ except FileNotFoundError:
21
+ print(f"Error: Configuration file '{file}' not found.")
22
+ sys.exit()
23
+ except yaml.YAMLError as e:
24
+ print(f"Error: Invalid YAML format in '{file}': {e}")
25
+ sys.exit()
26
+
27
+ return config_data
28
+
29
+
30
+ def get_conf(file, params):
31
+ config = get_config_from_yaml(file)
32
+ override_params(params, config)
33
+ return config
34
+
35
+
36
+ def override_params(params, file_config):
37
+ for param in params:
38
+ name, value = param.split('=')
39
+ file_config['params'][name] = value
40
+
41
+
42
+ def build_handler(session: Session, file: TextIOWrapper, params: List[str]):
43
+ conf = get_conf(file, params)
44
+ log.debug(f"config: {json_preety(conf)}")
45
+ location = build_job(session, conf["job"], conf["params"])
46
+ print(location)
@@ -0,0 +1,15 @@
1
+ from typing import Optional
2
+
3
+ from jenkinsctl.configs.session import Session
4
+ from jenkinsctl.jenkins.commons import get_last_build_no_if_none
5
+ from jenkinsctl.jenkins.job import get_build
6
+ from jenkinsctl.jenkins.utils import get_build_params, to_yaml, normalize_job_path
7
+
8
+
9
+ def config_handler(session: Session, job_name: str, build_no: Optional[int]):
10
+ job_name = normalize_job_path(job_name)
11
+ build_no = get_last_build_no_if_none(session, job_name, build_no)
12
+
13
+ build = get_build(session, job_name, build_no)
14
+ params = get_build_params(build)
15
+ print(to_yaml(job_name, params))
@@ -0,0 +1,22 @@
1
+ def handle_enable_completion(shell: str):
2
+ if shell == "bash":
3
+ print("\n# To enable autocompletion for bash, add the following to your ~/.bashrc:")
4
+ print("echo 'eval \"$(_JENKINSCTL_COMPLETE=bash_source jenkinsctl)\"' >> ~/.bashrc")
5
+ print("\n# Then reload your bash configuration:")
6
+ print("source ~/.bashrc")
7
+
8
+ elif shell == "zsh":
9
+ print("\n# To enable autocompletion for zsh, add the following to your ~/.zshrc:")
10
+ print("echo 'eval \"$(_JENKINSCTL_COMPLETE=zsh_source jenkinsctl)\"' >> ~/.zshrc")
11
+ print("\n# Then reload your zsh configuration:")
12
+ print("source ~/.zshrc")
13
+
14
+ elif shell == "fish":
15
+ print(
16
+ "\n# To enable autocompletion for fish, add the following to your Fish config (~/.config/fish/config.fish):")
17
+ print("echo 'eval (jenkinsctl enable-completion fish | source)' >> ~/.config/fish/config.fish")
18
+ print("\n# Then reload your fish shell configuration:")
19
+ print("source ~/.config/fish/config.fish")
20
+
21
+ else:
22
+ print("\n# Unsupported shell. Please specify bash, zsh, or fish.")
@@ -0,0 +1,18 @@
1
+ from jenkinsctl.configs.session import Session
2
+ from jenkinsctl.jenkins.job import get_jobs
3
+ from jenkinsctl.jenkins.utils import normalize_job_path
4
+
5
+
6
+ def jobs_handler(session: Session, folder_name: str):
7
+ folder_name = normalize_job_path(folder_name)
8
+ response = get_jobs(session, folder_name)
9
+ all_jobs = response["jobs"]
10
+
11
+ folders = [job for job in all_jobs if job["_class"] == "com.cloudbees.hudson.plugins.folder.Folder"]
12
+ jobs = [job for job in all_jobs if job["_class"] != "com.cloudbees.hudson.plugins.folder.Folder"]
13
+
14
+ for folder in folders:
15
+ print(f"[{folder["name"]}]")
16
+
17
+ for job in jobs:
18
+ print(f"{job["name"]}")
@@ -0,0 +1,14 @@
1
+ from typing import Optional
2
+
3
+ from jenkinsctl.configs.session import Session
4
+ from jenkinsctl.jenkins.commons import get_last_build_no_if_none
5
+ from jenkinsctl.jenkins.console_util import print_json
6
+ from jenkinsctl.jenkins.job import progressive_log, get_build
7
+ from jenkinsctl.jenkins.utils import normalize_job_path
8
+
9
+
10
+ def json_handler(session: Session, job_name: str, build_no: Optional[int]):
11
+ job_name = normalize_job_path(job_name)
12
+ build_no = get_last_build_no_if_none(session, job_name, build_no)
13
+ build = get_build(session, job_name, build_no)
14
+ print_json(build)
@@ -0,0 +1,62 @@
1
+ from itertools import islice
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from jenkinsctl.configs.session import Session
7
+ from jenkinsctl.jenkins.console_util import format_timestamp
8
+ from jenkinsctl.jenkins.job import get_job, get_builds_iter
9
+ from jenkinsctl.jenkins.utils import normalize_job_path
10
+
11
+
12
+ def list_handler(session: Session, job_name: str, number: int):
13
+ job_name = normalize_job_path(job_name)
14
+ job = get_job(session, job_name)
15
+
16
+ console = Console()
17
+ table = Table(show_header=True, header_style="bold cyan", box=None)
18
+
19
+ table.add_column("NUMBER", width=6)
20
+ table.add_column("IN PROGRESS", width=15)
21
+ table.add_column("RESULT", width=10)
22
+ table.add_column("BUILDING", width=8)
23
+ table.add_column("BRANCH", width=15)
24
+ table.add_column("REVISION", width=8)
25
+ table.add_column("STARTED BY", width=15)
26
+ table.add_column("BUILD TIME", width=20)
27
+
28
+ for build in islice(get_builds_iter(session, job), number):
29
+ number = build["number"]
30
+ building = build["building"]
31
+ in_progress = build["inProgress"]
32
+ result = build["result"]
33
+ timestamp = build["timestamp"]
34
+
35
+ actions = build["actions"]
36
+ causes = next((action["causes"] for action in actions if action.get("causes") is not None), None)
37
+ user_id = next((cause["userId"] for cause in causes if cause.get("userId") is not None), None)
38
+
39
+ branch = None
40
+ revision = None
41
+ # fixme: not working below code
42
+ # last_build = next((action["lastBuildRevision"] for action in actions if action.get("lastBuildRevision") is not None))
43
+ last_build = None
44
+ if last_build:
45
+ branch_prefix = "refs/remotes/origin/"
46
+ branch = last_build["branch"][0]["name"]
47
+ if branch.startswith(branch_prefix):
48
+ branch = branch[len(branch_prefix):]
49
+ revision = last_build["SHA1"][:5]
50
+
51
+
52
+ table.add_row(
53
+ str(number),
54
+ str(in_progress),
55
+ str(result),
56
+ str(building),
57
+ branch,
58
+ revision,
59
+ user_id,
60
+ str(format_timestamp(timestamp)))
61
+
62
+ console.print(table)
@@ -0,0 +1,12 @@
1
+ from typing import Optional
2
+
3
+ from jenkinsctl.configs.session import Session
4
+ from jenkinsctl.jenkins.commons import get_last_build_no_if_none
5
+ from jenkinsctl.jenkins.job import progressive_log
6
+ from jenkinsctl.jenkins.utils import normalize_job_path
7
+
8
+
9
+ def logs_handler(session: Session, job_name: str, build_no: Optional[int]):
10
+ job_name = normalize_job_path(job_name)
11
+ build_no = get_last_build_no_if_none(session, job_name, build_no)
12
+ progressive_log(session, job_name, build_no)
@@ -0,0 +1,14 @@
1
+ from typing import Optional
2
+
3
+ from jenkinsctl.configs.session import Session
4
+ from jenkinsctl.jenkins.commons import get_last_build_no_if_none
5
+ from jenkinsctl.jenkins.job import get_build, build_job
6
+ from jenkinsctl.jenkins.utils import get_build_params, normalize_job_path
7
+
8
+
9
+ def rebuild_handler(session: Session, job_name: str, build_no: Optional[int]):
10
+ job_name = normalize_job_path(job_name)
11
+ build_no = get_last_build_no_if_none(session, job_name, build_no)
12
+ build = get_build(session, job_name, build_no)
13
+ location = build_job(session, job_name, get_build_params(build))
14
+ print(location)
File without changes
@@ -0,0 +1,19 @@
1
+ import logging
2
+ import sys
3
+
4
+ def setup_logging(log_level=logging.INFO):
5
+ logger = logging.getLogger()
6
+ logger.setLevel(log_level)
7
+
8
+ console_handler = logging.StreamHandler(sys.stdout)
9
+ console_handler.setLevel(log_level)
10
+ console_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
11
+
12
+ file_handler = logging.FileHandler('jenkinsctl.log')
13
+ file_handler.setLevel(log_level)
14
+ file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
15
+
16
+ logger.addHandler(console_handler)
17
+ logger.addHandler(file_handler)
18
+
19
+ return logger
@@ -0,0 +1,21 @@
1
+ from urllib.parse import urljoin
2
+
3
+ import requests
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class Session(requests.Session):
9
+ def __init__(self, base_url: str):
10
+ super().__init__()
11
+ self.base_url = base_url
12
+
13
+ def request(
14
+ self,
15
+ method,
16
+ url,
17
+ **kwargs
18
+ ):
19
+ joined_url = urljoin(self.base_url, url)
20
+ return super().request(method, joined_url, **kwargs)
21
+
@@ -0,0 +1,25 @@
1
+ import logging
2
+ from contextlib import contextmanager
3
+
4
+ from jenkinsctl.configs.config import settings
5
+ from jenkinsctl.configs.session import Session
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ @contextmanager
10
+ def error_handler_and_session():
11
+ server_url: str = settings.server_url
12
+ username: str = settings.username
13
+ api_key: str = settings.api_key
14
+
15
+ session = None
16
+ try:
17
+ session = Session(server_url)
18
+ session.auth = (username, api_key)
19
+ yield session
20
+ except Exception as e:
21
+ log.debug(f"An error occurred", exc_info=e )
22
+ print(f"An error occurred, run same command with --verbose flag to print error details")
23
+ finally:
24
+ if session:
25
+ session.close()
@@ -0,0 +1,9 @@
1
+ from jenkinsctl.jenkins.job import get_job
2
+ from jenkinsctl.jenkins.utils import get_last_build
3
+
4
+
5
+ def get_last_build_no_if_none(session, job_name, build_no):
6
+ if build_no is None:
7
+ job = get_job(session, job_name)
8
+ return get_last_build(job)
9
+ return build_no
@@ -0,0 +1,31 @@
1
+ import json
2
+
3
+ from rich.console import Console
4
+ from datetime import datetime
5
+ from dateutil import tz
6
+
7
+
8
+ def get_console():
9
+ return Console()
10
+
11
+
12
+ def format_timestamp(epoch_timestamp):
13
+ # Convert epoch timestamp to a naive datetime object
14
+ naive_dt = datetime.fromtimestamp(epoch_timestamp / 1000)
15
+
16
+ # Get the local timezone
17
+ local_tz = tz.tzlocal()
18
+
19
+ # Convert naive datetime to local timezone
20
+ local_dt = naive_dt.astimezone(local_tz)
21
+
22
+ # Format the datetime object to a string
23
+ return local_dt.strftime('%Y-%m-%d %H:%M')
24
+
25
+
26
+ def print_json(dict_obj):
27
+ json_str = json.dumps(dict_obj)
28
+ get_console().print_json(json_str)
29
+
30
+ def json_preety(dict_obj):
31
+ return json.dumps(dict_obj, indent=2)
@@ -0,0 +1,69 @@
1
+ import time
2
+
3
+ from urllib.parse import urlparse
4
+
5
+ from jenkinsctl.configs.session import Session
6
+
7
+ def _remove_base_url(url: str):
8
+ return urlparse(url).path
9
+
10
+ def _get(session: Session, url: str):
11
+ url = _remove_base_url(url)
12
+ url = f"{url}api/json"
13
+ return session.get(url).json()
14
+
15
+
16
+ def build_job(session: Session, job_name: str, params: dict):
17
+ response = None
18
+ if len(params) == 0:
19
+ url = f"/job/{job_name}/build"
20
+ response = session.post(url)
21
+ else:
22
+ url = f"/job/{job_name}/buildWithParameters"
23
+ response = session.post(url, params=params)
24
+
25
+ return response.headers.get("Location")
26
+
27
+
28
+ def get_job(session: Session, job_name: str):
29
+ url = f"/job/{job_name}/"
30
+ return _get(session, url)
31
+
32
+
33
+ def get_jobs(session: Session, folder_name: str):
34
+ if folder_name.strip() == "":
35
+ return _get(session, "")
36
+
37
+ url = f"/job/{folder_name}/"
38
+ return _get(session, url)
39
+
40
+
41
+ def get_builds_iter(session: Session, job_json):
42
+ builds = job_json["builds"]
43
+ for build in builds:
44
+ yield _get_build(session, job_json, build["number"])
45
+
46
+
47
+ def _get_build(session: Session, job_json, build_no):
48
+ builds = job_json["builds"]
49
+ build = next((build for build in builds if build["number"] == build_no), None)
50
+
51
+ return _get(session, build["url"])
52
+
53
+
54
+ def get_build(session: Session, job_name: str, build_no: int):
55
+ url = f"/job/{job_name}/{build_no}/"
56
+ return _get(session, url)
57
+
58
+
59
+ def progressive_log(session: Session, job_name: str, build_no: int):
60
+ url = f"/job/{job_name}/{build_no}/logText/progressiveText"
61
+ start_byte = 0
62
+ while True:
63
+ response = session.get(url, params={'start': start_byte})
64
+ text = response.text
65
+ print(text, end="")
66
+ start_byte = int(response.headers.get('X-Text-Size', 0))
67
+ if response.headers.get('X-More-Data') == 'false' or text.strip() == "":
68
+ break
69
+ time.sleep(2)
@@ -0,0 +1,37 @@
1
+ import yaml
2
+
3
+
4
+ def get_build_params(build_json):
5
+ actions = build_json["actions"]
6
+ params = next((action["parameters"] for action in actions if action.get("parameters") is not None), [])
7
+ params = dict([(param["name"], param["value"]) for param in params])
8
+ return params
9
+
10
+
11
+ def normalize_job_path(path: str):
12
+ return (path.strip()
13
+ .removeprefix("/")
14
+ .replace("/", "/job/"))
15
+
16
+
17
+ def print_build(build_json):
18
+ number = build_json["number"]
19
+ building = build_json["building"]
20
+ in_progress = build_json["inProgress"]
21
+ result = build_json["result"]
22
+ timestamp = build_json["timestamp"]
23
+
24
+ actions = build_json["actions"]
25
+ causes = next((action["causes"] for action in actions if action.get("causes") is not None), None)
26
+ user_id = next((cause["userId"] for cause in causes if cause.get("userId") is not None), None)
27
+
28
+ print(number, building, in_progress, result, timestamp, user_id)
29
+ print(get_build_params(build_json))
30
+
31
+
32
+ def get_last_build(job) -> int:
33
+ return job["lastBuild"]["number"]
34
+
35
+
36
+ def to_yaml(job_name, params):
37
+ return yaml.dump({"job": job_name, "params": params})
@@ -0,0 +1,36 @@
1
+ [tool.poetry]
2
+ name = "jenkinsctl"
3
+ version = "1.0.0"
4
+ description = "A command-line tool to interact with Jenkins jobs 🚀"
5
+ authors = ["Aman Shaw <amanshaw4511@protonmail.com>"]
6
+ readme = "README.md"
7
+ license = "GPL-3.0-or-later"
8
+ maintainers = [
9
+ "Aman Shaw <amanshaw4511@protonmail.com>"
10
+ ]
11
+ homepage = "https://github.com/amanshaw4511/jenkinsctl"
12
+ repository = "https://github.com/amanshaw4511/jenkinsctl"
13
+ keywords = ["jenkins", "jenkin"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+
19
+ [tool.poetry.dependencies]
20
+ python = "^3.8"
21
+ pyyaml = "^6.0.2"
22
+ dynaconf = "^3.2.6"
23
+ rich = "^13.8.1"
24
+ python-dateutil = "^2.9.0.post0"
25
+ requests = "^2.32.3"
26
+ click = "^8.1.7"
27
+ click-completion = "^0.5.2"
28
+
29
+
30
+ [build-system]
31
+ requires = ["poetry-core"]
32
+ build-backend = "poetry.core.masonry.api"
33
+
34
+
35
+ [tool.poetry.scripts]
36
+ jenkinsctl = "jenkinsctl.cli:cli"
@@ -1,8 +0,0 @@
1
-
2
-
3
- def use_vprint(verbose_flag):
4
- def vprint(*args, **kwargs):
5
- if verbose_flag:
6
- print(*args, **kwargs)
7
-
8
- return vprint
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env python3
2
- # PYTHON_ARGCOMPLETE_OK
3
-
4
- import time
5
- import sys
6
- import yaml
7
- from threading import Thread
8
-
9
- from .commons import use_vprint
10
-
11
-
12
- def handle_build_command(args):
13
- vprint = use_vprint(args.verbose)
14
- vprint(f"Passed args : {vars(args)}")
15
-
16
- conf = get_conf(args, vprint)
17
- vprint("Final config: ", conf)
18
-
19
- create_build(args.client, conf, args.suppress_logs, vprint)
20
-
21
-
22
- def get_config_from_yaml(file):
23
- config_data = None
24
- try:
25
- with open(file.name, "r") as config_file:
26
- config_data = yaml.safe_load(config_file) # Use safe_load for YAML
27
- except FileNotFoundError:
28
- print(f"Error: Configuration file '{file}' not found.")
29
- sys.exit()
30
- except yaml.YAMLError as e:
31
- print(f"Error: Invalid YAML format in '{file}': {e}")
32
- sys.exit()
33
-
34
- return config_data
35
-
36
-
37
- def get_conf(args, vprint):
38
- config = get_config_from_yaml(args.file)
39
- vprint(f"Config from file : {config}")
40
- override_params(args, config)
41
- return config
42
-
43
-
44
- def override_params(args, file_config):
45
- for param in args.param:
46
- name, value = param.split('=')
47
- file_config['params'][name] = value
48
-
49
-
50
- def get_build(queued_item):
51
- build = queued_item.get_build()
52
- while build == None:
53
- time.sleep(2)
54
- build = queued_item.get_build()
55
- return build
56
-
57
-
58
- def get_build_no(queued_item):
59
- return queued_item.api_json()["executable"]["number"]
60
-
61
-
62
- def print_build_log(build):
63
- for line in build.progressive_output():
64
- print(line)
65
-
66
-
67
- def approve_pending_input(build):
68
- if not hasattr(build, 'get_pending_input'):
69
- return
70
-
71
- while not build.get_pending_input() and build.building:
72
- time.sleep(1)
73
-
74
- if build.building:
75
- build.get_pending_input().submit()
76
-
77
-
78
- def create_build(client, conf, suppress_logs: bool, vprint=print):
79
-
80
- client = client()
81
- vprint(f"client version: {client.version}")
82
-
83
- job = client.get_job(conf["job"])
84
- vprint(f"job : {job}")
85
-
86
- queued_item = job.build(**conf["params"])
87
- vprint(f"queued : {queued_item}")
88
-
89
- build = get_build(queued_item)
90
- vprint(f"build : {build}")
91
-
92
- build_number = get_build_no(queued_item)
93
-
94
- if not suppress_logs:
95
- print(f"STARTED... build number : {build_number}")
96
-
97
- approve_pending_input_thread = Thread(
98
- target=approve_pending_input, args=(build,))
99
- approve_pending_input_thread.start()
100
-
101
- if not suppress_logs:
102
- log_thread = Thread(target=print_build_log, args=(build,))
103
- log_thread.start()
104
- log_thread.join()
105
-
106
- approve_pending_input_thread.join()
107
-
108
- if not suppress_logs:
109
- print(f"FINISHED... build number : {build_number}")
110
- else:
111
- print(build_number)
112
-
113
- return build_number
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env python3
2
- # PYTHON_ARGCOMPLETE_OK
3
-
4
- import argparse
5
- import argcomplete
6
- from .jbuild import handle_build_command
7
- from api4jenkins import Jenkins
8
- from .config import settings
9
- from .jget_config import handle_get_config
10
-
11
-
12
- server_url = settings.server_url
13
- username = settings.username
14
- api_key = settings.api_key
15
-
16
-
17
- def get_client():
18
- return Jenkins(server_url, auth=(username, api_key))
19
-
20
-
21
- def get_args():
22
- parser = argparse.ArgumentParser()
23
- subparsers = parser.add_subparsers(title="Subcommand", dest="subcommand")
24
-
25
- add_build_subparser(subparsers)
26
- add_get_config_subparser(subparsers)
27
-
28
- argcomplete.autocomplete(parser)
29
- args = parser.parse_args()
30
- args.func(args)
31
-
32
-
33
- def add_build_subparser(subparsers):
34
- subparser = subparsers.add_parser("build", help="run new build")
35
- subparser.set_defaults(func=handle_build_command, client=get_client)
36
- subparser.add_argument(
37
- "-f", "--file", type=argparse.FileType("r"), help="Yaml configuration file"
38
- )
39
- subparser.add_argument("-v", "--verbose", action="store_true")
40
- subparser.add_argument("-s", "--suppress-logs", action="store_true")
41
- subparser.add_argument("--param", action="append", default=[])
42
-
43
-
44
- def add_get_config_subparser(subparsers):
45
- subparser = subparsers.add_parser("config", help="get config of a build")
46
- subparser.set_defaults(func=handle_get_config, client=get_client)
47
- subparser.add_argument("job_name")
48
- subparser.add_argument("build_no", type=int, nargs="?")
49
- subparser.add_argument("-v", "--verbose", action="store_true")
50
-
51
-
52
- if __name__ == '__main__':
53
- get_args()
@@ -1,41 +0,0 @@
1
- import yaml
2
-
3
- from .commons import use_vprint
4
-
5
-
6
- def handle_get_config(args):
7
- vprint = use_vprint(args.verbose)
8
- vprint(f"Passed args : {vars(args)}")
9
-
10
- client = args.client()
11
-
12
- job_name = args.job_name
13
-
14
- build = get_build(client, vprint, job_name, args.build_no)
15
-
16
- params = get_params(build)
17
-
18
- print(to_yaml(job_name, params))
19
-
20
-
21
- def get_build(client, vprint, job_name, build_no):
22
- job = client.get_job(job_name)
23
-
24
- final_build_no = get_last_build_no(job) if build_no is None else build_no
25
- vprint(f"final build no {final_build_no}")
26
-
27
- build = job.get(final_build_no)
28
-
29
- return build
30
-
31
-
32
- def get_last_build_no(job):
33
- return job.get_last_build().number
34
-
35
-
36
- def get_params(build):
37
- return dict([(param.name, param.value) for param in build.get_parameters()])
38
-
39
-
40
- def to_yaml(job_name, params):
41
- return yaml.dump({"job": job_name, "params": params})
@@ -1,114 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: jenkinsctl
3
- Version: 0.0.8
4
- Summary: Build Jenkins jobs effortlessly using a single command. 🚀
5
- Author-email: Aman Shaw <amanshaw4511@protonmail.com>
6
- Project-URL: Homepage, https://github.com/amanshaw4511/jenkinsctl
7
- Project-URL: Repository, https://github.com/amanshaw4511/jenkinsctl
8
- Project-URL: Issues, https://github.com/amanshaw4511/jenkinsctl/issues
9
- Keywords: jenkins,jenkin
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.8
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: api4jenkins
17
- Requires-Dist: dynaconf
18
- Requires-Dist: pyyaml
19
- Requires-Dist: argcomplete
20
-
21
- # jenkinsctl
22
- Build Jenkins jobs effortlessly using a single command. 🚀
23
-
24
- ## Installation 📦
25
-
26
- ```sh
27
- pip install jenkinsctl
28
- ```
29
-
30
- ## Jenkins Configuration 🛠️
31
- Before using jenkinsctl, configure your Jenkins server details in your shell profile.
32
- Add these lines in your ~/.bashrc or ~/.zshrc file:
33
- ```sh
34
- export JENKINS_SERVER_URL=http://localhost:8080
35
- export JENKINS_USERNAME=amanshaw4511
36
- export JENKINS_API_KEY=21df49caf41726094323b803a6de363eae
37
- ```
38
- Adjust the values to match your Jenkins server's URL, your username, and the corresponding API key. This configuration is essential for jenkinsctl to interact with Jenkins and execute tasks efficiently.
39
-
40
- ## Usage 🤖
41
- ```sh
42
- $ jenkinsctl --help
43
- usage: jenkinsctl [-h] {build,config} ...
44
-
45
- options:
46
- -h, --help show this help message and exit
47
-
48
- Subcommand:
49
- {build,config}
50
- build run new build
51
- config get config of a build
52
- ```
53
-
54
- ### Run a Jenkins Job
55
- ```sh
56
- $ jenkinsctl build --help
57
- usage: jenkinsctl build [-h] [-f FILE] [-v] [-s SUPPRESS_LOGS] [--param PARAM]
58
-
59
- options:
60
- -h, --help show this help message and exit
61
- -f FILE, --file FILE Yaml configuration file
62
- -v, --verbose
63
- -s SUPPRESS_LOGS, --suppress-logs SUPPRESS_LOGS
64
- --param PARAM
65
- ```
66
-
67
- ### Get Config of a Jenkin Build in YAML Format
68
- ```sh
69
- $ jenkinsctl config --help
70
- usage: jenkinsctl config [-h] job_name build_no
71
-
72
- positional arguments:
73
- job_name
74
- build_no
75
-
76
- options:
77
- -h, --help show this help message and exit
78
- ```
79
-
80
- ## Examples 🎭
81
- ### Runing a Jenkins Job
82
- Create a YAML configuration file, let's say `my_job.yaml`, with job parameters like this:
83
- ```yaml
84
- job: my_job
85
- params:
86
- param1: some value
87
- param2: 10
88
- param3: true
89
- ```
90
- Initiate the job build using the following command:
91
- ```sh
92
- jenkinsctl build -f my_job.yaml
93
- ```
94
- This command executes the job based on the specified YAML configuration.
95
-
96
- ### Overriding Specific Parameter from Configuration
97
- ```sh
98
- jenkinsctl build -f my_job.yaml --param param2=11 --param param3=false
99
- ```
100
- This command will override the value of `param2` and `param3` from original configuration file `my_job.yaml`, passing an effective configuration as follows to run jenkin job :
101
- ```yaml
102
- job: my_job
103
- params:
104
- param1: some value
105
- param2: 11
106
- param3: false
107
- ```
108
-
109
- ### Generating Config from Existing Builds
110
- Capture and reproduce configurations from previous Jenkins builds.
111
- To generate a YAML configuration file from a specific build (e.g. 2nd build) of a job (e.g., `my_job`), use the following command:
112
- ```sh
113
- jenkinsctl config my_job 2 > my_job.yaml
114
- ```
@@ -1,15 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- jenkinsctl/__init__.py
5
- jenkinsctl/commons.py
6
- jenkinsctl/config.py
7
- jenkinsctl/jbuild.py
8
- jenkinsctl/jenkins.py
9
- jenkinsctl/jget_config.py
10
- jenkinsctl.egg-info/PKG-INFO
11
- jenkinsctl.egg-info/SOURCES.txt
12
- jenkinsctl.egg-info/dependency_links.txt
13
- jenkinsctl.egg-info/entry_points.txt
14
- jenkinsctl.egg-info/requires.txt
15
- jenkinsctl.egg-info/top_level.txt
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- jenkinsctl = jenkinsctl.jenkins:get_args
@@ -1,4 +0,0 @@
1
- api4jenkins
2
- dynaconf
3
- pyyaml
4
- argcomplete
@@ -1,2 +0,0 @@
1
- dist
2
- jenkinsctl
@@ -1,32 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "jenkinsctl"
7
- version = "v0.0.8"
8
- dependencies = ["api4jenkins", "dynaconf", "pyyaml", "argcomplete"]
9
- authors = [{ name = "Aman Shaw", email = "amanshaw4511@protonmail.com" }]
10
- description = "Build Jenkins jobs effortlessly using a single command. 🚀"
11
- readme = "README.md"
12
- requires-python = ">=3.8"
13
- classifiers = [
14
- "Programming Language :: Python :: 3",
15
- "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
16
- "Operating System :: OS Independent",
17
- ]
18
- keywords = ["jenkins", "jenkin"]
19
-
20
- [project.urls]
21
- Homepage = "https://github.com/amanshaw4511/jenkinsctl"
22
- Repository = "https://github.com/amanshaw4511/jenkinsctl"
23
- Issues = "https://github.com/amanshaw4511/jenkinsctl/issues"
24
-
25
- [project.scripts]
26
- jenkinsctl = "jenkinsctl.jenkins:get_args"
27
-
28
- [tool.setuptools.packages.find]
29
- where = ["."] # list of folders that contain the packages (["."] by default)
30
- #include = [] # package names should match these glob patterns (["*"] by default)
31
- exclude = [] # exclude packages matching these glob patterns (empty by default)
32
- #namespaces = false # to disable scanning PEP 420 namespaces (true by default)
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
File without changes