docs: Add config file support implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
494
docs/superpowers/plans/2026-03-18-config-file-support.md
Normal file
494
docs/superpowers/plans/2026-03-18-config-file-support.md
Normal file
@@ -0,0 +1,494 @@
|
||||
# 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 1: Write the failing test for load_config with missing file**
|
||||
|
||||
```python
|
||||
# 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**
|
||||
|
||||
```python
|
||||
# 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 5: Commit**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```python
|
||||
# Add to tests/test_config.py
|
||||
|
||||
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**
|
||||
|
||||
```python
|
||||
# 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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```python
|
||||
# Add to tests/test_config.py
|
||||
|
||||
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**
|
||||
|
||||
```python
|
||||
# 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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```python
|
||||
# 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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```python
|
||||
# Add to tests/test_config.py
|
||||
|
||||
import tempfile
|
||||
|
||||
|
||||
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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```markdown
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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 |
|
||||
Reference in New Issue
Block a user