Add 'running' command to list currently running Jenkins jobs

- Add new 'running' CLI command with optional --count flag
- Implement list_running_jobs method in JenkinsClient
- Apply code formatting for improved readability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 15:32:48 +08:00
parent 54838dba25
commit 0789d41674
2 changed files with 127 additions and 18 deletions

View File

@@ -13,11 +13,20 @@ console = Console()
@click.group() @click.group()
@click.option("--url", "-u", help="Jenkins server URL (default: JENKINS_URL env)") @click.option("--url", "-u", help="Jenkins server URL (default: JENKINS_URL env)")
@click.option("--username", "-U", help="Jenkins username (default: JENKINS_USERNAME env)") @click.option(
@click.option("--password", "-P", help="Jenkins password/token (default: JENKINS_PASSWORD env)") "--username", "-U", help="Jenkins username (default: JENKINS_USERNAME env)"
)
@click.option(
"--password", "-P", help="Jenkins password/token (default: JENKINS_PASSWORD env)"
)
@click.version_option() @click.version_option()
@click.pass_context @click.pass_context
def main(ctx: click.Context, url: Optional[str], username: Optional[str], password: Optional[str]): def main(
ctx: click.Context,
url: Optional[str],
username: Optional[str],
password: Optional[str],
):
"""A CLI tool to interact with Jenkins server for building jobs.""" """A CLI tool to interact with Jenkins server for building jobs."""
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj["url"] = url ctx.obj["url"] = url
@@ -29,11 +38,17 @@ def get_client(ctx: click.Context) -> JenkinsClient:
"""Create Jenkins client from options or environment variables.""" """Create Jenkins client from options or environment variables."""
url = ctx.obj.get("url") or os.environ.get("JENKINS_URL") url = ctx.obj.get("url") or os.environ.get("JENKINS_URL")
username = ctx.obj.get("username") or os.environ.get("JENKINS_USERNAME") username = ctx.obj.get("username") or os.environ.get("JENKINS_USERNAME")
password = ctx.obj.get("password") or os.environ.get("JENKINS_PASSWORD") or os.environ.get("JENKINS_TOKEN") password = (
ctx.obj.get("password")
or os.environ.get("JENKINS_PASSWORD")
or os.environ.get("JENKINS_TOKEN")
)
if not all([url, username, password]): if not all([url, username, password]):
console.print("[red]Error: Missing Jenkins configuration.[/red]") console.print("[red]Error: Missing Jenkins configuration.[/red]")
console.print("Use --url/--username/--password options or set environment variables:") console.print(
"Use --url/--username/--password options or set environment variables:"
)
console.print(" JENKINS_URL, JENKINS_USERNAME, JENKINS_PASSWORD") console.print(" JENKINS_URL, JENKINS_USERNAME, JENKINS_PASSWORD")
raise SystemExit(1) raise SystemExit(1)
@@ -59,7 +74,9 @@ def list_jobs(ctx: click.Context, pattern: Optional[str]):
@main.command() @main.command()
@click.argument("job_name") @click.argument("job_name")
@click.option("--params", "-p", multiple=True, help="Build parameters in KEY=VALUE format") @click.option(
"--params", "-p", multiple=True, help="Build parameters in KEY=VALUE format"
)
@click.pass_context @click.pass_context
def build(ctx: click.Context, job_name: str, params: tuple[str, ...]): def build(ctx: click.Context, job_name: str, params: tuple[str, ...]):
"""Trigger a build for a job.""" """Trigger a build for a job."""
@@ -94,5 +111,14 @@ def watch(ctx: click.Context, job_name: str):
client.watch_build(job_name) client.watch_build(job_name)
@main.command()
@click.option("--count", "-c", is_flag=True, help="Only show count of running jobs")
@click.pass_context
def running(ctx: click.Context, count: bool):
"""List all currently running jobs."""
client = get_client(ctx)
client.list_running_jobs(count_only=count)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,7 +1,7 @@
"""Jenkins client wrapper with rich output support.""" """Jenkins client wrapper with rich output support."""
import time import time
from typing import Any, Optional from typing import Optional
import jenkins import jenkins
from rich.console import Console from rich.console import Console
@@ -25,10 +25,12 @@ class JenkinsClient:
version = self.server.get_version() version = self.server.get_version()
jobs = self.server.get_all_jobs() jobs = self.server.get_all_jobs()
console.print(f"\n[bold]Jenkins Server Information[/bold]\n") console.print("\n[bold]Jenkins Server Information[/bold]\n")
console.print(f" URL: {self.url}") console.print(f" URL: {self.url}")
console.print(f" Version: {version}") console.print(f" Version: {version}")
console.print(f" User: {user.get('fullName', user.get('id', 'Unknown'))}") console.print(
f" User: {user.get('fullName', user.get('id', 'Unknown'))}"
)
console.print(f" Jobs: {len(jobs)}") console.print(f" Jobs: {len(jobs)}")
except jenkins.JenkinsException as e: except jenkins.JenkinsException as e:
console.print(f"[red]Error connecting to Jenkins: {e}[/red]") console.print(f"[red]Error connecting to Jenkins: {e}[/red]")
@@ -63,7 +65,9 @@ class JenkinsClient:
if parameters: if parameters:
queue_id = self.server.build_job(job_name, parameters=parameters) queue_id = self.server.build_job(job_name, parameters=parameters)
console.print(f"Build #{next_build} queued with parameters: {parameters}") console.print(
f"Build #{next_build} queued with parameters: {parameters}"
)
else: else:
queue_id = self.server.build_job(job_name) queue_id = self.server.build_job(job_name)
console.print(f"Build #{next_build} queued") console.print(f"Build #{next_build} queued")
@@ -90,15 +94,23 @@ class JenkinsClient:
table.add_column("Property", style="cyan") table.add_column("Property", style="cyan")
table.add_column("Value") table.add_column("Value")
table.add_row("Status", f"[{status_color}]{info['result'] or 'RUNNING'}[/{status_color}]") table.add_row(
"Status",
f"[{status_color}]{info['result'] or 'RUNNING'}[/{status_color}]",
)
table.add_row("Building", str(info["building"])) table.add_row("Building", str(info["building"]))
table.add_row("Duration", f"{info['duration'] // 1000}s") table.add_row("Duration", f"{info['duration'] // 1000}s")
table.add_row("Timestamp", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(info["timestamp"] / 1000))) table.add_row(
"Timestamp",
time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(info["timestamp"] / 1000)
),
)
console.print(table) console.print(table)
except jenkins.NotFoundException: except jenkins.NotFoundException:
console.print(f"[red]Job or build not found[/red]") console.print("[red]Job or build not found[/red]")
except jenkins.JenkinsException as e: except jenkins.JenkinsException as e:
console.print(f"[red]Error getting build status: {e}[/red]") console.print(f"[red]Error getting build status: {e}[/red]")
@@ -113,28 +125,99 @@ class JenkinsClient:
return return
build_number = last_build["number"] build_number = last_build["number"]
console.print(f"Watching build #{build_number} of [cyan]{job_name}[/cyan]...\n") console.print(
f"Watching build #{build_number} of [cyan]{job_name}[/cyan]...\n"
)
while True: while True:
info = self.server.get_build_info(job_name, build_number) info = self.server.get_build_info(job_name, build_number)
status = "RUNNING" if info["building"] else info["result"] status = "RUNNING" if info["building"] else info["result"]
status_color = self._get_status_color(info["result"]) if not info["building"] else "yellow" status_color = (
self._get_status_color(info["result"])
if not info["building"]
else "yellow"
)
console.print(f" [{status_color}]{status}[/{status_color}] - Duration: {info['duration'] // 1000}s", end="\r") console.print(
f" [{status_color}]{status}[/{status_color}] - Duration: {info['duration'] // 1000}s",
end="\r",
)
if not info["building"]: if not info["building"]:
console.print() # New line console.print() # New line
console.print(f"\nBuild completed with status: [{status_color}]{info['result']}[/{status_color}]") console.print(
f"\nBuild completed with status: [{status_color}]{info['result']}[/{status_color}]"
)
break break
time.sleep(interval) time.sleep(interval)
except jenkins.NotFoundException: except jenkins.NotFoundException:
console.print(f"[red]Job not found[/red]") console.print("[red]Job not found[/red]")
except jenkins.JenkinsException as e: except jenkins.JenkinsException as e:
console.print(f"[red]Error watching build: {e}[/red]") console.print(f"[red]Error watching build: {e}[/red]")
def list_running_jobs(self, count_only: bool = False):
"""List all currently running jobs."""
try:
jobs = self.server.get_all_jobs()
running_count = 0
for job in jobs:
color = job.get("color", "")
# Check if job is running (color contains "anime")
if "anime" in color:
running_count += 1
if count_only:
print(running_count)
return running_count
if running_count == 0:
console.print("[green]No jobs currently running[/green]")
return 0
running_jobs = []
for job in jobs:
color = job.get("color", "")
if "anime" in color:
job_info = self.server.get_job_info(job["name"])
last_build = job_info.get("lastBuild")
if last_build:
build_info = self.server.get_build_info(
job["name"], last_build["number"]
)
running_jobs.append(
{
"name": job["name"],
"build_number": last_build["number"],
"duration": build_info.get("duration", 0) // 1000,
"url": job["url"],
}
)
table = Table(title=f"Running Jobs ({len(running_jobs)})")
table.add_column("Name", style="cyan")
table.add_column("Build", style="yellow")
table.add_column("Duration", style="green")
table.add_column("URL")
for job in running_jobs:
table.add_row(
job["name"],
f"#{job['build_number']}",
f"{job['duration']}s",
job["url"],
)
console.print(table)
return len(running_jobs)
except jenkins.JenkinsException as e:
console.print(f"[red]Error listing running jobs: {e}[/red]")
return -1
def _get_status_color(self, result: Optional[str]) -> str: def _get_status_color(self, result: Optional[str]) -> str:
"""Get color for build status.""" """Get color for build status."""
if result is None: if result is None: