Files
zzpyjenkins/docs/superpowers/plans/2026-03-18-config-file-support.md
2026-03-18 13:27:44 +08:00

13 KiB

Config File Support Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace CLI credential options with TOML config file support.

Architecture: Read Jenkins server configurations from ~/.config/zzpyjenkins/config.toml. CLI uses -s/--server option to select which configured server to use.

Tech Stack: Python 3.11+ (tomllib built-in), Click, Rich


File Structure

File Action Responsibility
src/zzpyjenkins/config.py Create Load and parse TOML config file
src/zzpyjenkins/cli.py Modify Replace credential options with --server option
tests/test_config.py Create Test config loading and validation

Chunk 1: Config Module

Task 1: Config Loading

Files:

  • Create: src/zzpyjenkins/config.py

  • Create: tests/test_config.py

  • Step 0: Create tests directory

mkdir -p tests
touch tests/__init__.py
  • Step 1: Write the failing test for load_config with missing file
# tests/test_config.py
"""Tests for config module."""

import pytest
from pathlib import Path

from zzpyjenkins.config import load_config, ConfigError


def test_load_config_file_not_found():
    """Test load_config raises error when config file doesn't exist."""
    with pytest.raises(ConfigError) as exc_info:
        load_config(Path("/nonexistent/path/config.toml"))
    assert "Config file not found" in str(exc_info.value)
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_config.py -v Expected: FAIL with "ModuleNotFoundError" or "ImportError"

  • Step 3: Write minimal implementation - ConfigError and load_config skeleton
# src/zzpyjenkins/config.py
"""Configuration file handling."""

from pathlib import Path
import tomllib

from rich.console import Console

console = Console()


class ConfigError(Exception):
    """Raised when configuration is invalid or missing."""
    pass


def load_config(config_path: Path) -> dict:
    """Load and parse TOML config file.

    Args:
        config_path: Path to config.toml file

    Returns:
        Dictionary of server configurations

    Raises:
        ConfigError: If file doesn't exist or is invalid
    """
    if not config_path.exists():
        raise ConfigError(f"Config file not found: {config_path}")

    try:
        with open(config_path, "rb") as f:
            return tomllib.load(f)
    except tomllib.TOMLDecodeError as e:
        raise ConfigError(f"Invalid TOML syntax: {e}") from e
  • Step 4: Run test to verify it passes

Run: uv run pytest tests/test_config.py -v Expected: PASS

  • Step 4.5: Add test for invalid TOML syntax
# Add to tests/test_config.py
import tempfile


def test_load_config_invalid_toml():
    """Test load_config raises error for invalid TOML syntax."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("invalid toml [[[[")
        temp_path = Path(f.name)

    try:
        with pytest.raises(ConfigError) as exc_info:
            load_config(temp_path)
        assert "Invalid TOML syntax" in str(exc_info.value)
    finally:
        temp_path.unlink()
  • Step 5: Commit
git add src/zzpyjenkins/config.py tests/test_config.py
git commit -m "feat: add config module with load_config function"

Task 2: Get Server Config

Files:

  • Modify: src/zzpyjenkins/config.py

  • Modify: tests/test_config.py

  • Step 1: Write the failing test for get_server_config

# Add to tests/test_config.py
# Update import to include get_server_config:
from zzpyjenkins.config import load_config, ConfigError, get_server_config


def test_get_server_config_success():
    """Test getting a valid server config."""
    config = {
        "work": {
            "url": "https://jenkins.example.com",
            "username": "user",
            "password": "pass"
        }
    }
    result = get_server_config(config, "work")
    assert result == ("https://jenkins.example.com", "user", "pass")


def test_get_server_config_not_found():
    """Test error when server name doesn't exist."""
    config = {
        "work": {
            "url": "https://jenkins.example.com",
            "username": "user",
            "password": "pass"
        }
    }
    with pytest.raises(ConfigError) as exc_info:
        get_server_config(config, "home")
    assert "Server 'home' not found" in str(exc_info.value)
    assert "work" in str(exc_info.value)


def test_get_server_config_missing_field():
    """Test error when required field is missing."""
    config = {
        "incomplete": {
            "url": "https://jenkins.example.com",
            "username": "user"
            # missing password
        }
    }
    with pytest.raises(ConfigError) as exc_info:
        get_server_config(config, "incomplete")
    assert "password" in str(exc_info.value)
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_config.py -v Expected: FAIL with "NameError: name 'get_server_config' is not defined"

  • Step 3: Write implementation for get_server_config
# Add to src/zzpyjenkins/config.py

REQUIRED_FIELDS = ["url", "username", "password"]


def get_server_config(config: dict, server_name: str) -> tuple[str, str, str]:
    """Extract server configuration from loaded config.

    Args:
        config: Loaded config dictionary
        server_name: Name of server section to use

    Returns:
        Tuple of (url, username, password)

    Raises:
        ConfigError: If server not found or missing required fields
    """
    if server_name not in config:
        available = ", ".join(config.keys())
        raise ConfigError(
            f"Server '{server_name}' not found. Available servers: {available}"
        )

    server_config = config[server_name]

    for field in REQUIRED_FIELDS:
        if field not in server_config:
            raise ConfigError(
                f"Missing required field '{field}' in server '{server_name}'"
            )

    return (
        server_config["url"],
        server_config["username"],
        server_config["password"],
    )
  • Step 4: Run test to verify it passes

Run: uv run pytest tests/test_config.py -v Expected: PASS

  • Step 5: Commit
git add src/zzpyjenkins/config.py tests/test_config.py
git commit -m "feat: add get_server_config function with validation"

Task 3: Default Config Path

Files:

  • Modify: src/zzpyjenkins/config.py

  • Modify: tests/test_config.py

  • Step 1: Write the failing test for get_default_config_path

# Add to tests/test_config.py
# Update import to include get_default_config_path:
from zzpyjenkins.config import (
    load_config,
    ConfigError,
    get_server_config,
    get_default_config_path,
)


def test_get_default_config_path():
    """Test default config path is correct."""
    path = get_default_config_path()
    assert path.name == "config.toml"
    assert ".config" in str(path)
    assert "zzpyjenkins" in str(path)
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_config.py::test_get_default_config_path -v Expected: FAIL with "NameError: name 'get_default_config_path' is not defined"

  • Step 3: Write implementation for get_default_config_path
# Add to src/zzpyjenkins/config.py

import os


def get_default_config_path() -> Path:
    """Get the default config file path.

    Returns:
        Path to ~/.config/zzpyjenkins/config.toml
    """
    xdg_config = os.environ.get("XDG_CONFIG_HOME")
    if xdg_config:
        config_dir = Path(xdg_config)
    else:
        config_dir = Path.home() / ".config"

    return config_dir / "zzpyjenkins" / "config.toml"
  • Step 4: Run test to verify it passes

Run: uv run pytest tests/test_config.py::test_get_default_config_path -v Expected: PASS

  • Step 5: Commit
git add src/zzpyjenkins/config.py tests/test_config.py
git commit -m "feat: add get_default_config_path function"

Chunk 2: CLI Integration

Task 4: Update CLI Options

Files:

  • Modify: src/zzpyjenkins/cli.py

  • Step 1: Update imports and add server option

Replace the current imports and main function with:

# src/zzpyjenkins/cli.py
"""Command-line interface for zzpyjenkins."""

from typing import Optional

import click
from rich.console import Console

from .config import ConfigError, get_default_config_path, get_server_config, load_config
from .jenkins_client import JenkinsClient

console = Console()


@click.group()
@click.option("--server", "-s", required=True, help="Server name from config file")
@click.version_option()
@click.pass_context
def main(ctx: click.Context, server: str):
    """A CLI tool to interact with Jenkins server for building jobs."""
    ctx.ensure_object(dict)
    ctx.obj["server"] = server
  • Step 2: Update get_client function

Replace the current get_client function with:

# src/zzpyjenkins/cli.py (replace get_client function)

def get_client(ctx: click.Context) -> JenkinsClient:
    """Create Jenkins client from config file."""
    server_name = ctx.obj.get("server")

    if not server_name:
        console.print("[red]Error: Server name is required. Use -s/--server option.[/red]")
        raise SystemExit(1)

    config_path = get_default_config_path()

    try:
        config = load_config(config_path)
        url, username, password = get_server_config(config, server_name)
    except ConfigError as e:
        console.print(f"[red]Error: {e}[/red]")
        raise SystemExit(1)

    return JenkinsClient(url, username, password)
  • Step 3: Remove old environment variable references

Remove the import os line (no longer needed) and any remaining references to environment variables.

  • Step 4: Run tests to verify existing functionality still works

Run: uv run pytest tests/ -v Expected: All tests pass

  • Step 5: Commit
git add src/zzpyjenkins/cli.py
git commit -m "feat: replace credential options with --server config option"

Chunk 3: Integration Test and Finalization

Task 5: Integration Test

Files:

  • Modify: tests/test_config.py

  • Step 1: Write integration test with temp config file

# Add to tests/test_config.py
# tempfile is already imported in Task 1

def test_full_config_flow():
    """Test complete config loading flow with temp file."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
[work]
url = "https://jenkins.example.com"
username = "testuser"
password = "testpass"
""")
        temp_path = Path(f.name)

    try:
        config = load_config(temp_path)
        url, username, password = get_server_config(config, "work")

        assert url == "https://jenkins.example.com"
        assert username == "testuser"
        assert password == "testpass"
    finally:
        temp_path.unlink()
  • Step 2: Run all tests

Run: uv run pytest tests/ -v Expected: All tests pass

  • Step 3: Commit
git add tests/test_config.py
git commit -m "test: add integration test for config flow"

Task 6: Update CLAUDE.md

Files:

  • Modify: CLAUDE.md

  • Step 1: Update Configuration section

Replace the Configuration section with:

## Configuration

Credentials are read from `~/.config/zzpyjenkins/config.toml`. Create this file with your Jenkins server configurations:

```toml
[work]
url = "https://jenkins.work.com"
username = "myuser"
password = "mypassword"

[home]
url = "http://localhost:8080"
username = "admin"
password = "admin123"

Use -s/--server option to select which server to use:

zzpyjenkins -s work info
zzpyjenkins --server home list

- [ ] **Step 2: Commit**

```bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with config file usage"

Task 7: Run Lint and Format

  • Step 1: Format code

Run: uv run ruff format .

  • Step 2: Lint code

Run: uv run ruff check . --fix

  • Step 3: Run all tests

Run: uv run pytest tests/ -v Expected: All tests pass

  • Step 4: Commit any formatting changes
git add -A
git commit -m "style: format and lint code"

Summary

Task Description
1 Create config module with load_config
2 Add get_server_config with validation
3 Add get_default_config_path
4 Update CLI to use --server option
5 Integration test
6 Update documentation
7 Lint and format