Initial commit: zzpyjenkins CLI tool for Jenkins interaction

A CLI tool built with Click and Rich to interact with Jenkins servers.
Provides commands for viewing server info, listing jobs, triggering builds,
checking build status, and watching build progress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 20:55:31 +08:00
commit 25ad8f3a71
8 changed files with 431 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.venv/
venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Testing
.pytest_cache/
.coverage
htmlcov/
# uv
.uv/
uv.lock
# Environment
.env
.env.local

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

39
CLAUDE.md Normal file
View File

@@ -0,0 +1,39 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
zzpyjenkins is a CLI tool to interact with Jenkins servers for building jobs. It wraps the `python-jenkins` library with a user-friendly CLI using Click and rich terminal output.
## Development Commands
```bash
# Install dependencies
uv sync
# Run the CLI
uv run zzpyjenkins --help
# Format code
uv run ruff format .
# Lint
uv run ruff check .
# Run tests
uv run pytest
```
## Architecture
- **`src/zzpyjenkins/cli.py`**: Click-based CLI entry point. Defines commands (`info`, `list`, `build`, `status`, `watch`) and handles parameter parsing. Uses `get_client()` to create `JenkinsClient` from environment variables.
- **`src/zzpyjenkins/jenkins_client.py`**: `JenkinsClient` class that wraps `python-jenkins` library. Handles all Jenkins API interactions and formats output using Rich (tables, colored status).
## Configuration
The tool requires these environment variables:
- `JENKINS_URL`: Jenkins server URL
- `JENKINS_USERNAME`: Jenkins username
- `JENKINS_PASSWORD` or `JENKINS_TOKEN`: API token or password

78
README.md Normal file
View File

@@ -0,0 +1,78 @@
# zzpyjenkins
A CLI tool to interact with Jenkins server for building jobs.
## Installation
### Using uvx (recommended)
```bash
uvx install zzpyjenkins
```
Or run directly:
```bash
uvx --from zzpyjenkins zzpyjenkins --help
```
### From source
```bash
uv sync
uv run zzpyjenkins --help
```
## Configuration
Set the following environment variables:
```bash
export JENKINS_URL="https://your-jenkins-server.com"
export JENKINS_USERNAME="your_username"
export JENKINS_PASSWORD="your_api_token" # or JENKINS_TOKEN
```
## Usage
```bash
# Show Jenkins server info
zzpyjenkins info
# List all jobs
zzpyjenkins list
# List jobs with filter
zzpyjenkins list -p "my-project"
# Trigger a build
zzpyjenkins build my-job-name
# Trigger a build with parameters
zzpyjenkins build my-job-name -p BRANCH=main -p ENV=staging
# Get build status
zzpyjenkins status my-job-name
# Get specific build status
zzpyjenkins status my-job-name 123
# Watch build progress
zzpyjenkins watch my-job-name
```
## Development
```bash
# Install dev dependencies
uv sync
# Run tests
uv run pytest
# Format code
uv run ruff format .
# Lint
uv run ruff check .
```

27
pyproject.toml Normal file
View File

@@ -0,0 +1,27 @@
[project]
name = "zzpyjenkins"
version = "0.1.0"
description = "A CLI tool to interact with Jenkins server for building jobs"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"python-jenkins>=1.8.0",
"click>=8.1.0",
"rich>=13.0.0",
]
[project.scripts]
zzpyjenkins = "zzpyjenkins.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/zzpyjenkins"]
[tool.uv]
dev-dependencies = [
"pytest>=8.0.0",
"ruff>=0.4.0",
]

View File

@@ -0,0 +1,3 @@
"""zzpyjenkins - A CLI tool to interact with Jenkins server."""
__version__ = "0.1.0"

88
src/zzpyjenkins/cli.py Normal file
View File

@@ -0,0 +1,88 @@
"""Command-line interface for zzpyjenkins."""
import os
from pathlib import Path
from typing import Optional
import click
import jenkins
from rich.console import Console
from rich.table import Table
from .jenkins_client import JenkinsClient
console = Console()
def get_client() -> JenkinsClient:
"""Create Jenkins client from environment variables or config."""
url = os.environ.get("JENKINS_URL")
username = os.environ.get("JENKINS_USERNAME")
password = os.environ.get("JENKINS_PASSWORD") or os.environ.get("JENKINS_TOKEN")
if not all([url, username, password]):
console.print("[red]Error: Missing Jenkins configuration.[/red]")
console.print("Set environment variables: JENKINS_URL, JENKINS_USERNAME, JENKINS_PASSWORD")
raise SystemExit(1)
return JenkinsClient(url, username, password)
@click.group()
@click.version_option()
def main():
"""A CLI tool to interact with Jenkins server for building jobs."""
pass
@main.command()
def info():
"""Show Jenkins server information."""
client = get_client()
client.print_server_info()
@main.command("list")
@click.option("--pattern", "-p", default=None, help="Filter jobs by name pattern")
def list_jobs(pattern: Optional[str]):
"""List all jobs on Jenkins server."""
client = get_client()
client.list_jobs(pattern)
@main.command()
@click.argument("job_name")
@click.option("--params", "-p", multiple=True, help="Build parameters in KEY=VALUE format")
def build(job_name: str, params: tuple[str, ...]):
"""Trigger a build for a job."""
client = get_client()
# Parse parameters
parameters = {}
for param in params:
if "=" in param:
key, value = param.split("=", 1)
parameters[key] = value
client.build_job(job_name, parameters if parameters else None)
@main.command()
@click.argument("job_name")
@click.argument("build_number", type=int, required=False)
def status(job_name: str, build_number: Optional[int]):
"""Get build status for a job."""
client = get_client()
client.get_build_status(job_name, build_number)
@main.command()
@click.argument("job_name")
def watch(job_name: str):
"""Watch the latest build of a job in real-time."""
client = get_client()
client.watch_build(job_name)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,150 @@
"""Jenkins client wrapper with rich output support."""
import time
from typing import Any, Optional
import jenkins
from rich.console import Console
from rich.table import Table
console = Console()
class JenkinsClient:
"""Wrapper for Jenkins operations with rich output."""
def __init__(self, url: str, username: str, password: str):
"""Initialize Jenkins client."""
self.url = url
self.server = jenkins.Jenkins(url, username=username, password=password)
def print_server_info(self):
"""Print Jenkins server information."""
try:
user = self.server.get_whoami()
version = self.server.get_version()
jobs = self.server.get_all_jobs()
console.print(f"\n[bold]Jenkins Server Information[/bold]\n")
console.print(f" URL: {self.url}")
console.print(f" Version: {version}")
console.print(f" User: {user.get('fullName', user.get('id', 'Unknown'))}")
console.print(f" Jobs: {len(jobs)}")
except jenkins.JenkinsException as e:
console.print(f"[red]Error connecting to Jenkins: {e}[/red]")
def list_jobs(self, pattern: Optional[str] = None):
"""List all jobs, optionally filtered by pattern."""
try:
jobs = self.server.get_all_jobs()
if pattern:
jobs = [j for j in jobs if pattern.lower() in j["name"].lower()]
table = Table(title="Jenkins Jobs")
table.add_column("Name", style="cyan")
table.add_column("Color", style="green")
table.add_column("URL")
for job in jobs:
table.add_row(job["name"], job.get("color", "unknown"), job["url"])
console.print(table)
console.print(f"\nTotal: {len(jobs)} jobs")
except jenkins.JenkinsException as e:
console.print(f"[red]Error listing jobs: {e}[/red]")
def build_job(self, job_name: str, parameters: Optional[dict] = None):
"""Trigger a build for a job."""
try:
next_build = self.server.get_job_info(job_name)["nextBuildNumber"]
console.print(f"Triggering build for [cyan]{job_name}[/cyan]...")
if parameters:
queue_id = self.server.build_job(job_name, parameters=parameters)
console.print(f"Build #{next_build} queued with parameters: {parameters}")
else:
queue_id = self.server.build_job(job_name)
console.print(f"Build #{next_build} queued")
console.print(f"Queue ID: {queue_id}")
return queue_id
except jenkins.NotFoundException:
console.print(f"[red]Job '{job_name}' not found[/red]")
except jenkins.JenkinsException as e:
console.print(f"[red]Error triggering build: {e}[/red]")
def get_build_status(self, job_name: str, build_number: Optional[int] = None):
"""Get build status for a job."""
try:
if build_number is None:
build_number = self.server.get_job_info(job_name)["lastBuild"]["number"]
info = self.server.get_build_info(job_name, build_number)
status_color = self._get_status_color(info["result"])
table = Table(title=f"Build #{build_number} - {job_name}")
table.add_column("Property", style="cyan")
table.add_column("Value")
table.add_row("Status", f"[{status_color}]{info['result'] or 'RUNNING'}[/{status_color}]")
table.add_row("Building", str(info["building"]))
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)))
console.print(table)
except jenkins.NotFoundException:
console.print(f"[red]Job or build not found[/red]")
except jenkins.JenkinsException as e:
console.print(f"[red]Error getting build status: {e}[/red]")
def watch_build(self, job_name: str, interval: int = 5):
"""Watch the latest build of a job."""
try:
job_info = self.server.get_job_info(job_name)
last_build = job_info.get("lastBuild")
if not last_build:
console.print("[yellow]No builds found for this job[/yellow]")
return
build_number = last_build["number"]
console.print(f"Watching build #{build_number} of [cyan]{job_name}[/cyan]...\n")
while True:
info = self.server.get_build_info(job_name, build_number)
status = "RUNNING" if info["building"] else info["result"]
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")
if not info["building"]:
console.print() # New line
console.print(f"\nBuild completed with status: [{status_color}]{info['result']}[/{status_color}]")
break
time.sleep(interval)
except jenkins.NotFoundException:
console.print(f"[red]Job not found[/red]")
except jenkins.JenkinsException as e:
console.print(f"[red]Error watching build: {e}[/red]")
def _get_status_color(self, result: Optional[str]) -> str:
"""Get color for build status."""
if result is None:
return "yellow"
if result == "SUCCESS":
return "green"
if result == "FAILURE":
return "red"
if result == "UNSTABLE":
return "yellow"
if result == "ABORTED":
return "grey50"
return "white"