python-jcli 0.1.0__py3-none-any.whl → 1.0.1__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 -1
- jcli/cli.py +565 -51
- jcli/cli_helpers.py +5 -42
- jcli/skills/jcli/SKILL.md +15 -2
- jcli/skills/jcli/auth/SKILL.md +86 -0
- jcli/skills/jcli/build/SKILL.md +123 -0
- jcli/skills/jcli/credential/SKILL.md +14 -5
- jcli/skills/jcli/job/SKILL.md +20 -4
- jcli/skills/jcli/node/SKILL.md +4 -2
- jcli/skills/jcli/pipeline/SKILL.md +3 -3
- jcli/skills/jcli/plugin/SKILL.md +3 -0
- jcli/skills/jcli/system/SKILL.md +15 -2
- jcli/skills/jcli/view/SKILL.md +4 -3
- jcli/specs/_auth.yaml +15 -0
- jcli/{plugins/skills.py → specs/plugins/jcli_commands.py} +124 -63
- jcli/specs/plugins/jenkins_auth.py +75 -0
- jcli/specs/plugins/jenkins_methods.py +332 -0
- jcli/specs/plugins/jenkins_pipeline_validate.py +51 -0
- jcli/specs/plugins/jenkins_plugin.py +77 -0
- jcli/specs/plugins/jenkins_system.py +137 -0
- jcli/specs/resources/build.yaml +160 -0
- jcli/specs/resources/credential.yaml +97 -0
- jcli/specs/resources/job.yaml +184 -0
- jcli/specs/resources/node.yaml +69 -0
- jcli/specs/resources/pipeline.yaml +62 -0
- jcli/specs/resources/plugin.yaml +65 -0
- jcli/specs/resources/system.yaml +72 -0
- jcli/specs/resources/view.yaml +75 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/METADATA +57 -12
- python_jcli-1.0.1.dist-info/RECORD +40 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/WHEEL +1 -1
- jcli/plugins/__init__.py +0 -28
- jcli/plugins/build.py +0 -197
- jcli/plugins/config.py +0 -268
- jcli/plugins/credential.py +0 -210
- jcli/plugins/job.py +0 -425
- jcli/plugins/node.py +0 -134
- jcli/plugins/pipeline.py +0 -108
- jcli/plugins/plugin.py +0 -173
- jcli/plugins/system.py +0 -173
- jcli/plugins/view.py +0 -148
- jcli/sdk/build.py +0 -182
- jcli/sdk/credential.py +0 -143
- jcli/sdk/job.py +0 -191
- jcli/sdk/job_templates.py +0 -128
- jcli/sdk/node.py +0 -139
- jcli/sdk/pipeline.py +0 -121
- jcli/sdk/plugin.py +0 -155
- jcli/sdk/system.py +0 -202
- jcli/sdk/view.py +0 -94
- jcli/skills/jcli/config/SKILL.md +0 -81
- python_jcli-0.1.0.dist-info/RECORD +0 -44
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/entry_points.txt +0 -0
- {python_jcli-0.1.0.dist-info → python_jcli-1.0.1.dist-info}/top_level.txt +0 -0
jcli/plugins/plugin.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
"""Jenkins Plugin management commands."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
from jcli.cli_helpers import get_client, get_formatter
|
|
8
|
-
from jcli.sdk.plugin import (
|
|
9
|
-
check_plugin_updates,
|
|
10
|
-
get_plugin,
|
|
11
|
-
install_plugins,
|
|
12
|
-
list_plugins,
|
|
13
|
-
uninstall_plugin,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _parse_plugin_spec(spec: str) -> tuple[str, str | None]:
|
|
18
|
-
"""Parse a plugin spec like 'git' or 'git@5.0.0'.
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
(short_name, version_or_None)
|
|
22
|
-
"""
|
|
23
|
-
if "@" in spec:
|
|
24
|
-
name, _, version = spec.partition("@")
|
|
25
|
-
return name, version
|
|
26
|
-
return spec, None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# ------------------------------------------------------------------
|
|
30
|
-
# Plugin command group
|
|
31
|
-
# ------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@click.group("plugin", help="Manage Jenkins plugins.")
|
|
35
|
-
@click.pass_context
|
|
36
|
-
def plugin_group(ctx):
|
|
37
|
-
"""Plugin management commands."""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@plugin_group.command("list")
|
|
41
|
-
@click.pass_context
|
|
42
|
-
def list_cmd(ctx):
|
|
43
|
-
"""List installed Jenkins plugins."""
|
|
44
|
-
client = get_client(ctx)
|
|
45
|
-
fmt = get_formatter(ctx)
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
plugins = list_plugins(client)
|
|
49
|
-
except Exception as exc:
|
|
50
|
-
fmt.print_error(str(exc))
|
|
51
|
-
raise SystemExit(1)
|
|
52
|
-
|
|
53
|
-
if not plugins:
|
|
54
|
-
fmt.print_info("No plugins found.")
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
headers = ["Name", "Version", "Active", "Update"]
|
|
58
|
-
rows = [
|
|
59
|
-
[p.get("shortName", "?"), p.get("version", "?"),
|
|
60
|
-
"Yes" if p.get("active") else "No",
|
|
61
|
-
"Yes" if p.get("hasUpdate") else "No"]
|
|
62
|
-
for p in plugins
|
|
63
|
-
]
|
|
64
|
-
fmt.print_table(headers, rows, title="Installed Plugins")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@plugin_group.command("get")
|
|
68
|
-
@click.argument("name")
|
|
69
|
-
@click.pass_context
|
|
70
|
-
def get_cmd(ctx, name):
|
|
71
|
-
"""Show details for a single plugin."""
|
|
72
|
-
client = get_client(ctx)
|
|
73
|
-
fmt = get_formatter(ctx)
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
plugin = get_plugin(client, name)
|
|
77
|
-
except Exception as exc:
|
|
78
|
-
fmt.print_error(str(exc))
|
|
79
|
-
raise SystemExit(1)
|
|
80
|
-
|
|
81
|
-
# Display all fields in a vertical key-value table
|
|
82
|
-
headers = ["Field", "Value"]
|
|
83
|
-
rows = [[str(k), str(v)] for k, v in plugin.items()] if plugin else []
|
|
84
|
-
fmt.print_table(
|
|
85
|
-
headers,
|
|
86
|
-
rows,
|
|
87
|
-
title=f"Plugin: {plugin.get('shortName', name)}" if plugin else f"Plugin {name}",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@plugin_group.command("install")
|
|
92
|
-
@click.argument("plugin_spec")
|
|
93
|
-
@click.pass_context
|
|
94
|
-
def install_cmd(ctx, plugin_spec):
|
|
95
|
-
"""Install a Jenkins plugin.
|
|
96
|
-
|
|
97
|
-
PLUGIN_SPEC can be just a plugin name (e.g. 'git') or
|
|
98
|
-
name@version (e.g. 'git@5.0.0') to install a specific version.
|
|
99
|
-
"""
|
|
100
|
-
client = get_client(ctx)
|
|
101
|
-
fmt = get_formatter(ctx)
|
|
102
|
-
|
|
103
|
-
name, version = _parse_plugin_spec(plugin_spec)
|
|
104
|
-
plugins_dict: dict[str, str | None] = {name: version}
|
|
105
|
-
|
|
106
|
-
try:
|
|
107
|
-
install_plugins(client, plugins_dict)
|
|
108
|
-
except Exception as exc:
|
|
109
|
-
fmt.print_error(str(exc))
|
|
110
|
-
raise SystemExit(1)
|
|
111
|
-
|
|
112
|
-
msg = f"Plugin '{name}' installed successfully"
|
|
113
|
-
if version:
|
|
114
|
-
msg += f" (version {version})"
|
|
115
|
-
fmt.print_success(msg)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
@plugin_group.command("uninstall")
|
|
119
|
-
@click.argument("name")
|
|
120
|
-
@click.pass_context
|
|
121
|
-
def uninstall_cmd(ctx, name):
|
|
122
|
-
"""Uninstall a Jenkins plugin.
|
|
123
|
-
|
|
124
|
-
The plugin is scheduled for removal; a server restart is typically
|
|
125
|
-
required to complete the uninstall.
|
|
126
|
-
"""
|
|
127
|
-
client = get_client(ctx)
|
|
128
|
-
fmt = get_formatter(ctx)
|
|
129
|
-
|
|
130
|
-
try:
|
|
131
|
-
uninstall_plugin(client, name)
|
|
132
|
-
except Exception as exc:
|
|
133
|
-
fmt.print_error(str(exc))
|
|
134
|
-
raise SystemExit(1)
|
|
135
|
-
|
|
136
|
-
fmt.print_success(f"Plugin '{name}' scheduled for uninstall (restart may be required)")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@plugin_group.command("check-updates")
|
|
140
|
-
@click.pass_context
|
|
141
|
-
def check_updates_cmd(ctx):
|
|
142
|
-
"""Check for available plugin updates."""
|
|
143
|
-
client = get_client(ctx)
|
|
144
|
-
fmt = get_formatter(ctx)
|
|
145
|
-
try:
|
|
146
|
-
result = check_plugin_updates(client)
|
|
147
|
-
fmt.print_info("Plugin update check triggered. Run 'jcli plugin list' to see results.")
|
|
148
|
-
if isinstance(result, dict) and result:
|
|
149
|
-
fmt.print_json(result)
|
|
150
|
-
except Exception as exc:
|
|
151
|
-
fmt.print_error(str(exc))
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@plugin_group.command("restart")
|
|
155
|
-
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
|
|
156
|
-
@click.pass_context
|
|
157
|
-
def restart_cmd(ctx, yes):
|
|
158
|
-
"""Restart Jenkins after plugin changes."""
|
|
159
|
-
if not yes:
|
|
160
|
-
click.confirm("Restart Jenkins server?", abort=True)
|
|
161
|
-
client = get_client(ctx)
|
|
162
|
-
fmt = get_formatter(ctx)
|
|
163
|
-
try:
|
|
164
|
-
from jcli.sdk.system import safe_restart
|
|
165
|
-
safe_restart(client)
|
|
166
|
-
fmt.print_success("Jenkins restart initiated.")
|
|
167
|
-
except Exception as exc:
|
|
168
|
-
fmt.print_error(str(exc))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def register(parent_group):
|
|
172
|
-
"""Register the plugin subgroup under the parent Click group."""
|
|
173
|
-
parent_group.add_command(plugin_group)
|
jcli/plugins/system.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
"""Jenkins System management commands."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
from jcli.cli_helpers import get_client, get_formatter
|
|
8
|
-
from jcli.sdk.client import JenkinsClient
|
|
9
|
-
from jcli.sdk.config import Config
|
|
10
|
-
from jcli.sdk.exceptions import JenkinsConfigError
|
|
11
|
-
from jcli.sdk.system import (
|
|
12
|
-
cancel_quiet_down,
|
|
13
|
-
generate_token,
|
|
14
|
-
get_system_info,
|
|
15
|
-
get_system_load,
|
|
16
|
-
list_users,
|
|
17
|
-
quiet_down,
|
|
18
|
-
run_script,
|
|
19
|
-
safe_restart,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@click.group("system", help="Jenkins system information and management.")
|
|
26
|
-
def system_group():
|
|
27
|
-
"""System management commands."""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@system_group.command("info")
|
|
31
|
-
@click.pass_context
|
|
32
|
-
def system_info(ctx: click.Context):
|
|
33
|
-
"""Show Jenkins system information."""
|
|
34
|
-
formatter = get_formatter(ctx)
|
|
35
|
-
client = get_client(ctx)
|
|
36
|
-
|
|
37
|
-
info = get_system_info(client)
|
|
38
|
-
|
|
39
|
-
formatter.print_info(f"Jenkins {info['version']}")
|
|
40
|
-
formatter.print_info(f"Mode: {info['mode']}")
|
|
41
|
-
formatter.print_info(f"Quieting down: {info['quietingDown']}")
|
|
42
|
-
|
|
43
|
-
rows = [
|
|
44
|
-
("Executors", str(info["num_executors"])),
|
|
45
|
-
("Nodes", str(info["num_nodes"])),
|
|
46
|
-
("Jobs", str(info["num_jobs"])),
|
|
47
|
-
("Slave Agent Port", str(info["slaveAgentPort"])),
|
|
48
|
-
]
|
|
49
|
-
formatter.print_table(
|
|
50
|
-
headers=("Property", "Value"),
|
|
51
|
-
rows=rows,
|
|
52
|
-
title="System Overview",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@system_group.command("restart")
|
|
57
|
-
@click.pass_context
|
|
58
|
-
def system_restart(ctx: click.Context):
|
|
59
|
-
"""Trigger a safe restart of Jenkins."""
|
|
60
|
-
formatter = get_formatter(ctx)
|
|
61
|
-
client = get_client(ctx)
|
|
62
|
-
|
|
63
|
-
if safe_restart(client):
|
|
64
|
-
formatter.print_success("Safe restart initiated.")
|
|
65
|
-
else:
|
|
66
|
-
formatter.print_error("Failed to initiate safe restart.")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@system_group.command("quiet-down")
|
|
70
|
-
@click.option("--reason", "-r", default="", help="Reason for quiet-down.")
|
|
71
|
-
@click.pass_context
|
|
72
|
-
def system_quiet_down(ctx: click.Context, reason: str):
|
|
73
|
-
"""Put Jenkins into quiet-down mode."""
|
|
74
|
-
formatter = get_formatter(ctx)
|
|
75
|
-
client = get_client(ctx)
|
|
76
|
-
|
|
77
|
-
if quiet_down(client, reason=reason):
|
|
78
|
-
formatter.print_success("Jenkins is now in quiet-down mode.")
|
|
79
|
-
if reason:
|
|
80
|
-
formatter.print_info(f"Reason: {reason}")
|
|
81
|
-
else:
|
|
82
|
-
formatter.print_error("Failed to enter quiet-down mode.")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@system_group.command("cancel-quiet-down")
|
|
86
|
-
@click.pass_context
|
|
87
|
-
def system_cancel_quiet_down(ctx: click.Context):
|
|
88
|
-
"""Cancel quiet-down mode."""
|
|
89
|
-
formatter = get_formatter(ctx)
|
|
90
|
-
client = get_client(ctx)
|
|
91
|
-
|
|
92
|
-
if cancel_quiet_down(client):
|
|
93
|
-
formatter.print_success("Quiet-down mode cancelled.")
|
|
94
|
-
else:
|
|
95
|
-
formatter.print_error("Failed to cancel quiet-down mode.")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@system_group.command("load")
|
|
99
|
-
@click.pass_context
|
|
100
|
-
def system_load(ctx: click.Context):
|
|
101
|
-
"""Show current system load (queue length, executor usage)."""
|
|
102
|
-
formatter = get_formatter(ctx)
|
|
103
|
-
client = get_client(ctx)
|
|
104
|
-
|
|
105
|
-
load = get_system_load(client)
|
|
106
|
-
|
|
107
|
-
rows = [
|
|
108
|
-
("Queue Length", str(load["queue_length"])),
|
|
109
|
-
("Total Executors", str(load["total_executors"])),
|
|
110
|
-
("Busy Executors", str(load["busy_executors"])),
|
|
111
|
-
("Idle Executors", str(load["idle_executors"])),
|
|
112
|
-
]
|
|
113
|
-
formatter.print_table(
|
|
114
|
-
headers=("Metric", "Value"),
|
|
115
|
-
rows=rows,
|
|
116
|
-
title="System Load",
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@system_group.command("script")
|
|
121
|
-
@click.argument("script")
|
|
122
|
-
@click.pass_context
|
|
123
|
-
def script_cmd(ctx: click.Context, script: str):
|
|
124
|
-
"""Execute a Groovy script on the Jenkins server.
|
|
125
|
-
|
|
126
|
-
SCRIPT is the Groovy script text to execute.
|
|
127
|
-
"""
|
|
128
|
-
client = get_client(ctx)
|
|
129
|
-
fmt = get_formatter(ctx)
|
|
130
|
-
try:
|
|
131
|
-
result = run_script(client, script)
|
|
132
|
-
print(result)
|
|
133
|
-
except Exception as exc:
|
|
134
|
-
fmt.print_error(str(exc))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@system_group.command("users")
|
|
138
|
-
@click.pass_context
|
|
139
|
-
def users_cmd(ctx: click.Context):
|
|
140
|
-
"""List all Jenkins users."""
|
|
141
|
-
client = get_client(ctx)
|
|
142
|
-
fmt = get_formatter(ctx)
|
|
143
|
-
try:
|
|
144
|
-
data = list_users(client)
|
|
145
|
-
users = data.get("users", [])
|
|
146
|
-
if not users:
|
|
147
|
-
fmt.print_info("No users found.")
|
|
148
|
-
return
|
|
149
|
-
user_ids = [u["user"]["fullName"] for u in users]
|
|
150
|
-
fmt.print_json(user_ids)
|
|
151
|
-
except Exception as exc:
|
|
152
|
-
fmt.print_error(str(exc))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@system_group.command("token")
|
|
156
|
-
@click.argument("username")
|
|
157
|
-
@click.option("--name", "-n", default="jcli-generated", help="Token name/label.")
|
|
158
|
-
@click.pass_context
|
|
159
|
-
def token_cmd(ctx: click.Context, username: str, name: str):
|
|
160
|
-
"""Generate a new API token for a user."""
|
|
161
|
-
client = get_client(ctx)
|
|
162
|
-
fmt = get_formatter(ctx)
|
|
163
|
-
try:
|
|
164
|
-
result = generate_token(client, username, token_name=name)
|
|
165
|
-
fmt.print_json(result)
|
|
166
|
-
fmt.print_success(f"Token generated for user '{username}'.")
|
|
167
|
-
except Exception as exc:
|
|
168
|
-
fmt.print_error(str(exc))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def register(parent_group):
|
|
172
|
-
"""Register the system subgroup under the parent Click group."""
|
|
173
|
-
parent_group.add_command(system_group)
|
jcli/plugins/view.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Jenkins View management commands."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
|
|
10
|
-
from jcli.cli_helpers import get_client, get_formatter
|
|
11
|
-
from jcli.sdk.view import create_view as sdk_create_view
|
|
12
|
-
from jcli.sdk.view import delete_view as sdk_delete_view
|
|
13
|
-
from jcli.sdk.view import get_view as sdk_get_view
|
|
14
|
-
from jcli.sdk.view import list_views as sdk_list_views
|
|
15
|
-
from jcli.sdk.view import update_view as sdk_update_view
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _format_output(ctx: click.Context, data: object) -> str:
|
|
19
|
-
"""Format data according to the user-specified output format."""
|
|
20
|
-
ctx_obj = ctx.obj or {}
|
|
21
|
-
fmt = ctx_obj.get("format", "table")
|
|
22
|
-
|
|
23
|
-
if fmt == "json":
|
|
24
|
-
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
25
|
-
|
|
26
|
-
if fmt == "yaml":
|
|
27
|
-
import yaml
|
|
28
|
-
|
|
29
|
-
return yaml.dump(data, default_flow_style=False, allow_unicode=True)
|
|
30
|
-
|
|
31
|
-
# Default: table-like plain text
|
|
32
|
-
return str(data)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# ------------------------------------------------------------------
|
|
36
|
-
# Click command group
|
|
37
|
-
# ------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@click.group("view", help="Manage Jenkins views.")
|
|
41
|
-
def view_group():
|
|
42
|
-
"""View management commands."""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@view_group.command("list")
|
|
46
|
-
@click.pass_context
|
|
47
|
-
def view_list(ctx: click.Context) -> None:
|
|
48
|
-
"""List all Jenkins views."""
|
|
49
|
-
client = get_client(ctx)
|
|
50
|
-
views = sdk_list_views(client)
|
|
51
|
-
|
|
52
|
-
# Build table output
|
|
53
|
-
ctx_obj = ctx.obj or {}
|
|
54
|
-
if ctx_obj.get("format", "table") == "table":
|
|
55
|
-
if not views:
|
|
56
|
-
click.echo("No views found.")
|
|
57
|
-
return
|
|
58
|
-
for v in views:
|
|
59
|
-
job_count = len(v.get("jobs", []))
|
|
60
|
-
click.echo(f" {v['name']} ({job_count} jobs)")
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
click.echo(_format_output(ctx, views))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@view_group.command("get")
|
|
67
|
-
@click.argument("view_name", metavar="NAME")
|
|
68
|
-
@click.pass_context
|
|
69
|
-
def view_get(ctx: click.Context, view_name: str) -> None:
|
|
70
|
-
"""Get details for a Jenkins view."""
|
|
71
|
-
client = get_client(ctx)
|
|
72
|
-
data = sdk_get_view(client, view_name)
|
|
73
|
-
|
|
74
|
-
# Table output: show key fields
|
|
75
|
-
ctx_obj = ctx.obj or {}
|
|
76
|
-
if ctx_obj.get("format", "table") == "table":
|
|
77
|
-
click.echo(f"Name: {data.get('name', view_name)}")
|
|
78
|
-
click.echo(f"URL: {data.get('url', '')}")
|
|
79
|
-
click.echo(f"Description: {data.get('description', '')}")
|
|
80
|
-
jobs = data.get("jobs", [])
|
|
81
|
-
if jobs:
|
|
82
|
-
click.echo("Jobs:")
|
|
83
|
-
for j in jobs:
|
|
84
|
-
click.echo(f" - {j['name']} [{j.get('color', 'unknown')}]")
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
click.echo(_format_output(ctx, data))
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@view_group.command("create")
|
|
91
|
-
@click.argument("view_name", metavar="NAME")
|
|
92
|
-
@click.argument(
|
|
93
|
-
"config_file",
|
|
94
|
-
metavar="CONFIG_XML",
|
|
95
|
-
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
96
|
-
)
|
|
97
|
-
@click.pass_context
|
|
98
|
-
def view_create(ctx: click.Context, view_name: str, config_file: Path) -> None:
|
|
99
|
-
"""Create a new Jenkins view from an XML config file."""
|
|
100
|
-
xml_data = config_file.read_text(encoding="utf-8")
|
|
101
|
-
|
|
102
|
-
client = get_client(ctx)
|
|
103
|
-
sdk_create_view(client, view_name, xml_data)
|
|
104
|
-
click.echo(f"View '{view_name}' created successfully.")
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@view_group.command("update")
|
|
108
|
-
@click.argument("view_name")
|
|
109
|
-
@click.argument("config_file", type=click.Path(exists=True, dir_okay=False))
|
|
110
|
-
@click.pass_context
|
|
111
|
-
def update_view_cmd(ctx, view_name, config_file):
|
|
112
|
-
"""Update a view's configuration from an XML file."""
|
|
113
|
-
client = get_client(ctx)
|
|
114
|
-
fmt = get_formatter(ctx)
|
|
115
|
-
try:
|
|
116
|
-
with open(config_file, "r", encoding="utf-8") as f:
|
|
117
|
-
config_xml = f.read()
|
|
118
|
-
sdk_update_view(client, view_name, config_xml)
|
|
119
|
-
fmt.print_success(f"View '{view_name}' updated.")
|
|
120
|
-
except Exception as exc:
|
|
121
|
-
fmt.print_error(str(exc))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@view_group.command("delete")
|
|
125
|
-
@click.argument("view_name", metavar="NAME")
|
|
126
|
-
@click.option(
|
|
127
|
-
"--yes",
|
|
128
|
-
"-y",
|
|
129
|
-
is_flag=True,
|
|
130
|
-
help="Skip confirmation prompt.",
|
|
131
|
-
)
|
|
132
|
-
@click.pass_context
|
|
133
|
-
def view_delete(ctx: click.Context, view_name: str, yes: bool) -> None:
|
|
134
|
-
"""Delete a Jenkins view."""
|
|
135
|
-
if not yes:
|
|
136
|
-
click.confirm(
|
|
137
|
-
f"Are you sure you want to delete view '{view_name}'?",
|
|
138
|
-
abort=True,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
client = get_client(ctx)
|
|
142
|
-
sdk_delete_view(client, view_name)
|
|
143
|
-
click.echo(f"View '{view_name}' deleted successfully.")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def register(parent_group: click.Group) -> None:
|
|
147
|
-
"""Register the view subgroup under the parent Click group."""
|
|
148
|
-
parent_group.add_command(view_group)
|
jcli/sdk/build.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
"""Jenkins Build SDK — trigger, query, stop builds and manage queue."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
import urllib.parse
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from jcli.sdk.client import JenkinsClient
|
|
10
|
-
from jcli.sdk.exceptions import JenkinsError
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _job_path(job_name: str) -> str:
|
|
14
|
-
"""Convert a job name to a Jenkins URL path segment.
|
|
15
|
-
|
|
16
|
-
Handles nested jobs (folder/sub-job) by converting each slash-separated
|
|
17
|
-
segment into ``/job/<segment>``.
|
|
18
|
-
"""
|
|
19
|
-
segments = job_name.split("/")
|
|
20
|
-
return "/job/" + "/job/".join(segments)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def trigger_build(
|
|
24
|
-
client: JenkinsClient,
|
|
25
|
-
job_name: str,
|
|
26
|
-
params: dict[str, Any] | None = None,
|
|
27
|
-
) -> dict[str, Any]:
|
|
28
|
-
"""Trigger a new build for the given job.
|
|
29
|
-
|
|
30
|
-
If *params* is provided the build is triggered with parameters via
|
|
31
|
-
``/buildWithParameters``, otherwise a plain ``/build`` is used.
|
|
32
|
-
Jenkins returns a ``Location`` header pointing to the queued item.
|
|
33
|
-
This function follows that redirect and parses the queue-item JSON.
|
|
34
|
-
|
|
35
|
-
:returns: Queue item info as returned by Jenkins (dict).
|
|
36
|
-
"""
|
|
37
|
-
path = _job_path(job_name)
|
|
38
|
-
if params:
|
|
39
|
-
url = f"{path}/buildWithParameters"
|
|
40
|
-
resp = client.request("POST", url, params=params, allow_redirects=False)
|
|
41
|
-
else:
|
|
42
|
-
url = f"{path}/build"
|
|
43
|
-
resp = client.request("POST", url, allow_redirects=False)
|
|
44
|
-
|
|
45
|
-
# Jenkins responds with 201 Created or 302 Found and a Location header
|
|
46
|
-
# pointing to the queue item. Follow that URL to get the queue item JSON.
|
|
47
|
-
location = resp.headers.get("Location")
|
|
48
|
-
if location:
|
|
49
|
-
queue_path = urllib.parse.urlparse(location).path
|
|
50
|
-
# Strip the base URL prefix — client.get_json accepts an absolute path
|
|
51
|
-
if queue_path.startswith(client.base_url):
|
|
52
|
-
queue_path = queue_path[len(client.base_url) :]
|
|
53
|
-
try:
|
|
54
|
-
return client.get_json(queue_path + "api/json")
|
|
55
|
-
except JenkinsError:
|
|
56
|
-
pass
|
|
57
|
-
|
|
58
|
-
# Fallback — return a minimal dict
|
|
59
|
-
return {"status": "queued", "url": location or ""}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def get_build(
|
|
63
|
-
client: JenkinsClient,
|
|
64
|
-
job_name: str,
|
|
65
|
-
number: int,
|
|
66
|
-
) -> dict[str, Any]:
|
|
67
|
-
"""Get details for a specific build.
|
|
68
|
-
|
|
69
|
-
:returns: Build details as returned by the Jenkins API.
|
|
70
|
-
"""
|
|
71
|
-
path = f"{_job_path(job_name)}/{number}"
|
|
72
|
-
return client.get_json(f"{path}/api/json")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_builds(
|
|
76
|
-
client: JenkinsClient,
|
|
77
|
-
job_name: str,
|
|
78
|
-
limit: int = 10,
|
|
79
|
-
) -> list[dict[str, Any]]:
|
|
80
|
-
"""List recent builds for a job.
|
|
81
|
-
|
|
82
|
-
:param limit: Maximum number of builds to return (applied client-side).
|
|
83
|
-
:returns: List of build dicts (trimmed to *limit*).
|
|
84
|
-
"""
|
|
85
|
-
path = _job_path(job_name)
|
|
86
|
-
tree = f"builds[number,url,result,timestamp,duration,building]{f'{{{0},{limit - 1}}}' if limit > 0 else ''}"
|
|
87
|
-
data = client.get_json(f"{path}/api/json", tree=tree)
|
|
88
|
-
builds: list[dict[str, Any]] = data.get("builds", [])
|
|
89
|
-
return builds[:limit]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def get_build_log(
|
|
93
|
-
client: JenkinsClient,
|
|
94
|
-
job_name: str,
|
|
95
|
-
number: int,
|
|
96
|
-
start: int = 0,
|
|
97
|
-
) -> str:
|
|
98
|
-
"""Retrieve the console log for a build.
|
|
99
|
-
|
|
100
|
-
:param start: Byte offset to start reading from (useful for polling).
|
|
101
|
-
:returns: Console log text.
|
|
102
|
-
"""
|
|
103
|
-
path = f"{_job_path(job_name)}/{number}/consoleText"
|
|
104
|
-
resp = client.request("GET", path)
|
|
105
|
-
text = resp.text
|
|
106
|
-
if start > 0:
|
|
107
|
-
return text[start:]
|
|
108
|
-
return text
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def stop_build(
|
|
112
|
-
client: JenkinsClient,
|
|
113
|
-
job_name: str,
|
|
114
|
-
number: int,
|
|
115
|
-
) -> dict[str, Any]:
|
|
116
|
-
"""Stop (abort) a running build.
|
|
117
|
-
|
|
118
|
-
:returns: Empty dict on success.
|
|
119
|
-
"""
|
|
120
|
-
path = f"{_job_path(job_name)}/{number}"
|
|
121
|
-
client.request("POST", f"{path}/stop")
|
|
122
|
-
return {}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def get_queue(client: JenkinsClient) -> list[dict[str, Any]]:
|
|
126
|
-
"""Return all items currently in the Jenkins build queue.
|
|
127
|
-
|
|
128
|
-
:returns: List of queue item dicts.
|
|
129
|
-
"""
|
|
130
|
-
data = client.get_json("/queue/api/json")
|
|
131
|
-
return data.get("items", [])
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def cancel_queue_item(
|
|
135
|
-
client: JenkinsClient,
|
|
136
|
-
queue_id: int,
|
|
137
|
-
) -> dict[str, Any]:
|
|
138
|
-
"""Cancel a queued item.
|
|
139
|
-
|
|
140
|
-
:param queue_id: Numeric queue item ID.
|
|
141
|
-
:returns: Empty dict on success.
|
|
142
|
-
"""
|
|
143
|
-
client.request("POST", "/queue/cancelItem", params={"id": queue_id})
|
|
144
|
-
return {}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def get_build_artifacts(
|
|
148
|
-
client: JenkinsClient,
|
|
149
|
-
job_name: str,
|
|
150
|
-
build_number: int | str,
|
|
151
|
-
) -> Any:
|
|
152
|
-
"""Get build artifacts.
|
|
153
|
-
|
|
154
|
-
Lists artifacts produced by a build.
|
|
155
|
-
"""
|
|
156
|
-
path = _job_path(job_name) + f"/{build_number}/api/json"
|
|
157
|
-
return client.get_json(path, tree="artifacts[fileName,relativePath]")
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def wait_for_build(
|
|
161
|
-
client: JenkinsClient,
|
|
162
|
-
job_name: str,
|
|
163
|
-
number: int,
|
|
164
|
-
timeout: int = 600,
|
|
165
|
-
poll_interval: int = 2,
|
|
166
|
-
) -> dict[str, Any]:
|
|
167
|
-
"""Poll until a build completes or *timeout* is reached.
|
|
168
|
-
|
|
169
|
-
:param timeout: Maximum seconds to wait.
|
|
170
|
-
:param poll_interval: Seconds between status checks.
|
|
171
|
-
:returns: Final build dict.
|
|
172
|
-
:raises TimeoutError: if the build does not finish within *timeout*.
|
|
173
|
-
"""
|
|
174
|
-
deadline = time.monotonic() + timeout
|
|
175
|
-
while time.monotonic() < deadline:
|
|
176
|
-
build = get_build(client, job_name, number)
|
|
177
|
-
if not build.get("building", False):
|
|
178
|
-
return build
|
|
179
|
-
time.sleep(poll_interval)
|
|
180
|
-
raise TimeoutError(
|
|
181
|
-
f"Build {job_name}#{number} did not complete within {timeout}s"
|
|
182
|
-
)
|