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 +63 -0
- zenkins-0.1.0/README.md +39 -0
- zenkins-0.1.0/pyproject.toml +41 -0
- zenkins-0.1.0/setup.cfg +4 -0
- zenkins-0.1.0/tests/test_builds.py +86 -0
- zenkins-0.1.0/tests/test_client.py +76 -0
- zenkins-0.1.0/tests/test_jobs.py +46 -0
- zenkins-0.1.0/zenkins/__init__.py +24 -0
- zenkins-0.1.0/zenkins/__main__.py +6 -0
- zenkins-0.1.0/zenkins/build.py +15 -0
- zenkins-0.1.0/zenkins/builds.py +71 -0
- zenkins-0.1.0/zenkins/cli.py +73 -0
- zenkins-0.1.0/zenkins/client.py +126 -0
- zenkins-0.1.0/zenkins/init.py +39 -0
- zenkins-0.1.0/zenkins/jobs.py +66 -0
- zenkins-0.1.0/zenkins/log.py +14 -0
- zenkins-0.1.0/zenkins/queue.py +38 -0
- zenkins-0.1.0/zenkins/status.py +73 -0
- zenkins-0.1.0/zenkins/types.py +46 -0
- zenkins-0.1.0/zenkins.egg-info/PKG-INFO +63 -0
- zenkins-0.1.0/zenkins.egg-info/SOURCES.txt +23 -0
- zenkins-0.1.0/zenkins.egg-info/dependency_links.txt +1 -0
- zenkins-0.1.0/zenkins.egg-info/entry_points.txt +2 -0
- zenkins-0.1.0/zenkins.egg-info/requires.txt +2 -0
- zenkins-0.1.0/zenkins.egg-info/top_level.txt +1 -0
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
|
zenkins-0.1.0/README.md
ADDED
|
@@ -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"]
|
zenkins-0.1.0/setup.cfg
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zenkins
|