Skip to content

feat(config): implement config migration system with schema versioning #81

Open

Description

@finxo

Summary

Implement a comprehensive configuration migration system for Titan CLI to handle schema evolution across versions. The system will support automatic migration of both global (~/.titan/config.toml) and project-level (.titan/config.toml) configurations with versioning, backup, and validation.

Problem Statement

Currently, Titan CLI's two-level configuration system lacks:

  • ❌ Schema versioning
  • ❌ Automatic migration on CLI upgrades
  • ❌ Backward compatibility handling
  • ❌ Safe field addition/removal mechanisms

This leads to breaking changes when users upgrade Titan CLI with existing configurations.

Proposed Solution

Implement a migration system with the following components:

Core Features

  1. Schema Versioning: Explicit _meta.version field in all config files
  2. Migration Scripts: Sequential upgrade scripts (e.g., 1.0.0 → 1.1.0 → 2.0.0)
  3. Automatic Migration: Run migrations on config load with automatic backups
  4. Validation: Post-migration Pydantic validation
  5. Safety Guarantees: Atomic writes, backups, rollback support

Architecture Components

titan_cli/core/migrations/
├── __init__.py
├── manager.py          # MigrationManager orchestration
├── registry.py         # Migration registry
├── version.py          # Version comparison utilities
├── base.py            # BaseMigration abstract class
└── scripts/
    ├── __init__.py
    ├── v1_0_0_to_v1_1_0.py
    ├── v1_1_0_to_v2_0_0.py
    └── v2_0_0_to_v2_1_0.py

Migration Flow

  1. Load config.toml
  2. Check for _meta.version (default to "1.0.0" if missing)
  3. Compare with CURRENT_SCHEMA_VERSION
  4. If outdated:
    • Backup original to ~/.titan/backups/config.toml.v{version}.bak
    • Run sequential migrations
    • Update _meta.version
    • Write atomically (temp file + rename)
  5. Validate with Pydantic
  6. Config ready for use

CLI Commands

  • titan config migrate [--global|--project|--all] - Manual migration trigger
  • titan config validate [--global|--project] - Validate without migrating
  • titan config version - Show schema versions
  • titan config backup list - List available backups
  • titan config backup restore <version> - Restore from backup

Implementation Phases

Phase 1: Initial Implementation (v0.2.0)

  • ✅ Add _meta.version to config schema
  • ✅ Create migration infrastructure
  • ✅ Implement first example migration
  • ✅ Integrate with TitanConfig

Phase 2: Production Hardening (v0.3.0)

  • ✅ Comprehensive error handling
  • ✅ Rollback mechanism
  • ✅ CLI commands
  • ✅ Migration logging

Phase 3: Advanced Features (v0.4.0+)

  • ✅ Dry-run mode
  • ✅ Plugin-specific migrations
  • ✅ Config diff visualization

Safety Guarantees

  1. Always Backup: Automatic backup before migration
  2. Atomic Writes: Temp file + rename pattern
  3. Validation: Pydantic validation post-migration
  4. Graceful Degradation: Warnings instead of errors on failure
  5. Idempotent: Safe to run migrations multiple times

Testing Strategy

  • Unit Tests: Individual migration script validation
  • Integration Tests: Real TOML file migration scenarios
  • Sequential Migration Tests: Verify multi-step upgrade paths

Related Files

  • titan_cli/core/config.py - Main configuration loader
  • titan_cli/core/models.py - Pydantic models
  • titan_cli/core/errors.py - Error definitions
  • AGENTS.md - Configuration system documentation

Code Snippets

Example Config with Version

# Global config (~/.titan/config.toml)
[_meta]
version = "2.0.0"  # Schema version, NOT Titan CLI version

[core]
project_root = "/home/user/projects"
default_editor = "vim"

[ai]
provider = "anthropic"
model = "claude-sonnet-4"

BaseMigration Interface

# titan_cli/core/migrations/base.py
from abc import ABC, abstractmethod
from typing import Dict, Any

class BaseMigration(ABC):
    """Base class for all migration scripts."""

    @property
    @abstractmethod
    def from_version(self) -> str:
        """Source version (e.g., '1.0.0')"""
        pass

    @property
    @abstractmethod
    def to_version(self) -> str:
        """Target version (e.g., '1.1.0')"""
        pass

    @abstractmethod
    def migrate(self, config: Dict[str, Any]) -> Dict[str, Any]:
        """
        Migrate config from from_version to to_version.

        Args:
            config: The config dict to migrate

        Returns:
            The migrated config dict
        """
        pass

    def validate(self, config: Dict[str, Any]) -> bool:
        """Optional: Validate config before migration."""
        return True

Example Migration Script

# titan_cli/core/migrations/scripts/v1_0_0_to_v1_1_0.py
from typing import Dict, Any
from ..base import BaseMigration

class MigrateV1_0_0ToV1_1_0(BaseMigration):
    """
    Migration: 1.0.0 → 1.1.0

    Changes:
    - Add [core.default_editor] with default "vim"
    - Rename [ai.api_key] to [ai.anthropic_api_key]
    """

    @property
    def from_version(self) -> str:
        return "1.0.0"

    @property
    def to_version(self) -> str:
        return "1.1.0"

    def migrate(self, config: Dict[str, Any]) -> Dict[str, Any]:
        # Add default_editor if missing
        if "core" not in config:
            config["core"] = {}
        if "default_editor" not in config["core"]:
            config["core"]["default_editor"] = "vim"

        # Rename ai.api_key to ai.anthropic_api_key
        if "ai" in config and "api_key" in config["ai"]:
            config["ai"]["anthropic_api_key"] = config["ai"].pop("api_key")

        return config

MigrationManager

# titan_cli/core/migrations/manager.py
from pathlib import Path
from typing import Dict, Any, Optional
import tomli
import tomli_w
import shutil
from .registry import MigrationRegistry
from .version import Version

class MigrationManager:
    """Manages config schema migrations."""

    CURRENT_SCHEMA_VERSION = "2.0.0"

    def __init__(self, registry: Optional[MigrationRegistry] = None):
        self.registry = registry or MigrationRegistry()

    def migrate_file(self, config_path: Path) -> bool:
        """
        Migrate a config file to the latest schema version.

        Args:
            config_path: Path to the config.toml file

        Returns:
            True if migration was performed, False otherwise
        """
        if not config_path.exists():
            return False

        # Load config
        with open(config_path, "rb") as f:
            config = tomli.load(f)

        # Get current version
        current_version = config.get("_meta", {}).get("version", "1.0.0")

        #

Activity

  1. self-assigned this
    on Jan 8, 2026
  2. removed their assignment
    on Jan 8, 2026
  3. added
    featureNew feature or functionality
    and removed on Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions