python-jcli 0.1.0__py3-none-any.whl
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.
- jcli/__init__.py +1 -0
- jcli/__main__.py +3 -0
- jcli/cli.py +79 -0
- jcli/cli_helpers.py +58 -0
- jcli/plugins/__init__.py +28 -0
- jcli/plugins/build.py +197 -0
- jcli/plugins/config.py +268 -0
- jcli/plugins/credential.py +210 -0
- jcli/plugins/job.py +425 -0
- jcli/plugins/node.py +134 -0
- jcli/plugins/pipeline.py +108 -0
- jcli/plugins/plugin.py +173 -0
- jcli/plugins/skills.py +399 -0
- jcli/plugins/system.py +173 -0
- jcli/plugins/view.py +148 -0
- jcli/sdk/__init__.py +0 -0
- jcli/sdk/build.py +182 -0
- jcli/sdk/client.py +264 -0
- jcli/sdk/config.py +227 -0
- jcli/sdk/credential.py +143 -0
- jcli/sdk/exceptions.py +59 -0
- jcli/sdk/job.py +191 -0
- jcli/sdk/job_templates.py +128 -0
- jcli/sdk/node.py +139 -0
- jcli/sdk/output/__init__.py +3 -0
- jcli/sdk/output/formatter.py +131 -0
- jcli/sdk/pipeline.py +121 -0
- jcli/sdk/plugin.py +155 -0
- jcli/sdk/system.py +202 -0
- jcli/sdk/view.py +94 -0
- jcli/skills/jcli/SKILL.md +73 -0
- jcli/skills/jcli/config/SKILL.md +81 -0
- jcli/skills/jcli/credential/SKILL.md +47 -0
- jcli/skills/jcli/job/SKILL.md +55 -0
- jcli/skills/jcli/node/SKILL.md +51 -0
- jcli/skills/jcli/pipeline/SKILL.md +58 -0
- jcli/skills/jcli/plugin/SKILL.md +51 -0
- jcli/skills/jcli/system/SKILL.md +70 -0
- jcli/skills/jcli/view/SKILL.md +47 -0
- python_jcli-0.1.0.dist-info/METADATA +311 -0
- python_jcli-0.1.0.dist-info/RECORD +44 -0
- python_jcli-0.1.0.dist-info/WHEEL +5 -0
- python_jcli-0.1.0.dist-info/entry_points.txt +2 -0
- python_jcli-0.1.0.dist-info/top_level.txt +1 -0
jcli/sdk/config.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Configuration management for jcli.
|
|
2
|
+
|
|
3
|
+
Handles loading/saving YAML config, profile CRUD, and environment variable overrides.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".jcli"
|
|
15
|
+
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
|
16
|
+
|
|
17
|
+
DEFAULT_PROFILE_NAME = "default"
|
|
18
|
+
|
|
19
|
+
# Environment variable names
|
|
20
|
+
ENV_URL = "JCLI_URL"
|
|
21
|
+
ENV_USERNAME = "JCLI_USERNAME"
|
|
22
|
+
ENV_API_TOKEN = "JCLI_API_TOKEN"
|
|
23
|
+
ENV_PROFILE = "JCLI_PROFILE"
|
|
24
|
+
|
|
25
|
+
# Profile field names
|
|
26
|
+
FIELD_URL = "url"
|
|
27
|
+
FIELD_USERNAME = "username"
|
|
28
|
+
FIELD_API_TOKEN = "api_token"
|
|
29
|
+
FIELD_DESCRIPTION = "description"
|
|
30
|
+
|
|
31
|
+
PROFILE_FIELDS = (FIELD_URL, FIELD_USERNAME, FIELD_API_TOKEN, FIELD_DESCRIPTION)
|
|
32
|
+
|
|
33
|
+
DEFAULT_CONFIG_TEMPLATE: dict[str, Any] = {
|
|
34
|
+
"active_profile": DEFAULT_PROFILE_NAME,
|
|
35
|
+
"profiles": {
|
|
36
|
+
DEFAULT_PROFILE_NAME: {
|
|
37
|
+
FIELD_URL: "https://jenkins.example.com",
|
|
38
|
+
FIELD_USERNAME: "admin",
|
|
39
|
+
FIELD_API_TOKEN: "your-api-token-here",
|
|
40
|
+
FIELD_DESCRIPTION: "Default Jenkins instance",
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ConfigError(Exception):
|
|
47
|
+
"""Raised when configuration operations fail."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProfileNotFoundError(ConfigError):
|
|
51
|
+
"""Raised when a requested profile does not exist."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Config:
|
|
55
|
+
"""Jenkins CLI configuration manager.
|
|
56
|
+
|
|
57
|
+
Manages profiles (url, username, api_token, description) stored in YAML.
|
|
58
|
+
Environment variables (JCLI_URL, JCLI_USERNAME, JCLI_API_TOKEN, JCLI_PROFILE)
|
|
59
|
+
override config file values when present.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, config_path: str | Path | None = None) -> None:
|
|
63
|
+
self._config_path = Path(config_path) if config_path else DEFAULT_CONFIG_FILE
|
|
64
|
+
self._data: dict[str, Any] = {}
|
|
65
|
+
self._loaded = False
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def config_path(self) -> Path:
|
|
69
|
+
return self._config_path
|
|
70
|
+
|
|
71
|
+
def load(self) -> "Config":
|
|
72
|
+
"""Load config from disk. Creates default config if file doesn't exist.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
self, for chaining.
|
|
76
|
+
"""
|
|
77
|
+
if self._config_path.exists():
|
|
78
|
+
with open(self._config_path, "r", encoding="utf-8") as f:
|
|
79
|
+
loaded = yaml.safe_load(f)
|
|
80
|
+
if loaded and isinstance(loaded, dict):
|
|
81
|
+
self._data = loaded
|
|
82
|
+
else:
|
|
83
|
+
self._data = copy.deepcopy(DEFAULT_CONFIG_TEMPLATE)
|
|
84
|
+
else:
|
|
85
|
+
self._data = copy.deepcopy(DEFAULT_CONFIG_TEMPLATE)
|
|
86
|
+
self.save()
|
|
87
|
+
|
|
88
|
+
self._loaded = True
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def save(self) -> None:
|
|
92
|
+
"""Persist current config to disk, creating parent dirs as needed."""
|
|
93
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
with open(self._config_path, "w", encoding="utf-8") as f:
|
|
95
|
+
yaml.dump(self._data, f, default_flow_style=False, allow_unicode=True)
|
|
96
|
+
|
|
97
|
+
def _ensure_loaded(self) -> None:
|
|
98
|
+
if not self._loaded:
|
|
99
|
+
self.load()
|
|
100
|
+
|
|
101
|
+
def _get_profiles(self) -> dict[str, dict[str, str]]:
|
|
102
|
+
self._ensure_loaded()
|
|
103
|
+
profiles = self._data.get("profiles", {})
|
|
104
|
+
if not isinstance(profiles, dict):
|
|
105
|
+
return {}
|
|
106
|
+
return profiles
|
|
107
|
+
|
|
108
|
+
def _apply_env_overrides(self, profile_data: dict[str, str]) -> dict[str, str]:
|
|
109
|
+
"""Apply environment variable overrides to a profile's data.
|
|
110
|
+
|
|
111
|
+
Environment variables take precedence over config file values.
|
|
112
|
+
"""
|
|
113
|
+
result = dict(profile_data)
|
|
114
|
+
|
|
115
|
+
env_url = os.environ.get(ENV_URL)
|
|
116
|
+
if env_url:
|
|
117
|
+
result[FIELD_URL] = env_url
|
|
118
|
+
|
|
119
|
+
env_username = os.environ.get(ENV_USERNAME)
|
|
120
|
+
if env_username:
|
|
121
|
+
result[FIELD_USERNAME] = env_username
|
|
122
|
+
|
|
123
|
+
env_token = os.environ.get(ENV_API_TOKEN)
|
|
124
|
+
if env_token:
|
|
125
|
+
result[FIELD_API_TOKEN] = env_token
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def get_active_profile_name(self) -> str:
|
|
130
|
+
"""Return the active profile name (env override > config file > default)."""
|
|
131
|
+
self._ensure_loaded()
|
|
132
|
+
env_profile = os.environ.get(ENV_PROFILE)
|
|
133
|
+
if env_profile:
|
|
134
|
+
return env_profile
|
|
135
|
+
return self._data.get("active_profile", DEFAULT_PROFILE_NAME)
|
|
136
|
+
|
|
137
|
+
def get_active_profile(self) -> dict[str, str]:
|
|
138
|
+
"""Return the active profile data with env overrides applied.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ProfileNotFoundError: if the active profile doesn't exist.
|
|
142
|
+
"""
|
|
143
|
+
name = self.get_active_profile_name()
|
|
144
|
+
return self.get_profile(name)
|
|
145
|
+
|
|
146
|
+
def list_profiles(self) -> dict[str, dict[str, str]]:
|
|
147
|
+
"""Return all profiles (without env overrides)."""
|
|
148
|
+
return self._get_profiles()
|
|
149
|
+
|
|
150
|
+
def get_profile(self, name: str) -> dict[str, str]:
|
|
151
|
+
"""Return a specific profile with env overrides applied.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Profile name.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Profile data dict with keys: url, username, api_token, description.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ProfileNotFoundError: if profile doesn't exist.
|
|
161
|
+
"""
|
|
162
|
+
profiles = self._get_profiles()
|
|
163
|
+
if name not in profiles:
|
|
164
|
+
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
|
165
|
+
return self._apply_env_overrides(profiles[name])
|
|
166
|
+
|
|
167
|
+
def add_profile(
|
|
168
|
+
self,
|
|
169
|
+
name: str,
|
|
170
|
+
url: str,
|
|
171
|
+
username: str,
|
|
172
|
+
api_token: str,
|
|
173
|
+
description: str = "",
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Create or update a profile.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
name: Profile name.
|
|
179
|
+
url: Jenkins server URL.
|
|
180
|
+
username: Jenkins username.
|
|
181
|
+
api_token: Jenkins API token.
|
|
182
|
+
description: Optional description.
|
|
183
|
+
"""
|
|
184
|
+
self._ensure_loaded()
|
|
185
|
+
profiles = self._data.setdefault("profiles", {})
|
|
186
|
+
profiles[name] = {
|
|
187
|
+
FIELD_URL: url,
|
|
188
|
+
FIELD_USERNAME: username,
|
|
189
|
+
FIELD_API_TOKEN: api_token,
|
|
190
|
+
FIELD_DESCRIPTION: description,
|
|
191
|
+
}
|
|
192
|
+
self.save()
|
|
193
|
+
|
|
194
|
+
def remove_profile(self, name: str) -> None:
|
|
195
|
+
"""Remove a profile.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: Profile name to remove.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ProfileNotFoundError: if profile doesn't exist.
|
|
202
|
+
"""
|
|
203
|
+
self._ensure_loaded()
|
|
204
|
+
profiles = self._data.get("profiles", {})
|
|
205
|
+
if name not in profiles:
|
|
206
|
+
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
|
207
|
+
del profiles[name]
|
|
208
|
+
# If we removed the active profile, reset to default
|
|
209
|
+
if self._data.get("active_profile") == name:
|
|
210
|
+
self._data["active_profile"] = DEFAULT_PROFILE_NAME
|
|
211
|
+
self.save()
|
|
212
|
+
|
|
213
|
+
def set_active_profile(self, name: str) -> None:
|
|
214
|
+
"""Set the active profile.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
name: Profile name to activate.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ProfileNotFoundError: if profile doesn't exist.
|
|
221
|
+
"""
|
|
222
|
+
self._ensure_loaded()
|
|
223
|
+
profiles = self._data.get("profiles", {})
|
|
224
|
+
if name not in profiles:
|
|
225
|
+
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
|
226
|
+
self._data["active_profile"] = name
|
|
227
|
+
self.save()
|
jcli/sdk/credential.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Jenkins Credential SDK.
|
|
2
|
+
|
|
3
|
+
Provides functions for managing Jenkins credentials via the REST API.
|
|
4
|
+
Default store is ``system``, default domain is ``_`` (global domain).
|
|
5
|
+
|
|
6
|
+
API reference:
|
|
7
|
+
- List: GET /credentials/store/{store}/domain/{domain}/api/json
|
|
8
|
+
- Get: GET /credentials/store/{store}/domain/_/credential/{id}/api/json
|
|
9
|
+
- Create: POST /credentials/store/{store}/domain/{domain}/createCredentials
|
|
10
|
+
- Update: POST /credentials/store/{store}/domain/_/credential/{id}/updateCredentials
|
|
11
|
+
- Delete: POST /credentials/store/{store}/domain/{domain}/credential/{id}/doDelete
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from jcli.sdk.client import JenkinsClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_STORE = "system"
|
|
22
|
+
DEFAULT_DOMAIN = "_"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_base_path(store: str, domain: str) -> str:
|
|
26
|
+
"""Build the base path for credential store/domain operations."""
|
|
27
|
+
return f"/credentials/store/{store}/domain/{domain}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def list_credentials(
|
|
31
|
+
client: JenkinsClient,
|
|
32
|
+
store: str = DEFAULT_STORE,
|
|
33
|
+
domain: str = DEFAULT_DOMAIN,
|
|
34
|
+
depth: int | None = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""List all credentials in a store/domain.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
client: Jenkins API client.
|
|
40
|
+
store: Credential store ID (default: ``system``).
|
|
41
|
+
domain: Credential domain ID (default: ``_``).
|
|
42
|
+
depth: Optional API depth parameter for nested expansion.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Parsed JSON response from Jenkins.
|
|
46
|
+
"""
|
|
47
|
+
path = f"{_build_base_path(store, domain)}/api/json"
|
|
48
|
+
params: dict[str, Any] = {}
|
|
49
|
+
if depth is not None:
|
|
50
|
+
params["depth"] = str(depth)
|
|
51
|
+
return client.get_json(path, params=params)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_credential(
|
|
55
|
+
client: JenkinsClient,
|
|
56
|
+
cred_id: str,
|
|
57
|
+
store: str = DEFAULT_STORE,
|
|
58
|
+
) -> Any:
|
|
59
|
+
"""Get details of a single credential.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
client: Jenkins API client.
|
|
63
|
+
cred_id: Credential ID.
|
|
64
|
+
store: Credential store ID (default: ``system``).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Parsed JSON response containing credential details.
|
|
68
|
+
"""
|
|
69
|
+
path = (
|
|
70
|
+
f"{_build_base_path(store, DEFAULT_DOMAIN)}"
|
|
71
|
+
f"/credential/{cred_id}/api/json"
|
|
72
|
+
)
|
|
73
|
+
return client.get_json(path)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_credential(
|
|
77
|
+
client: JenkinsClient,
|
|
78
|
+
xml_data: str | bytes,
|
|
79
|
+
store: str = DEFAULT_STORE,
|
|
80
|
+
domain: str = DEFAULT_DOMAIN,
|
|
81
|
+
) -> Any:
|
|
82
|
+
"""Create a new credential from XML config.
|
|
83
|
+
|
|
84
|
+
The XML must follow the Jenkins credentials XStream format for the
|
|
85
|
+
target credential type (e.g. ``UsernamePasswordCredentialsImpl``).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
client: Jenkins API client.
|
|
89
|
+
xml_data: Credential XML config (str or bytes).
|
|
90
|
+
store: Credential store ID (default: ``system``).
|
|
91
|
+
domain: Credential domain ID (default: ``_``).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
``requests.Response`` from the POST request.
|
|
95
|
+
"""
|
|
96
|
+
path = f"{_build_base_path(store, domain)}/createCredentials"
|
|
97
|
+
return client.post_xml(path, xml_data)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def update_credential(
|
|
101
|
+
client: JenkinsClient,
|
|
102
|
+
cred_id: str,
|
|
103
|
+
xml_data: str | bytes,
|
|
104
|
+
store: str = DEFAULT_STORE,
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""Update an existing credential.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
client: Jenkins API client.
|
|
110
|
+
cred_id: Credential ID to update.
|
|
111
|
+
xml_data: New credential XML config.
|
|
112
|
+
store: Credential store ID (default: ``system``).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
``requests.Response`` from the POST request.
|
|
116
|
+
"""
|
|
117
|
+
path = (
|
|
118
|
+
f"{_build_base_path(store, DEFAULT_DOMAIN)}"
|
|
119
|
+
f"/credential/{cred_id}/updateCredentials"
|
|
120
|
+
)
|
|
121
|
+
return client.post_xml(path, xml_data)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def delete_credential(
|
|
125
|
+
client: JenkinsClient,
|
|
126
|
+
cred_id: str,
|
|
127
|
+
store: str = DEFAULT_STORE,
|
|
128
|
+
) -> Any:
|
|
129
|
+
"""Delete a credential.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
client: Jenkins API client.
|
|
133
|
+
cred_id: Credential ID to delete.
|
|
134
|
+
store: Credential store ID (default: ``system``).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
``requests.Response`` from the POST request.
|
|
138
|
+
"""
|
|
139
|
+
path = (
|
|
140
|
+
f"{_build_base_path(store, DEFAULT_DOMAIN)}"
|
|
141
|
+
f"/credential/{cred_id}/doDelete"
|
|
142
|
+
)
|
|
143
|
+
return client.post_data(path, data={})
|
jcli/sdk/exceptions.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Jenkins SDK exception hierarchy.
|
|
2
|
+
|
|
3
|
+
All exceptions inherit from JenkinsError, so callers can catch broadly or narrowly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JenkinsError(Exception):
|
|
8
|
+
"""Base exception for all Jenkins-related errors."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str = "Jenkins error") -> None:
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.message = message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JenkinsAuthError(JenkinsError):
|
|
16
|
+
"""Raised when authentication fails (HTTP 401)."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str = "Authentication failed (401)") -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JenkinsNotFoundError(JenkinsError):
|
|
23
|
+
"""Raised when a resource is not found (HTTP 404)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str = "Resource not found (404)") -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JenkinsConnectionError(JenkinsError):
|
|
30
|
+
"""Raised when connection to Jenkins fails or times out."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str = "Connection failed") -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JenkinsAPIError(JenkinsError):
|
|
37
|
+
"""Raised for non-401/404 API errors.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
status_code: HTTP status code from the response.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, message: str = "API error", status_code: int | None = None) -> None:
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.status_code = status_code
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JenkinsConfigError(JenkinsError):
|
|
49
|
+
"""Raised for configuration errors (invalid URL, missing credentials, etc.)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, message: str = "Configuration error") -> None:
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class JenkinsCrumbError(JenkinsError):
|
|
56
|
+
"""Raised when Crumb (CSRF token) fetch fails."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, message: str = "Crumb fetch failed") -> None:
|
|
59
|
+
super().__init__(message)
|
jcli/sdk/job.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Jenkins Job SDK — CRUD and lifecycle operations for Jenkins jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from jcli.sdk.client import JenkinsClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_jobs(client: JenkinsClient, depth: int = 0) -> list[dict[str, Any]]:
|
|
11
|
+
"""List all jobs with name, url, and color.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
client: Authenticated JenkinsClient instance.
|
|
15
|
+
depth: Recursion depth for folder jobs (0 = top-level only).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
List of job dicts with keys: name, url, color.
|
|
19
|
+
"""
|
|
20
|
+
data = client.get_json("/api/json", tree="jobs[name,url,color]")
|
|
21
|
+
return data.get("jobs", [])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_job(client: JenkinsClient, name: str) -> dict[str, Any]:
|
|
25
|
+
"""Get full details for a specific job.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
client: Authenticated JenkinsClient instance.
|
|
29
|
+
name: Job name (URL-encoding is handled by the client).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Job details dict as returned by the Jenkins REST API.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
JenkinsNotFoundError: If the job does not exist.
|
|
36
|
+
"""
|
|
37
|
+
return client.get_json(f"/job/{name}/api/json")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_job(client: JenkinsClient, name: str, config_xml: str) -> None:
|
|
41
|
+
"""Create a new job from an XML configuration string.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
client: Authenticated JenkinsClient instance.
|
|
45
|
+
name: Name for the new job.
|
|
46
|
+
config_xml: Jenkins job config XML as a string.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
JenkinsAPIError: On creation failure (e.g. duplicate name, bad XML).
|
|
50
|
+
"""
|
|
51
|
+
client.post_xml(f"/createItem?name={name}", config_xml)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def delete_job(client: JenkinsClient, name: str) -> None:
|
|
55
|
+
"""Delete a job.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
client: Authenticated JenkinsClient instance.
|
|
59
|
+
name: Name of the job to delete.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
JenkinsNotFoundError: If the job does not exist.
|
|
63
|
+
"""
|
|
64
|
+
client.post_data(f"/job/{name}/doDelete", data={})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def copy_job(client: JenkinsClient, from_name: str, new_name: str) -> None:
|
|
68
|
+
"""Copy an existing job to a new name.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
client: Authenticated JenkinsClient instance.
|
|
72
|
+
from_name: Source job name.
|
|
73
|
+
new_name: Name for the copied job.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
JenkinsNotFoundError: If the source job does not exist.
|
|
77
|
+
"""
|
|
78
|
+
client.post_data(
|
|
79
|
+
"/createItem",
|
|
80
|
+
data={"name": new_name, "mode": "copy", "from": from_name},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def rename_job(
|
|
85
|
+
client: "JenkinsClient",
|
|
86
|
+
old_name: str,
|
|
87
|
+
new_name: str,
|
|
88
|
+
) -> Any:
|
|
89
|
+
"""Rename a job.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
client: Jenkins API client.
|
|
93
|
+
old_name: Current job name.
|
|
94
|
+
new_name: New job name.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Response from Jenkins (redirect on success).
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
JenkinsNotFoundError: If the job does not exist.
|
|
101
|
+
"""
|
|
102
|
+
path = f"/job/{old_name}/doRename"
|
|
103
|
+
return client.request("POST", path, params={"newName": new_name})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def enable_job(client: JenkinsClient, name: str) -> None:
|
|
107
|
+
"""Enable a disabled job.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
client: Authenticated JenkinsClient instance.
|
|
111
|
+
name: Name of the job to enable.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
JenkinsNotFoundError: If the job does not exist.
|
|
115
|
+
"""
|
|
116
|
+
client.post_data(f"/job/{name}/enable", data={})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def disable_job(client: JenkinsClient, name: str) -> None:
|
|
120
|
+
"""Disable a job.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
client: Authenticated JenkinsClient instance.
|
|
124
|
+
name: Name of the job to disable.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
JenkinsNotFoundError: If the job does not exist.
|
|
128
|
+
"""
|
|
129
|
+
client.post_data(f"/job/{name}/disable", data={})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def update_job_config(
|
|
133
|
+
client: JenkinsClient,
|
|
134
|
+
name: str,
|
|
135
|
+
config_xml: str,
|
|
136
|
+
) -> Any:
|
|
137
|
+
"""Update a job's configuration.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
client: Jenkins API client.
|
|
141
|
+
name: Job name.
|
|
142
|
+
config_xml: New XML configuration.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Response from Jenkins.
|
|
146
|
+
"""
|
|
147
|
+
return client.post_xml(f"/job/{name}/config.xml", config_xml)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_job_config(client: JenkinsClient, name: str) -> str:
|
|
151
|
+
"""Retrieve the XML configuration for a job.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
client: Authenticated JenkinsClient instance.
|
|
155
|
+
name: Name of the job.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Job configuration as an XML string.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
JenkinsNotFoundError: If the job does not exist.
|
|
162
|
+
"""
|
|
163
|
+
resp = client.request("GET", f"/job/{name}/config.xml")
|
|
164
|
+
return resp.text
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def create_folder(client: JenkinsClient, name: str) -> Any:
|
|
168
|
+
"""Create a Jenkins folder.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
client: Jenkins API client.
|
|
172
|
+
name: Folder name.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Response from Jenkins.
|
|
176
|
+
"""
|
|
177
|
+
path = f"/createItem?name={name}&mode=com.cloudbees.hudson.plugins.folder.Folder"
|
|
178
|
+
return client.request("POST", path)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def delete_folder(client: JenkinsClient, name: str) -> Any:
|
|
182
|
+
"""Delete a Jenkins folder and all its contents.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
client: Jenkins API client.
|
|
186
|
+
name: Folder name.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Response from Jenkins.
|
|
190
|
+
"""
|
|
191
|
+
return client.request("POST", f"/job/{name}/doDelete")
|