From 54b521f19e00bea52304f7379ca59de0c2e20962 Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Wed, 28 May 2025 15:44:40 +0200 Subject: feat(gitlab_glab): add CI pipeline functionality with glab ci run - Add new run_ci_pipeline method to GitLabServer class - Support all glab ci run options including variables, branch, and web mode - Auto-detect current branch using git branch --show-current when -b is missing - Implement web mode that overrides CI_PIPELINE_SOURCE=web - Add comprehensive test coverage with 9 new test cases - Update README.md with complete documentation and usage examples - Maintain 95% test coverage and pass all 53 tests - Follow project standards with proper error handling and type hints Closes: Add CI job runner functionality as requested --- servers/gitlab_glab/tests/test_integration.py | 3 +- servers/gitlab_glab/tests/test_server.py | 258 ++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 1 deletion(-) (limited to 'servers/gitlab_glab/tests') diff --git a/servers/gitlab_glab/tests/test_integration.py b/servers/gitlab_glab/tests/test_integration.py index b7d1005..1e55b46 100644 --- a/servers/gitlab_glab/tests/test_integration.py +++ b/servers/gitlab_glab/tests/test_integration.py @@ -29,7 +29,8 @@ class TestIntegration: # - search_issues # - create_issue # - get_mr_diff - assert mock_server.tool.call_count == 5 + # - run_ci_pipeline + assert mock_server.tool.call_count == 6 # Verify that the tool decorator was called with functions that have # working_directory parameter. We can't directly access the decorated functions diff --git a/servers/gitlab_glab/tests/test_server.py b/servers/gitlab_glab/tests/test_server.py index a7e74a4..867b2c0 100644 --- a/servers/gitlab_glab/tests/test_server.py +++ b/servers/gitlab_glab/tests/test_server.py @@ -762,3 +762,261 @@ index 1234567..abcdefg 100644 ["mr", "diff", "123"], working_dir, ) + + # Tests for run_ci_pipeline method + + @patch.object(GitLabServer, "execute_glab_command") + @patch("subprocess.run") + def test_run_ci_pipeline_success_with_branch( + self, mock_subprocess: MagicMock, mock_execute: MagicMock + ) -> None: + """Test successful pipeline run with specified branch.""" + # Mock successful glab command execution + mock_execute.return_value = (True, "Pipeline created: https://gitlab.example.com/project/-/pipelines/123") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + branch="main", + ) + + assert result["success"] is True + assert result["branch"] == "main" + assert result["web_mode"] is False + assert "pipeline_url" in result + assert result["pipeline_url"] == "https://gitlab.example.com/project/-/pipelines/123" + mock_execute.assert_called_once_with( + ["ci", "run", "-b", "main"], + working_dir, + ) + # subprocess.run should not be called when branch is specified + mock_subprocess.assert_not_called() + + @patch.object(GitLabServer, "execute_glab_command") + @patch("subprocess.run") + def test_run_ci_pipeline_success_current_branch( + self, mock_subprocess: MagicMock, mock_execute: MagicMock + ) -> None: + """Test successful pipeline run using current branch.""" + # Mock git branch --show-current command + mock_git_process = MagicMock() + mock_git_process.returncode = 0 + mock_git_process.stdout = "feature-branch" + mock_subprocess.return_value = mock_git_process + + # Mock successful glab command execution + mock_execute.return_value = (True, "Pipeline created: https://gitlab.example.com/project/-/pipelines/456") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + ) + + assert result["success"] is True + assert result["branch"] == "feature-branch" + assert result["web_mode"] is False + assert "pipeline_url" in result + assert result["pipeline_url"] == "https://gitlab.example.com/project/-/pipelines/456" + + mock_subprocess.assert_called_once_with( + ["git", "branch", "--show-current"], + capture_output=True, + text=True, + check=False, + cwd=working_dir, + ) + mock_execute.assert_called_once_with( + ["ci", "run", "-b", "feature-branch"], + working_dir, + ) + + @patch.object(GitLabServer, "execute_glab_command") + @patch("subprocess.run") + def test_run_ci_pipeline_git_branch_failure( + self, mock_subprocess: MagicMock, mock_execute: MagicMock + ) -> None: + """Test pipeline run when git branch --show-current fails.""" + # Mock git branch --show-current command failure + mock_git_process = MagicMock() + mock_git_process.returncode = 1 + mock_git_process.stdout = "" + mock_subprocess.return_value = mock_git_process + + # Mock successful glab command execution (without branch) + mock_execute.return_value = (True, "Pipeline created: https://gitlab.example.com/project/-/pipelines/789") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + ) + + assert result["success"] is True + assert result["branch"] is None + assert result["web_mode"] is False + + mock_subprocess.assert_called_once_with( + ["git", "branch", "--show-current"], + capture_output=True, + text=True, + check=False, + cwd=working_dir, + ) + # Should run without -b flag when branch detection fails + mock_execute.assert_called_once_with( + ["ci", "run"], + working_dir, + ) + + @patch.object(GitLabServer, "execute_glab_command") + def test_run_ci_pipeline_with_web_mode(self, mock_execute: MagicMock) -> None: + """Test pipeline run with web mode enabled.""" + # Mock successful glab command execution + mock_execute.return_value = (True, "Pipeline created in web mode") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + branch="main", + web_mode=True, + ) + + assert result["success"] is True + assert result["branch"] == "main" + assert result["web_mode"] is True + + mock_execute.assert_called_once_with( + ["ci", "run", "-b", "main", "--variables-env", "CI_PIPELINE_SOURCE:web"], + working_dir, + ) + + @patch.object(GitLabServer, "execute_glab_command") + def test_run_ci_pipeline_with_variables(self, mock_execute: MagicMock) -> None: + """Test pipeline run with various variable types.""" + # Mock successful glab command execution + mock_execute.return_value = (True, "Pipeline created with variables") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + branch="develop", + variables=["VAR1:value1", "VAR2:value2"], + variables_env=["ENV1:envvalue1"], + variables_file=["FILE1:file1.txt"], + variables_from="/path/to/vars.json", + repo="group/project", + ) + + assert result["success"] is True + assert result["branch"] == "develop" + + expected_args = [ + "ci", "run", + "-b", "develop", + "--variables", "VAR1:value1", + "--variables", "VAR2:value2", + "--variables-env", "ENV1:envvalue1", + "--variables-file", "FILE1:file1.txt", + "-f", "/path/to/vars.json", + "-R", "group/project" + ] + mock_execute.assert_called_once_with(expected_args, working_dir) + + @patch.object(GitLabServer, "execute_glab_command") + def test_run_ci_pipeline_with_web_mode_and_variables( + self, mock_execute: MagicMock + ) -> None: + """Test pipeline run with web mode and additional variables.""" + # Mock successful glab command execution + mock_execute.return_value = (True, "Pipeline created") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + branch="main", + variables_env=["CUSTOM_VAR:value"], + web_mode=True, + ) + + assert result["success"] is True + assert result["web_mode"] is True + + expected_args = [ + "ci", "run", + "-b", "main", + "--variables-env", "CUSTOM_VAR:value", + "--variables-env", "CI_PIPELINE_SOURCE:web" + ] + mock_execute.assert_called_once_with(expected_args, working_dir) + + @patch.object(GitLabServer, "execute_glab_command") + def test_run_ci_pipeline_glab_failure(self, mock_execute: MagicMock) -> None: + """Test pipeline run when glab command fails.""" + # Mock glab command failure + mock_execute.return_value = (False, {"error": "Authentication required"}) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + branch="main", + ) + + assert result == {"error": "Authentication required"} + mock_execute.assert_called_once_with( + ["ci", "run", "-b", "main"], + working_dir, + ) + + @patch.object(GitLabServer, "execute_glab_command") + def test_run_ci_pipeline_no_url_in_output(self, mock_execute: MagicMock) -> None: + """Test pipeline run when output doesn't contain pipeline URL.""" + # Mock successful glab command execution without URL + mock_execute.return_value = ( + True, + "Pipeline created successfully\nCheck status later" + ) + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + branch="main", + ) + + assert result["success"] is True + assert result["branch"] == "main" + assert "pipeline_url" not in result + assert "Pipeline created successfully" in result["output"] + + @patch.object(GitLabServer, "execute_glab_command") + @patch("subprocess.run") + def test_run_ci_pipeline_git_exception( + self, mock_subprocess: MagicMock, mock_execute: MagicMock + ) -> None: + """Test pipeline run when git command raises an exception.""" + # Mock git branch --show-current command exception + mock_subprocess.side_effect = Exception("Git command failed") + + # Mock successful glab command execution (without branch) + mock_execute.return_value = (True, "Pipeline created") + + server = GitLabServer() + working_dir = "/test/directory" + result = server.run_ci_pipeline( + working_directory=working_dir, + ) + + assert result["success"] is True + assert result["branch"] is None + + # Should run without -b flag when git command fails + mock_execute.assert_called_once_with( + ["ci", "run"], + working_dir, + ) -- cgit v1.2.3