⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,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,
Expand Down Expand Up @@ -578,7 +601,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"'
Expand All @@ -591,7 +615,7 @@ def to_cloud_run(
click.echo('Creating Dockerfile...')
host_option = '--host=0.0.0.0' if adk_version > '0.5.0' else ''
allow_origins_option = (
f'--allow_origins={",".join(allow_origins)}' if allow_origins else ''
f"--allow_origins={','.join(allow_origins)}" if allow_origins else ''
)
a2a_option = '--a2a' if a2a else ''
dockerfile_content = _DOCKERFILE_TEMPLATE.format(
Expand Down Expand Up @@ -795,19 +819,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.')
Expand Down Expand Up @@ -1073,7 +1090,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"'
Expand All @@ -1083,7 +1101,7 @@ def to_gke(
click.secho('✅ Environment prepared.', fg='green')

allow_origins_option = (
f'--allow_origins={",".join(allow_origins)}' if allow_origins else ''
f"--allow_origins={','.join(allow_origins)}" if allow_origins else ''
)

# create Dockerfile
Expand Down
199 changes: 199 additions & 0 deletions tests/unittests/cli/utils/test_cli_deploy_ignore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# 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,
otel_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,
otel_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"