From 7e7066ff6ea941ef7e5034ccf7c1f3e880f6062e Mon Sep 17 00:00:00 2001 From: "kotaro.saito" Date: Sat, 17 Jan 2026 16:15:25 +0900 Subject: [PATCH 1/2] fix(cli): respect ignore files in adk deploy commands The adk deploy commands (cloud_run, agent_engine, gke) were not properly respecting .gitignore, .gcloudignore, or .ae_ignore files, causing unwanted files (like venv, .git, etc.) to be uploaded. This change: - Adds a unified _get_ignore_patterns_func helper that reads all three ignore files. - Updates to_cloud_run, to_agent_engine, and to_gke to use this helper. - Removes hardcoded ignore patterns to strictly follow user configuration. - Adds comprehensive unit tests to verify the fix. Fixes #4183 --- src/google/adk/cli/cli_deploy.py | 40 +++- .../cli/utils/test_cli_deploy_ignore.py | 197 ++++++++++++++++++ 2 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 tests/unittests/cli/utils/test_cli_deploy_ignore.py diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index 465ca6c3e1..2bc6da7984 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -623,6 +623,29 @@ def _get_service_option_by_adk_version( return ' '.join(options) +def _get_ignore_patterns_func(agent_folder: str): + """Returns a shutil.ignore_patterns function with combined patterns from .gitignore, .gcloudignore and .ae_ignore.""" + patterns = set() + + for filename in ['.gitignore', '.gcloudignore', '.ae_ignore']: + filepath = os.path.join(agent_folder, filename) + if os.path.exists(filepath): + click.echo(f'Reading ignore patterns from {filename}...') + try: + with open(filepath, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + # If it ends with /, remove it for fnmatch compatibility + if line.endswith('/'): + line = line[:-1] + patterns.add(line) + except Exception as e: + click.secho(f'Warning: Failed to read {filename}: {e}', fg='yellow') + + return shutil.ignore_patterns(*patterns) + + def to_cloud_run( *, agent_folder: str, @@ -698,7 +721,8 @@ def to_cloud_run( # copy agent source code click.echo('Copying agent source code...') agent_src_path = os.path.join(temp_folder, 'agents', app_name) - shutil.copytree(agent_folder, agent_src_path) + ignore_func = _get_ignore_patterns_func(agent_folder) + shutil.copytree(agent_folder, agent_src_path, ignore=ignore_func) requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt') install_agent_deps = ( f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"' @@ -927,19 +951,12 @@ def to_agent_engine( shutil.rmtree(agent_src_path) try: - click.echo(f'Staging all files in: {agent_src_path}') - ignore_patterns = None - ae_ignore_path = os.path.join(agent_folder, '.ae_ignore') - if os.path.exists(ae_ignore_path): - click.echo(f'Ignoring files matching the patterns in {ae_ignore_path}') - with open(ae_ignore_path, 'r') as f: - patterns = [pattern.strip() for pattern in f.readlines()] - ignore_patterns = shutil.ignore_patterns(*patterns) + ignore_func = _get_ignore_patterns_func(agent_folder) click.echo('Copying agent source code...') shutil.copytree( agent_folder, agent_src_path, - ignore=ignore_patterns, + ignore=ignore_func, dirs_exist_ok=True, ) click.echo('Copying agent source code complete.') @@ -1218,7 +1235,8 @@ def to_gke( # copy agent source code click.echo(' - Copying agent source code...') agent_src_path = os.path.join(temp_folder, 'agents', app_name) - shutil.copytree(agent_folder, agent_src_path) + ignore_func = _get_ignore_patterns_func(agent_folder) + shutil.copytree(agent_folder, agent_src_path, ignore=ignore_func) requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt') install_agent_deps = ( f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"' diff --git a/tests/unittests/cli/utils/test_cli_deploy_ignore.py b/tests/unittests/cli/utils/test_cli_deploy_ignore.py new file mode 100644 index 0000000000..365ac2da88 --- /dev/null +++ b/tests/unittests/cli/utils/test_cli_deploy_ignore.py @@ -0,0 +1,197 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for ignore file support in cli_deploy.""" + +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess +from unittest import mock + +import click +import pytest + +import src.google.adk.cli.cli_deploy as cli_deploy + + +@pytest.fixture(autouse=True) +def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None: + """Suppress click.echo to keep test output clean.""" + monkeypatch.setattr(click, "echo", lambda *_a, **_k: None) + monkeypatch.setattr(click, "secho", lambda *_a, **_k: None) + + +def test_to_cloud_run_respects_ignore_files( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Test that to_cloud_run respects .gitignore and .gcloudignore.""" + agent_dir = tmp_path / "agent" + agent_dir.mkdir() + (agent_dir / "agent.py").write_text("# agent") + (agent_dir / "__init__.py").write_text("") + (agent_dir / "ignored_by_git.txt").write_text("ignored") + (agent_dir / "ignored_by_gcloud.txt").write_text("ignored") + (agent_dir / "not_ignored.txt").write_text("keep") + + (agent_dir / ".gitignore").write_text("ignored_by_git.txt\n") + (agent_dir / ".gcloudignore").write_text("ignored_by_gcloud.txt\n") + + temp_deploy_dir = tmp_path / "temp_deploy" + + # Mock subprocess.run to avoid actual gcloud call + monkeypatch.setattr(subprocess, "run", mock.Mock()) + # Mock shutil.rmtree to keep the temp folder for verification + monkeypatch.setattr( + shutil, + "rmtree", + lambda path, **kwargs: None + if "temp_deploy" in str(path) + else shutil.rmtree(path, **kwargs), + ) + + cli_deploy.to_cloud_run( + agent_folder=str(agent_dir), + project="proj", + region="us-central1", + service_name="svc", + app_name="app", + temp_folder=str(temp_deploy_dir), + port=8080, + trace_to_cloud=False, + with_ui=False, + log_level="info", + verbosity="info", + adk_version="1.0.0", + ) + + agent_src_path = temp_deploy_dir / "agents" / "app" + + assert (agent_src_path / "agent.py").exists() + assert (agent_src_path / "not_ignored.txt").exists() + + # These should be ignored + assert not ( + agent_src_path / "ignored_by_git.txt" + ).exists(), "Should respect .gitignore" + assert not ( + agent_src_path / "ignored_by_gcloud.txt" + ).exists(), "Should respect .gcloudignore" + + +def test_to_agent_engine_respects_multiple_ignore_files( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Test that to_agent_engine respects .gitignore, .gcloudignore and .ae_ignore.""" + # We need to be in the project dir for to_agent_engine + project_dir = tmp_path / "project" + project_dir.mkdir() + monkeypatch.chdir(project_dir) + + agent_dir = project_dir / "my_agent" + agent_dir.mkdir() + (agent_dir / "agent.py").write_text("root_agent = None") + (agent_dir / "__init__.py").write_text("from . import agent") + (agent_dir / "ignored_by_git.txt").write_text("ignored") + (agent_dir / "ignored_by_ae.txt").write_text("ignored") + + (agent_dir / ".gitignore").write_text("ignored_by_git.txt\n") + (agent_dir / ".ae_ignore").write_text("ignored_by_ae.txt\n") + + # Mock vertexai.Client and other things to avoid network/complex setup + monkeypatch.setattr("vertexai.Client", mock.Mock()) + # Mock shutil.rmtree to keep the temp folder for verification + original_rmtree = shutil.rmtree + + def mock_rmtree(path, **kwargs): + if "_tmp" in str(path): + return None + return original_rmtree(path, **kwargs) + + monkeypatch.setattr(shutil, "rmtree", mock_rmtree) + + cli_deploy.to_agent_engine( + agent_folder=str(agent_dir), + staging_bucket="gs://test", + adk_app="adk_app", + ) + + # Find the temp folder created by to_agent_engine + temp_folders = [ + d for d in project_dir.iterdir() if d.is_dir() and "_tmp" in d.name + ] + assert len(temp_folders) == 1 + agent_src_path = temp_folders[0] + + assert (agent_src_path / "agent.py").exists() + assert not ( + agent_src_path / "ignored_by_git.txt" + ).exists(), "Should respect .gitignore" + assert not ( + agent_src_path / "ignored_by_ae.txt" + ).exists(), "Should respect .ae_ignore" + + +def test_to_gke_respects_ignore_files( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Test that to_gke respects ignore files.""" + agent_dir = tmp_path / "agent" + agent_dir.mkdir() + (agent_dir / "agent.py").write_text("# agent") + (agent_dir / "__init__.py").write_text("") + (agent_dir / "ignored.txt").write_text("ignored") + (agent_dir / ".gitignore").write_text("ignored.txt\n") + + temp_deploy_dir = tmp_path / "temp_deploy" + + # Mock subprocess.run to avoid actual gcloud call + mock_run = mock.Mock() + mock_run.return_value.stdout = "deployment created" + monkeypatch.setattr(subprocess, "run", mock_run) + # Mock shutil.rmtree to keep the temp folder for verification + monkeypatch.setattr( + shutil, + "rmtree", + lambda path, **kwargs: None + if "temp_deploy" in str(path) + else shutil.rmtree(path, **kwargs), + ) + + cli_deploy.to_gke( + agent_folder=str(agent_dir), + project="proj", + region="us-central1", + cluster_name="cluster", + service_name="svc", + app_name="app", + temp_folder=str(temp_deploy_dir), + port=8080, + trace_to_cloud=False, + with_ui=False, + log_level="info", + adk_version="1.0.0", + ) + + agent_src_path = temp_deploy_dir / "agents" / "app" + + assert (agent_src_path / "agent.py").exists() + assert not ( + agent_src_path / "ignored.txt" + ).exists(), "Should respect .gitignore" From bb41f599e2355d15d71940bf98ada8efcd1c0f74 Mon Sep 17 00:00:00 2001 From: "kotaro.saito" Date: Sat, 17 Jan 2026 16:30:13 +0900 Subject: [PATCH 2/2] test(cli): update unit tests for ignore files after upstream merge --- tests/unittests/cli/utils/test_cli_deploy_ignore.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unittests/cli/utils/test_cli_deploy_ignore.py b/tests/unittests/cli/utils/test_cli_deploy_ignore.py index 365ac2da88..8b7831fbca 100644 --- a/tests/unittests/cli/utils/test_cli_deploy_ignore.py +++ b/tests/unittests/cli/utils/test_cli_deploy_ignore.py @@ -73,6 +73,7 @@ def test_to_cloud_run_respects_ignore_files( temp_folder=str(temp_deploy_dir), port=8080, trace_to_cloud=False, + otel_to_cloud=False, with_ui=False, log_level="info", verbosity="info", @@ -184,6 +185,7 @@ def test_to_gke_respects_ignore_files( temp_folder=str(temp_deploy_dir), port=8080, trace_to_cloud=False, + otel_to_cloud=False, with_ui=False, log_level="info", adk_version="1.0.0",