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