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:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal 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
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.11
|
||||||
39
CLAUDE.md
Normal file
39
CLAUDE.md
Normal 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
78
README.md
Normal 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
27
pyproject.toml
Normal 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",
|
||||||
|
]
|
||||||
3
src/zzpyjenkins/__init__.py
Normal file
3
src/zzpyjenkins/__init__.py
Normal 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
88
src/zzpyjenkins/cli.py
Normal 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()
|
||||||
150
src/zzpyjenkins/jenkins_client.py
Normal file
150
src/zzpyjenkins/jenkins_client.py
Normal 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"
|
||||||
Reference in New Issue
Block a user