From 25ad8f3a716a4260fa4dbff3d7aef31f71d72bd9 Mon Sep 17 00:00:00 2001 From: tech Date: Thu, 5 Mar 2026 20:55:31 +0800 Subject: [PATCH] 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 --- .gitignore | 45 +++++++++ .python-version | 1 + CLAUDE.md | 39 ++++++++ README.md | 78 ++++++++++++++++ pyproject.toml | 27 ++++++ src/zzpyjenkins/__init__.py | 3 + src/zzpyjenkins/cli.py | 88 ++++++++++++++++++ src/zzpyjenkins/jenkins_client.py | 150 ++++++++++++++++++++++++++++++ 8 files changed, 431 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/zzpyjenkins/__init__.py create mode 100644 src/zzpyjenkins/cli.py create mode 100644 src/zzpyjenkins/jenkins_client.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a588d83 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c398999 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8294f26 --- /dev/null +++ b/README.md @@ -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 . +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..63f2c07 --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/src/zzpyjenkins/__init__.py b/src/zzpyjenkins/__init__.py new file mode 100644 index 0000000..0ad6f9e --- /dev/null +++ b/src/zzpyjenkins/__init__.py @@ -0,0 +1,3 @@ +"""zzpyjenkins - A CLI tool to interact with Jenkins server.""" + +__version__ = "0.1.0" diff --git a/src/zzpyjenkins/cli.py b/src/zzpyjenkins/cli.py new file mode 100644 index 0000000..6afb7d3 --- /dev/null +++ b/src/zzpyjenkins/cli.py @@ -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() diff --git a/src/zzpyjenkins/jenkins_client.py b/src/zzpyjenkins/jenkins_client.py new file mode 100644 index 0000000..4bcb60e --- /dev/null +++ b/src/zzpyjenkins/jenkins_client.py @@ -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"