zenkins 0.1.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.
zenkins-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: zenkins
3
+ Version: 0.1.0
4
+ Summary: CLI tool for Jenkins. List jobs, check builds, view logs, trigger builds.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/vivainio/zenkins
7
+ Project-URL: Repository, https://github.com/vivainio/zenkins
8
+ Project-URL: Issues, https://github.com/vivainio/zenkins/issues
9
+ Keywords: jenkins,cli,ci,builds
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.31.0
23
+ Requires-Dist: platformdirs>=4.0.0
24
+
25
+ # zenkins
26
+
27
+ CLI tool for Jenkins. List jobs, check builds, view logs, trigger builds.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install zenkins
33
+ ```
34
+
35
+ ## Setup
36
+
37
+ Run `zenkins init` to configure your Jenkins connection. This creates `~/.config/jenkins/config` with your Jenkins URL and credentials.
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ zenkins jobs # List all jobs with status
43
+ zenkins status <job> # Show last build info
44
+ zenkins builds <job> # List recent builds
45
+ zenkins builds <job> -n 5 # List last 5 builds
46
+ zenkins log <job> # Show console output (last build)
47
+ zenkins log <job> 42 # Show console output for build #42
48
+ zenkins queue # Show build queue
49
+ zenkins build <job> # Trigger a build
50
+ ```
51
+
52
+ ## Library usage
53
+
54
+ ```python
55
+ import zenkins
56
+
57
+ s = zenkins.client()
58
+ resp = s.get("http://jenkins.example.com/api/json")
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,39 @@
1
+ # zenkins
2
+
3
+ CLI tool for Jenkins. List jobs, check builds, view logs, trigger builds.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install zenkins
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Run `zenkins init` to configure your Jenkins connection. This creates `~/.config/jenkins/config` with your Jenkins URL and credentials.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ zenkins jobs # List all jobs with status
19
+ zenkins status <job> # Show last build info
20
+ zenkins builds <job> # List recent builds
21
+ zenkins builds <job> -n 5 # List last 5 builds
22
+ zenkins log <job> # Show console output (last build)
23
+ zenkins log <job> 42 # Show console output for build #42
24
+ zenkins queue # Show build queue
25
+ zenkins build <job> # Trigger a build
26
+ ```
27
+
28
+ ## Library usage
29
+
30
+ ```python
31
+ import zenkins
32
+
33
+ s = zenkins.client()
34
+ resp = s.get("http://jenkins.example.com/api/json")
35
+ ```
36
+
37
+ ## License
38
+
39
+ MIT
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "zenkins"
3
+ version = "0.1.0"
4
+ description = "CLI tool for Jenkins. List jobs, check builds, view logs, trigger builds."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11"
8
+ keywords = ["jenkins", "cli", "ci", "builds"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Software Development",
19
+ "Topic :: Utilities",
20
+ ]
21
+ dependencies = [
22
+ "requests>=2.31.0",
23
+ "platformdirs>=4.0.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/vivainio/zenkins"
28
+ Repository = "https://github.com/vivainio/zenkins"
29
+ Issues = "https://github.com/vivainio/zenkins/issues"
30
+
31
+ [project.scripts]
32
+ zenkins = "zenkins.cli:main"
33
+
34
+ [dependency-groups]
35
+ dev = ["pytest>=8.0"]
36
+
37
+ [tool.uv]
38
+ package = true
39
+
40
+ [tool.setuptools]
41
+ packages = ["zenkins"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,86 @@
1
+ """Tests for zenkins.builds and zenkins.status."""
2
+
3
+ import argparse
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from zenkins.builds import builds_command, format_duration
7
+ from zenkins.status import status_command, format_timestamp
8
+
9
+
10
+ def test_format_duration_seconds():
11
+ assert format_duration(5000) == "5s"
12
+
13
+
14
+ def test_format_duration_minutes():
15
+ assert format_duration(125000) == "2m 5s"
16
+
17
+
18
+ def test_format_duration_hours():
19
+ assert format_duration(3725000) == "1h 2m"
20
+
21
+
22
+ def test_format_timestamp():
23
+ ts = format_timestamp(1700000000000)
24
+ # Exact date depends on timezone, just check format
25
+ assert "2023-11-1" in ts
26
+ assert ":" in ts
27
+
28
+
29
+ def test_builds_command(mock_session, capsys):
30
+ """Test builds listing."""
31
+ mock_resp = MagicMock()
32
+ mock_resp.json.return_value = {
33
+ "builds": [
34
+ {
35
+ "number": 42,
36
+ "result": "SUCCESS",
37
+ "timestamp": 1700000000000,
38
+ "duration": 60000,
39
+ "building": False,
40
+ },
41
+ {
42
+ "number": 41,
43
+ "result": "FAILURE",
44
+ "timestamp": 1699990000000,
45
+ "duration": 30000,
46
+ "building": False,
47
+ },
48
+ ]
49
+ }
50
+ mock_session.get.return_value = mock_resp
51
+
52
+ args = argparse.Namespace(job="test-job", n=10)
53
+ with patch("zenkins.client.get_base_url", return_value="http://j"):
54
+ builds_command(args)
55
+
56
+ out = capsys.readouterr().out
57
+ assert "#42" in out
58
+ assert "#41" in out
59
+ assert "SUCCESS" in out
60
+ assert "FAILURE" in out
61
+
62
+
63
+ def test_status_command(mock_session, capsys):
64
+ """Test status display."""
65
+ mock_resp = MagicMock()
66
+ mock_resp.json.return_value = {
67
+ "lastBuild": {
68
+ "number": 99,
69
+ "result": "SUCCESS",
70
+ "timestamp": 1700000000000,
71
+ "duration": 120000,
72
+ "building": False,
73
+ "displayName": "#99",
74
+ "description": None,
75
+ }
76
+ }
77
+ mock_session.get.return_value = mock_resp
78
+
79
+ args = argparse.Namespace(job="my-job")
80
+ with patch("zenkins.client.get_base_url", return_value="http://j"):
81
+ status_command(args)
82
+
83
+ out = capsys.readouterr().out
84
+ assert "my-job" in out
85
+ assert "#99" in out
86
+ assert "SUCCESS" in out
@@ -0,0 +1,76 @@
1
+ """Tests for zenkins.client."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+ from pathlib import Path
5
+
6
+ from zenkins.client import load_credentials, api_get, api_post
7
+
8
+
9
+ def test_load_credentials(tmp_path):
10
+ """Test loading KEY=VALUE config file."""
11
+ config = tmp_path / "config"
12
+ config.write_text(
13
+ "JENKINS_URL=http://jenkins.example.com\n"
14
+ "JENKINS_USER=testuser\n"
15
+ "JENKINS_TOKEN=abc123\n"
16
+ )
17
+
18
+ with patch("zenkins.client.CONFIG_FILE", config):
19
+ creds = load_credentials()
20
+
21
+ assert creds["url"] == "http://jenkins.example.com"
22
+ assert creds["user"] == "testuser"
23
+ assert creds["token"] == "abc123"
24
+
25
+
26
+ def test_load_credentials_missing_file(tmp_path):
27
+ """Test loading from nonexistent file returns empty dict."""
28
+ config = tmp_path / "nonexistent"
29
+
30
+ with patch("zenkins.client.CONFIG_FILE", config):
31
+ creds = load_credentials()
32
+
33
+ assert creds == {}
34
+
35
+
36
+ def test_load_credentials_comments_and_blanks(tmp_path):
37
+ """Test that comments and blank lines are ignored."""
38
+ config = tmp_path / "config"
39
+ config.write_text(
40
+ "# This is a comment\n"
41
+ "\n"
42
+ "JENKINS_URL=http://jenkins.example.com\n"
43
+ " \n"
44
+ "JENKINS_USER=testuser\n"
45
+ )
46
+
47
+ with patch("zenkins.client.CONFIG_FILE", config):
48
+ creds = load_credentials()
49
+
50
+ assert creds["url"] == "http://jenkins.example.com"
51
+ assert creds["user"] == "testuser"
52
+ assert "token" not in creds
53
+
54
+
55
+ def test_api_get(mock_session):
56
+ """Test api_get combines base URL and path."""
57
+ mock_resp = MagicMock()
58
+ mock_session.get.return_value = mock_resp
59
+
60
+ with patch("zenkins.client.get_base_url", return_value="http://jenkins.example.com"):
61
+ resp = api_get("/api/json")
62
+
63
+ mock_session.get.assert_called_once_with("http://jenkins.example.com/api/json")
64
+ mock_resp.raise_for_status.assert_called_once()
65
+
66
+
67
+ def test_api_post(mock_session):
68
+ """Test api_post combines base URL and path."""
69
+ mock_resp = MagicMock()
70
+ mock_session.post.return_value = mock_resp
71
+
72
+ with patch("zenkins.client.get_base_url", return_value="http://jenkins.example.com"):
73
+ resp = api_post("/job/test/build")
74
+
75
+ mock_session.post.assert_called_once_with("http://jenkins.example.com/job/test/build")
76
+ mock_resp.raise_for_status.assert_called_once()
@@ -0,0 +1,46 @@
1
+ """Tests for zenkins.jobs."""
2
+
3
+ import argparse
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from zenkins.jobs import jobs_command, STATUS_MAP, COLOR_MAP
7
+
8
+
9
+ def test_jobs_command(mock_session, capsys):
10
+ """Test jobs listing."""
11
+ mock_resp = MagicMock()
12
+ mock_resp.json.return_value = {
13
+ "jobs": [
14
+ {"name": "my-job", "color": "blue", "url": "http://j/job/my-job/"},
15
+ {"name": "broken-job", "color": "red", "url": "http://j/job/broken-job/"},
16
+ ]
17
+ }
18
+ mock_session.get.return_value = mock_resp
19
+
20
+ with patch("zenkins.client.get_base_url", return_value="http://j"):
21
+ jobs_command(argparse.Namespace())
22
+
23
+ out = capsys.readouterr().out
24
+ assert "my-job" in out
25
+ assert "broken-job" in out
26
+ assert "SUCCESS" in out
27
+ assert "FAILURE" in out
28
+
29
+
30
+ def test_jobs_empty(mock_session, capsys):
31
+ """Test empty jobs list."""
32
+ mock_resp = MagicMock()
33
+ mock_resp.json.return_value = {"jobs": []}
34
+ mock_session.get.return_value = mock_resp
35
+
36
+ with patch("zenkins.client.get_base_url", return_value="http://j"):
37
+ jobs_command(argparse.Namespace())
38
+
39
+ out = capsys.readouterr().out
40
+ assert "No jobs found" in out
41
+
42
+
43
+ def test_status_map_covers_colors():
44
+ """All colors in COLOR_MAP should have a STATUS_MAP entry."""
45
+ for color in COLOR_MAP:
46
+ assert color in STATUS_MAP
@@ -0,0 +1,24 @@
1
+ """Zenkins - Jenkins CLI tool."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("zenkins")
6
+
7
+
8
+ def client() -> "requests.Session":
9
+ """Get an authenticated Jenkins session.
10
+
11
+ Returns a requests.Session configured with Jenkins credentials
12
+ from ~/.config/jenkins/config.
13
+
14
+ Usage:
15
+ import zenkins
16
+ s = zenkins.client()
17
+ resp = s.get("http://jenkins.example.com/api/json")
18
+
19
+ Returns:
20
+ requests.Session: Authenticated session
21
+ """
22
+ from zenkins.client import get_session
23
+
24
+ return get_session()
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m zenkins."""
2
+
3
+ from zenkins.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,15 @@
1
+ """zenkins build <job> - trigger a build."""
2
+
3
+ import argparse
4
+
5
+ from zenkins.client import api_post
6
+
7
+ GREEN = "\033[32m"
8
+ RESET = "\033[0m"
9
+
10
+
11
+ def build_command(args: argparse.Namespace) -> None:
12
+ """Trigger a build for a job."""
13
+ job = args.job
14
+ api_post(f"/job/{job}/build")
15
+ print(f"{GREEN}Build triggered:{RESET} {job}")
@@ -0,0 +1,71 @@
1
+ """zenkins builds <job> [-n N] - recent builds."""
2
+
3
+ import argparse
4
+ from datetime import datetime, timezone
5
+
6
+ from zenkins.client import api_get
7
+
8
+ GREEN = "\033[32m"
9
+ RED = "\033[31m"
10
+ YELLOW = "\033[33m"
11
+ GRAY = "\033[90m"
12
+ RESET = "\033[0m"
13
+
14
+ RESULT_COLORS = {
15
+ "SUCCESS": GREEN,
16
+ "FAILURE": RED,
17
+ "UNSTABLE": YELLOW,
18
+ "ABORTED": GRAY,
19
+ None: YELLOW,
20
+ }
21
+
22
+
23
+ def format_duration(ms: int) -> str:
24
+ """Format milliseconds to human-readable duration."""
25
+ seconds = ms // 1000
26
+ if seconds < 60:
27
+ return f"{seconds}s"
28
+ minutes = seconds // 60
29
+ secs = seconds % 60
30
+ if minutes < 60:
31
+ return f"{minutes}m {secs}s"
32
+ hours = minutes // 60
33
+ mins = minutes % 60
34
+ return f"{hours}h {mins}m"
35
+
36
+
37
+ def builds_command(args: argparse.Namespace) -> None:
38
+ """List recent builds for a job."""
39
+ job = args.job
40
+ n = args.n
41
+ tree = f"builds[number,result,timestamp,duration,building]{{{0},{n}}}"
42
+ resp = api_get(f"/job/{job}/api/json?tree={tree}")
43
+ data = resp.json()
44
+
45
+ builds = data.get("builds", [])
46
+ if not builds:
47
+ print(f"No builds found for {job}")
48
+ return
49
+
50
+ for build in builds:
51
+ number = build["number"]
52
+ result = build.get("result")
53
+ building = build.get("building", False)
54
+ ts = build.get("timestamp", 0)
55
+ dur = build.get("duration", 0)
56
+
57
+ dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).astimezone()
58
+ date_str = dt.strftime("%Y-%m-%d %H:%M")
59
+ dur_str = format_duration(dur)
60
+
61
+ if building:
62
+ color = YELLOW
63
+ status = "BUILDING"
64
+ elif result:
65
+ color = RESULT_COLORS.get(result, GRAY)
66
+ status = result
67
+ else:
68
+ color = GRAY
69
+ status = "?"
70
+
71
+ print(f" #{number:<6} {color}{status:>10}{RESET} {date_str} {dur_str}")
@@ -0,0 +1,73 @@
1
+ """Zenkins CLI - Main entry point."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from zenkins import __version__
7
+ from zenkins.build import build_command
8
+ from zenkins.builds import builds_command
9
+ from zenkins.init import init_command
10
+ from zenkins.jobs import jobs_command
11
+ from zenkins.log import log_command
12
+ from zenkins.queue import queue_command
13
+ from zenkins.status import status_command
14
+
15
+
16
+ def main() -> None:
17
+ parser = argparse.ArgumentParser(
18
+ prog="zenkins",
19
+ description="Jenkins CLI tool",
20
+ )
21
+ parser.add_argument(
22
+ "-V",
23
+ "--version",
24
+ action="version",
25
+ version=f"%(prog)s {__version__}",
26
+ )
27
+
28
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
29
+
30
+ # init
31
+ subparsers.add_parser("init", help="Verify Jenkins configuration and connectivity")
32
+
33
+ # jobs
34
+ subparsers.add_parser("jobs", help="List all jobs with status")
35
+
36
+ # status
37
+ status_parser = subparsers.add_parser("status", help="Show last build info for a job")
38
+ status_parser.add_argument("job", help="Job name")
39
+
40
+ # builds
41
+ builds_parser = subparsers.add_parser("builds", help="List recent builds for a job")
42
+ builds_parser.add_argument("job", help="Job name")
43
+ builds_parser.add_argument("-n", type=int, default=10, help="Number of builds (default: 10)")
44
+
45
+ # log
46
+ log_parser = subparsers.add_parser("log", help="Show console output for a build")
47
+ log_parser.add_argument("job", help="Job name")
48
+ log_parser.add_argument("build", nargs="?", help="Build number (default: last build)")
49
+
50
+ # queue
51
+ subparsers.add_parser("queue", help="Show build queue")
52
+
53
+ # build
54
+ build_parser = subparsers.add_parser("build", help="Trigger a build")
55
+ build_parser.add_argument("job", help="Job name")
56
+
57
+ args = parser.parse_args()
58
+
59
+ if not args.command:
60
+ parser.print_help()
61
+ sys.exit(1)
62
+
63
+ commands = {
64
+ "init": init_command,
65
+ "jobs": jobs_command,
66
+ "status": status_command,
67
+ "builds": builds_command,
68
+ "log": log_command,
69
+ "queue": queue_command,
70
+ "build": build_command,
71
+ }
72
+
73
+ commands[args.command](args)
@@ -0,0 +1,126 @@
1
+ """Jenkins HTTP client - session management and credential loading."""
2
+
3
+ import sys
4
+ from functools import lru_cache
5
+ from pathlib import Path
6
+
7
+ import requests
8
+
9
+ from zenkins.types import Credentials
10
+
11
+ CONFIG_FILE = Path.home() / ".config" / "jenkins" / "config"
12
+
13
+
14
+ def load_credentials() -> Credentials:
15
+ """Load credentials from ~/.config/jenkins/config (KEY=VALUE format)."""
16
+ if not CONFIG_FILE.exists():
17
+ return {}
18
+
19
+ creds: Credentials = {}
20
+ for line in CONFIG_FILE.read_text().splitlines():
21
+ line = line.strip()
22
+ if not line or line.startswith("#"):
23
+ continue
24
+ if "=" not in line:
25
+ continue
26
+ key, _, value = line.partition("=")
27
+ key = key.strip()
28
+ value = value.strip()
29
+ if key == "JENKINS_URL":
30
+ creds["url"] = value
31
+ elif key == "JENKINS_USER":
32
+ creds["user"] = value
33
+ elif key == "JENKINS_TOKEN":
34
+ creds["token"] = value
35
+ return creds
36
+
37
+
38
+ def get_credentials() -> tuple[str, str, str]:
39
+ """Get Jenkins credentials.
40
+
41
+ Returns:
42
+ Tuple of (url, user, token)
43
+ """
44
+ creds = load_credentials()
45
+ url = creds.get("url")
46
+ user = creds.get("user")
47
+ token = creds.get("token")
48
+
49
+ if not url or not user or not token:
50
+ print(f"Error: Credentials not configured in {CONFIG_FILE}", file=sys.stderr)
51
+ print("\nRun 'zenkins init' to check configuration.", file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+ return url.rstrip("/"), user, token
55
+
56
+
57
+ # Injected session for testing
58
+ _session: requests.Session | None = None
59
+
60
+
61
+ def get_session() -> requests.Session:
62
+ """Get the requests session (cached or injected)."""
63
+ global _session
64
+ if _session is not None:
65
+ return _session
66
+ return _get_default_session()
67
+
68
+
69
+ @lru_cache(maxsize=1)
70
+ def _get_default_session() -> requests.Session:
71
+ """Create the default authenticated session."""
72
+ url, user, token = get_credentials()
73
+ s = requests.Session()
74
+ s.auth = (user, token)
75
+ return s
76
+
77
+
78
+ def set_session(session: requests.Session | None) -> None:
79
+ """Inject a session for testing. Pass None to reset."""
80
+ global _session
81
+ _session = session
82
+
83
+
84
+ def reset_session() -> None:
85
+ """Reset to default session and clear cache."""
86
+ global _session
87
+ _session = None
88
+ _get_default_session.cache_clear()
89
+
90
+
91
+ def get_base_url() -> str:
92
+ """Get the Jenkins base URL."""
93
+ url, _, _ = get_credentials()
94
+ return url
95
+
96
+
97
+ def api_get(path: str, **kwargs) -> requests.Response:
98
+ """GET a Jenkins API path.
99
+
100
+ Args:
101
+ path: API path (e.g., "/api/json" or "/job/foo/api/json")
102
+ **kwargs: Extra arguments passed to session.get()
103
+
104
+ Returns:
105
+ Response object
106
+ """
107
+ url = get_base_url() + path
108
+ resp = get_session().get(url, **kwargs)
109
+ resp.raise_for_status()
110
+ return resp
111
+
112
+
113
+ def api_post(path: str, **kwargs) -> requests.Response:
114
+ """POST to a Jenkins API path.
115
+
116
+ Args:
117
+ path: API path (e.g., "/job/foo/build")
118
+ **kwargs: Extra arguments passed to session.post()
119
+
120
+ Returns:
121
+ Response object
122
+ """
123
+ url = get_base_url() + path
124
+ resp = get_session().post(url, **kwargs)
125
+ resp.raise_for_status()
126
+ return resp
@@ -0,0 +1,39 @@
1
+ """zenkins init - verify Jenkins configuration."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from zenkins.client import CONFIG_FILE, load_credentials, api_get
7
+
8
+
9
+ def init_command(args: argparse.Namespace) -> None:
10
+ """Verify Jenkins credentials and connectivity."""
11
+ print(f"Config file: {CONFIG_FILE}")
12
+
13
+ if not CONFIG_FILE.exists():
14
+ print(f"\nError: Config file not found at {CONFIG_FILE}", file=sys.stderr)
15
+ print("Create it with:", file=sys.stderr)
16
+ print(" JENKINS_URL=http://your-jenkins.example.com", file=sys.stderr)
17
+ print(" JENKINS_USER=your-username", file=sys.stderr)
18
+ print(" JENKINS_TOKEN=your-api-token", file=sys.stderr)
19
+ sys.exit(1)
20
+
21
+ creds = load_credentials()
22
+ url = creds.get("url", "")
23
+ user = creds.get("user", "")
24
+ token = creds.get("token", "")
25
+
26
+ print(f"URL: {url}")
27
+ print(f"User: {user}")
28
+ print(f"Token: {'*' * 8}...{token[-4:]}" if len(token) > 4 else f"Token: {'*' * len(token)}")
29
+
30
+ # Test connectivity
31
+ print("\nTesting connection...")
32
+ try:
33
+ resp = api_get("/api/json?tree=mode,nodeDescription,useSecurity")
34
+ data = resp.json()
35
+ print(f"Connected: {data.get('nodeDescription', 'Jenkins')}")
36
+ print(f"Security: {'enabled' if data.get('useSecurity') else 'disabled'}")
37
+ except Exception as e:
38
+ print(f"Connection failed: {e}", file=sys.stderr)
39
+ sys.exit(1)
@@ -0,0 +1,66 @@
1
+ """zenkins jobs - list all jobs with status."""
2
+
3
+ import argparse
4
+
5
+ from zenkins.client import api_get
6
+
7
+ # ANSI colors
8
+ GREEN = "\033[32m"
9
+ RED = "\033[31m"
10
+ YELLOW = "\033[33m"
11
+ BLUE = "\033[34m"
12
+ GRAY = "\033[90m"
13
+ BOLD = "\033[1m"
14
+ RESET = "\033[0m"
15
+
16
+ COLOR_MAP = {
17
+ "blue": GREEN, # success
18
+ "blue_anime": GREEN, # success + building
19
+ "red": RED, # failure
20
+ "red_anime": RED, # failure + building
21
+ "yellow": YELLOW, # unstable
22
+ "yellow_anime": YELLOW, # unstable + building
23
+ "aborted": GRAY,
24
+ "aborted_anime": GRAY,
25
+ "disabled": GRAY,
26
+ "notbuilt": GRAY,
27
+ "notbuilt_anime": GRAY,
28
+ }
29
+
30
+ STATUS_MAP = {
31
+ "blue": "SUCCESS",
32
+ "blue_anime": "BUILDING",
33
+ "red": "FAILURE",
34
+ "red_anime": "BUILDING",
35
+ "yellow": "UNSTABLE",
36
+ "yellow_anime": "BUILDING",
37
+ "aborted": "ABORTED",
38
+ "aborted_anime": "BUILDING",
39
+ "disabled": "DISABLED",
40
+ "notbuilt": "NOT BUILT",
41
+ "notbuilt_anime": "BUILDING",
42
+ }
43
+
44
+
45
+ def jobs_command(args: argparse.Namespace) -> None:
46
+ """List all jobs with their current status."""
47
+ tree = "jobs[name,color,url]"
48
+ resp = api_get(f"/api/json?tree={tree}")
49
+ data = resp.json()
50
+ jobs = data.get("jobs", [])
51
+
52
+ if not jobs:
53
+ print("No jobs found.")
54
+ return
55
+
56
+ # Calculate column width
57
+ max_name = max(len(j["name"]) for j in jobs)
58
+
59
+ for job in sorted(jobs, key=lambda j: j["name"]):
60
+ color_code = job.get("color", "notbuilt")
61
+ ansi = COLOR_MAP.get(color_code, GRAY)
62
+ status = STATUS_MAP.get(color_code, color_code)
63
+ building = " *" if "_anime" in (color_code or "") else ""
64
+
65
+ name = job["name"].ljust(max_name)
66
+ print(f" {ansi}{status:>10}{RESET} {name}{building}")
@@ -0,0 +1,14 @@
1
+ """zenkins log <job> [build] - console output."""
2
+
3
+ import argparse
4
+
5
+ from zenkins.client import api_get
6
+
7
+
8
+ def log_command(args: argparse.Namespace) -> None:
9
+ """Show console output for a build."""
10
+ job = args.job
11
+ build = args.build or "lastBuild"
12
+
13
+ resp = api_get(f"/job/{job}/{build}/consoleText")
14
+ print(resp.text)
@@ -0,0 +1,38 @@
1
+ """zenkins queue - build queue."""
2
+
3
+ import argparse
4
+
5
+ from zenkins.client import api_get
6
+
7
+ YELLOW = "\033[33m"
8
+ RED = "\033[31m"
9
+ GRAY = "\033[90m"
10
+ RESET = "\033[0m"
11
+
12
+
13
+ def queue_command(args: argparse.Namespace) -> None:
14
+ """Show the Jenkins build queue."""
15
+ resp = api_get("/queue/api/json?tree=items[id,task[name],why,stuck,blocked,buildable]")
16
+ data = resp.json()
17
+
18
+ items = data.get("items", [])
19
+ if not items:
20
+ print("Build queue is empty.")
21
+ return
22
+
23
+ print(f"Queue: {len(items)} item(s)\n")
24
+ for item in items:
25
+ task_name = item.get("task", {}).get("name", "?")
26
+ why = item.get("why", "")
27
+ stuck = item.get("stuck", False)
28
+
29
+ if stuck:
30
+ color = RED
31
+ flag = " [STUCK]"
32
+ else:
33
+ color = YELLOW
34
+ flag = ""
35
+
36
+ print(f" {color}{task_name}{RESET}{flag}")
37
+ if why:
38
+ print(f" {GRAY}{why}{RESET}")
@@ -0,0 +1,73 @@
1
+ """zenkins status <job> - last build info."""
2
+
3
+ import argparse
4
+ from datetime import datetime, timezone
5
+
6
+ from zenkins.client import api_get
7
+
8
+ GREEN = "\033[32m"
9
+ RED = "\033[31m"
10
+ YELLOW = "\033[33m"
11
+ GRAY = "\033[90m"
12
+ BOLD = "\033[1m"
13
+ RESET = "\033[0m"
14
+
15
+ RESULT_COLORS = {
16
+ "SUCCESS": GREEN,
17
+ "FAILURE": RED,
18
+ "UNSTABLE": YELLOW,
19
+ "ABORTED": GRAY,
20
+ None: YELLOW,
21
+ }
22
+
23
+
24
+ def format_duration(ms: int) -> str:
25
+ """Format milliseconds to human-readable duration."""
26
+ seconds = ms // 1000
27
+ if seconds < 60:
28
+ return f"{seconds}s"
29
+ minutes = seconds // 60
30
+ secs = seconds % 60
31
+ if minutes < 60:
32
+ return f"{minutes}m {secs}s"
33
+ hours = minutes // 60
34
+ mins = minutes % 60
35
+ return f"{hours}h {mins}m"
36
+
37
+
38
+ def format_timestamp(ts: int) -> str:
39
+ """Format Unix millisecond timestamp."""
40
+ dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).astimezone()
41
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
42
+
43
+
44
+ def status_command(args: argparse.Namespace) -> None:
45
+ """Show status of the last build for a job."""
46
+ job = args.job
47
+ tree = "lastBuild[number,result,timestamp,duration,building,displayName,description]"
48
+ resp = api_get(f"/job/{job}/api/json?tree={tree}")
49
+ data = resp.json()
50
+
51
+ build = data.get("lastBuild")
52
+ if not build:
53
+ print(f"No builds found for {job}")
54
+ return
55
+
56
+ result = build.get("result")
57
+ building = build.get("building", False)
58
+ color = RESULT_COLORS.get(result, GRAY)
59
+
60
+ if building:
61
+ status_str = f"{YELLOW}BUILDING{RESET}"
62
+ elif result:
63
+ status_str = f"{color}{result}{RESET}"
64
+ else:
65
+ status_str = f"{GRAY}UNKNOWN{RESET}"
66
+
67
+ print(f"{BOLD}{job}{RESET} #{build['number']}")
68
+ print(f" Status: {status_str}")
69
+ print(f" Started: {format_timestamp(build['timestamp'])}")
70
+ print(f" Duration: {format_duration(build['duration'])}")
71
+
72
+ if build.get("description"):
73
+ print(f" Desc: {build['description']}")
@@ -0,0 +1,46 @@
1
+ """Type definitions for Jenkins API responses."""
2
+
3
+ from typing import TypedDict
4
+
5
+
6
+ class HealthReport(TypedDict, total=False):
7
+ description: str
8
+ score: int
9
+
10
+
11
+ class Job(TypedDict, total=False):
12
+ name: str
13
+ url: str
14
+ color: str
15
+ healthReport: list[HealthReport]
16
+ lastBuild: "Build | None"
17
+ lastSuccessfulBuild: "Build | None"
18
+ lastFailedBuild: "Build | None"
19
+ inQueue: bool
20
+
21
+
22
+ class Build(TypedDict, total=False):
23
+ number: int
24
+ url: str
25
+ result: str | None
26
+ timestamp: int
27
+ duration: int
28
+ building: bool
29
+ displayName: str
30
+ description: str | None
31
+ fullDisplayName: str
32
+
33
+
34
+ class QueueItem(TypedDict, total=False):
35
+ id: int
36
+ task: Job
37
+ why: str | None
38
+ stuck: bool
39
+ buildable: bool
40
+ blocked: bool
41
+
42
+
43
+ class Credentials(TypedDict, total=False):
44
+ url: str
45
+ user: str
46
+ token: str
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: zenkins
3
+ Version: 0.1.0
4
+ Summary: CLI tool for Jenkins. List jobs, check builds, view logs, trigger builds.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/vivainio/zenkins
7
+ Project-URL: Repository, https://github.com/vivainio/zenkins
8
+ Project-URL: Issues, https://github.com/vivainio/zenkins/issues
9
+ Keywords: jenkins,cli,ci,builds
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.31.0
23
+ Requires-Dist: platformdirs>=4.0.0
24
+
25
+ # zenkins
26
+
27
+ CLI tool for Jenkins. List jobs, check builds, view logs, trigger builds.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install zenkins
33
+ ```
34
+
35
+ ## Setup
36
+
37
+ Run `zenkins init` to configure your Jenkins connection. This creates `~/.config/jenkins/config` with your Jenkins URL and credentials.
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ zenkins jobs # List all jobs with status
43
+ zenkins status <job> # Show last build info
44
+ zenkins builds <job> # List recent builds
45
+ zenkins builds <job> -n 5 # List last 5 builds
46
+ zenkins log <job> # Show console output (last build)
47
+ zenkins log <job> 42 # Show console output for build #42
48
+ zenkins queue # Show build queue
49
+ zenkins build <job> # Trigger a build
50
+ ```
51
+
52
+ ## Library usage
53
+
54
+ ```python
55
+ import zenkins
56
+
57
+ s = zenkins.client()
58
+ resp = s.get("http://jenkins.example.com/api/json")
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,23 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_builds.py
4
+ tests/test_client.py
5
+ tests/test_jobs.py
6
+ zenkins/__init__.py
7
+ zenkins/__main__.py
8
+ zenkins/build.py
9
+ zenkins/builds.py
10
+ zenkins/cli.py
11
+ zenkins/client.py
12
+ zenkins/init.py
13
+ zenkins/jobs.py
14
+ zenkins/log.py
15
+ zenkins/queue.py
16
+ zenkins/status.py
17
+ zenkins/types.py
18
+ zenkins.egg-info/PKG-INFO
19
+ zenkins.egg-info/SOURCES.txt
20
+ zenkins.egg-info/dependency_links.txt
21
+ zenkins.egg-info/entry_points.txt
22
+ zenkins.egg-info/requires.txt
23
+ zenkins.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zenkins = zenkins.cli:main
@@ -0,0 +1,2 @@
1
+ requests>=2.31.0
2
+ platformdirs>=4.0.0
@@ -0,0 +1 @@
1
+ zenkins