diff --git a/plugins/titan-plugin-git/titan_plugin_git/clients/git_client.py b/plugins/titan-plugin-git/titan_plugin_git/clients/git_client.py index 2d2b4c7e..3e4dfedd 100644 --- a/plugins/titan-plugin-git/titan_plugin_git/clients/git_client.py +++ b/plugins/titan-plugin-git/titan_plugin_git/clients/git_client.py @@ -51,7 +51,8 @@ def __init__( self, repo_path: str = ".", main_branch: str = "main", - default_remote: str = "origin" + default_remote: str = "origin", + rc_branch: str | None = None, ): """ Initialize Git client. @@ -64,6 +65,7 @@ def __init__( self.repo_path = repo_path self.main_branch = main_branch self.default_remote = default_remote + self.rc_branch = rc_branch # Initialize network layer self.network = GitNetwork(repo_path=repo_path) diff --git a/plugins/titan-plugin-git/titan_plugin_git/plugin.py b/plugins/titan-plugin-git/titan_plugin_git/plugin.py index 31ada652..243707d9 100644 --- a/plugins/titan-plugin-git/titan_plugin_git/plugin.py +++ b/plugins/titan-plugin-git/titan_plugin_git/plugin.py @@ -49,7 +49,8 @@ def initialize(self, config: TitanConfig, secrets: SecretManager) -> None: # Initialize client with validated configuration self._client = GitClient( main_branch=validated_config.main_branch, - default_remote=validated_config.default_remote + default_remote=validated_config.default_remote, + rc_branch=validated_config.rc_branch, ) def _get_plugin_config(self, config: TitanConfig) -> dict: diff --git a/plugins/titan-plugin-github/tests/unit/test_pull_request_steps.py b/plugins/titan-plugin-github/tests/unit/test_pull_request_steps.py index 257b879e..c7eca057 100644 --- a/plugins/titan-plugin-github/tests/unit/test_pull_request_steps.py +++ b/plugins/titan-plugin-github/tests/unit/test_pull_request_steps.py @@ -8,6 +8,7 @@ merge_pull_request_step, verify_pull_request_state_step, ) +from titan_plugin_github.steps.create_pr_step import create_pr_step class MockTextual: @@ -18,6 +19,7 @@ def __init__(self): self.success_text = Mock() self.warning_text = Mock() self.dim_text = Mock() + self.text = Mock() def loading(self, _message): class _Loader: @@ -147,3 +149,26 @@ def test_verify_pull_request_state_step_requires_expected_state(): assert isinstance(result, Error) assert result.message == "No expected PR state in context" ctx.textual.end_step.assert_called_once_with("error") + + +def test_create_pr_step_uses_context_base_branch(): + github = Mock() + github.config.auto_assign_prs = False + github.create_pull_request.return_value = ClientSuccess( + data=Mock(number=4105, url="https://github.example/pr/4105"), + message="ok", + ) + ctx = make_context( + github, + pr_title="notes: Add release notes for 26.18", + pr_body="Release notes", + pr_head_branch="notes/release-notes", + pr_base_branch="rc/26.18", + ) + ctx.git = Mock(main_branch="develop") + + result = create_pr_step(ctx) + + assert isinstance(result, Success) + github.create_pull_request.assert_called_once() + assert github.create_pull_request.call_args.kwargs["base"] == "rc/26.18" diff --git a/plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py b/plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py index 3c2e7651..c4479b67 100644 --- a/plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py +++ b/plugins/titan-plugin-github/titan_plugin_github/steps/create_pr_step.py @@ -52,7 +52,7 @@ def create_pr_step(ctx: WorkflowContext) -> WorkflowResult: # 2. Get required data from context and client config title = ctx.get("pr_title") body = ctx.get("pr_body") - base = ctx.git.main_branch # Get base branch from git client config + base = ctx.get("pr_base_branch") or ctx.git.main_branch head = ctx.get("pr_head_branch") is_draft = ctx.get("pr_is_draft", False) # Default to not a draft diff --git a/titan_cli/core/plugins/models.py b/titan_cli/core/plugins/models.py index 0f14981d..f5c51046 100644 --- a/titan_cli/core/plugins/models.py +++ b/titan_cli/core/plugins/models.py @@ -41,6 +41,7 @@ class GitPluginConfig(BaseModel): """Configuration for Git plugin.""" main_branch: str = Field("main", description="Main/default branch name") default_remote: str = Field("origin", description="Default remote name") + rc_branch: str | None = Field(None, description="Prefix for versioned RC branches (for example, 'rc')") class GitHubPluginConfig(BaseModel): """Configuration for GitHub plugin.""" @@ -121,3 +122,33 @@ def validate_email(cls, v): if '@' not in v: raise ValueError("email must be a valid email address") return v.lower() # Normalize email to lowercase + + +class TekelPluginConfig(BaseModel): + """Configuration for Tekel plugin.""" + base_url: Optional[str] = Field( + None, + description="Tekel API base URL (e.g. 'https://tekel-api.example.com')", + json_schema_extra={"config_scope": "global"}, + ) + api_token: Optional[str] = Field( + None, + description="Tekel API token", + json_schema_extra={"format": "password", "required_in_schema": True}, + ) + timeout: int = Field( + 30, + description="Request timeout in seconds", + json_schema_extra={"config_scope": "global"}, + ) + + @field_validator('base_url') + @classmethod + def validate_base_url(cls, v): + if not v: + raise ValueError( + "Tekel base_url not configured. Add [plugins.tekel.config] with base_url in ~/.titan/config.toml" + ) + if not v.startswith(('http://', 'https://')): + raise ValueError("base_url must start with http:// or https://") + return v.rstrip('/')