agent-starter-pack 0.0.1b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agent-starter-pack might be problematic. Click here for more details.
- agent_starter_pack-0.0.1b0.dist-info/METADATA +143 -0
- agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
- agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
- agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
- agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
- agents/agentic_rag_vertexai_search/README.md +22 -0
- agents/agentic_rag_vertexai_search/app/agent.py +145 -0
- agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
- agents/agentic_rag_vertexai_search/app/templates.py +53 -0
- agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
- agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
- agents/crewai_coding_crew/README.md +34 -0
- agents/crewai_coding_crew/app/agent.py +86 -0
- agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
- agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
- agents/crewai_coding_crew/app/crew/crew.py +71 -0
- agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
- agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
- agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
- agents/langgraph_base_react/README.md +9 -0
- agents/langgraph_base_react/app/agent.py +73 -0
- agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
- agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
- agents/multimodal_live_api/README.md +50 -0
- agents/multimodal_live_api/app/agent.py +86 -0
- agents/multimodal_live_api/app/server.py +193 -0
- agents/multimodal_live_api/app/templates.py +51 -0
- agents/multimodal_live_api/app/vector_store.py +55 -0
- agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
- agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
- agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
- agents/multimodal_live_api/tests/unit/test_server.py +143 -0
- src/base_template/.gitignore +197 -0
- src/base_template/Makefile +37 -0
- src/base_template/README.md +91 -0
- src/base_template/app/utils/tracing.py +143 -0
- src/base_template/app/utils/typing.py +115 -0
- src/base_template/deployment/README.md +123 -0
- src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
- src/base_template/deployment/cd/staging.yaml +215 -0
- src/base_template/deployment/ci/pr_checks.yaml +51 -0
- src/base_template/deployment/terraform/apis.tf +34 -0
- src/base_template/deployment/terraform/build_triggers.tf +122 -0
- src/base_template/deployment/terraform/dev/apis.tf +42 -0
- src/base_template/deployment/terraform/dev/iam.tf +90 -0
- src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
- src/base_template/deployment/terraform/dev/providers.tf +29 -0
- src/base_template/deployment/terraform/dev/storage.tf +76 -0
- src/base_template/deployment/terraform/dev/variables.tf +126 -0
- src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
- src/base_template/deployment/terraform/iam.tf +130 -0
- src/base_template/deployment/terraform/locals.tf +50 -0
- src/base_template/deployment/terraform/log_sinks.tf +72 -0
- src/base_template/deployment/terraform/providers.tf +35 -0
- src/base_template/deployment/terraform/service_accounts.tf +42 -0
- src/base_template/deployment/terraform/storage.tf +100 -0
- src/base_template/deployment/terraform/variables.tf +202 -0
- src/base_template/deployment/terraform/vars/env.tfvars +43 -0
- src/base_template/pyproject.toml +113 -0
- src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
- src/cli/commands/create.py +534 -0
- src/cli/commands/setup_cicd.py +730 -0
- src/cli/main.py +35 -0
- src/cli/utils/__init__.py +35 -0
- src/cli/utils/cicd.py +662 -0
- src/cli/utils/gcp.py +120 -0
- src/cli/utils/logging.py +51 -0
- src/cli/utils/template.py +644 -0
- src/data_ingestion/README.md +79 -0
- src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
- src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
- src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
- src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
- src/data_ingestion/pyproject.toml +17 -0
- src/data_ingestion/uv.lock +999 -0
- src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
- src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
- src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
- src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
- src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
- src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
- src/deployment_targets/cloud_run/Dockerfile +29 -0
- src/deployment_targets/cloud_run/app/server.py +128 -0
- src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
- src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
- src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
- src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
- src/deployment_targets/cloud_run/uv.lock +6952 -0
- src/frontends/live_api_react/frontend/package-lock.json +19405 -0
- src/frontends/live_api_react/frontend/package.json +56 -0
- src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
- src/frontends/live_api_react/frontend/public/index.html +62 -0
- src/frontends/live_api_react/frontend/public/robots.txt +3 -0
- src/frontends/live_api_react/frontend/src/App.scss +189 -0
- src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
- src/frontends/live_api_react/frontend/src/App.tsx +205 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
- src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
- src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
- src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
- src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
- src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
- src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
- src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
- src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
- src/frontends/live_api_react/frontend/src/index.css +28 -0
- src/frontends/live_api_react/frontend/src/index.tsx +35 -0
- src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
- src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
- src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
- src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
- src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
- src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
- src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
- src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
- src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
- src/frontends/live_api_react/frontend/tsconfig.json +25 -0
- src/frontends/streamlit/frontend/side_bar.py +213 -0
- src/frontends/streamlit/frontend/streamlit_app.py +263 -0
- src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
- src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
- src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
- src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
- src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
- src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
- src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
- src/resources/containers/data_processing/Dockerfile +25 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
- src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
- src/resources/setup_cicd/cicd_variables.tf +36 -0
- src/resources/setup_cicd/github.tf +85 -0
- src/resources/setup_cicd/providers.tf +39 -0
- src/utils/generate_locks.py +135 -0
- src/utils/lock_utils.py +82 -0
- src/utils/watch_and_rebuild.py +190 -0
src/cli/utils/cicd.py
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Utilities for CI/CD setup and management."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.prompt import IntPrompt, Prompt
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def setup_git_provider(non_interactive: bool = False) -> str:
|
|
32
|
+
"""Interactive selection of git provider."""
|
|
33
|
+
if non_interactive:
|
|
34
|
+
return "github" # Default to GitHub in non-interactive mode
|
|
35
|
+
|
|
36
|
+
console.print("\n> Git Provider Configuration", style="bold blue")
|
|
37
|
+
providers = [
|
|
38
|
+
("github", "GitHub"),
|
|
39
|
+
# Add more providers here in the future
|
|
40
|
+
# ("gitlab", "GitLab (Coming soon)"),
|
|
41
|
+
# ("bitbucket", "Bitbucket (Coming soon)"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
console.print("Available Git providers:")
|
|
45
|
+
for i, (id, name) in enumerate(providers, 1):
|
|
46
|
+
if id == "github":
|
|
47
|
+
console.print(f"{i}. {name}")
|
|
48
|
+
else:
|
|
49
|
+
console.print(f"{i}. {name}", style="dim")
|
|
50
|
+
|
|
51
|
+
choice = IntPrompt.ask("Select your Git provider", default=1)
|
|
52
|
+
|
|
53
|
+
git_provider = providers[choice - 1][0]
|
|
54
|
+
if git_provider != "github":
|
|
55
|
+
console.print("⚠️ Only GitHub is currently supported.", style="bold yellow")
|
|
56
|
+
raise ValueError("Unsupported git provider")
|
|
57
|
+
|
|
58
|
+
return git_provider
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def setup_repository_name(
|
|
62
|
+
default_prefix: str = "genai-app", non_interactive: bool = False
|
|
63
|
+
) -> tuple[str, str]:
|
|
64
|
+
"""Interactive setup of repository name and owner."""
|
|
65
|
+
if non_interactive:
|
|
66
|
+
timestamp = int(time.time())
|
|
67
|
+
# Return empty string instead of None to match return type
|
|
68
|
+
return f"{default_prefix}-{timestamp}", ""
|
|
69
|
+
|
|
70
|
+
console.print("\n> Repository Configuration", style="bold blue")
|
|
71
|
+
|
|
72
|
+
# Get current GitHub username
|
|
73
|
+
result = run_command(["gh", "api", "user", "--jq", ".login"], capture_output=True)
|
|
74
|
+
github_username = result.stdout.strip()
|
|
75
|
+
|
|
76
|
+
# Get repository name
|
|
77
|
+
repo_name = Prompt.ask(
|
|
78
|
+
"Enter repository name", default=f"{default_prefix}-{int(time.time())}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Get repository owner (default to current user)
|
|
82
|
+
repo_owner = Prompt.ask(
|
|
83
|
+
"Enter repository owner (organization or username)", default=github_username
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return repo_name, repo_owner
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_github_connection(
|
|
90
|
+
project_id: str,
|
|
91
|
+
region: str,
|
|
92
|
+
connection_name: str,
|
|
93
|
+
repository_name: str,
|
|
94
|
+
repository_owner: str,
|
|
95
|
+
) -> tuple[str, str]:
|
|
96
|
+
"""Create and verify GitHub connection using gcloud command.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
project_id: GCP project ID
|
|
100
|
+
region: GCP region
|
|
101
|
+
connection_name: Name for the GitHub connection
|
|
102
|
+
repository_name: Name of repository to create
|
|
103
|
+
repository_owner: Owner of repository
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
tuple[str, str]: The OAuth token secret ID and the app installation ID
|
|
107
|
+
"""
|
|
108
|
+
console.print("\n🔗 Creating GitHub connection...")
|
|
109
|
+
|
|
110
|
+
# Create repository if details provided
|
|
111
|
+
try:
|
|
112
|
+
# Check if repo exists
|
|
113
|
+
result = run_command(
|
|
114
|
+
[
|
|
115
|
+
"gh",
|
|
116
|
+
"repo",
|
|
117
|
+
"view",
|
|
118
|
+
f"{repository_owner}/{repository_name}",
|
|
119
|
+
"--json",
|
|
120
|
+
"name",
|
|
121
|
+
],
|
|
122
|
+
capture_output=True,
|
|
123
|
+
check=False,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if result.returncode != 0:
|
|
127
|
+
# Repository doesn't exist, create it
|
|
128
|
+
console.print(
|
|
129
|
+
f"\n📦 Creating GitHub repository: {repository_owner}/{repository_name}"
|
|
130
|
+
)
|
|
131
|
+
run_command(
|
|
132
|
+
[
|
|
133
|
+
"gh",
|
|
134
|
+
"repo",
|
|
135
|
+
"create",
|
|
136
|
+
f"{repository_owner}/{repository_name}",
|
|
137
|
+
"--private",
|
|
138
|
+
"--description",
|
|
139
|
+
"Repository created by Terraform",
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
console.print("✅ GitHub repository created")
|
|
143
|
+
else:
|
|
144
|
+
console.print("✅ Using existing GitHub repository")
|
|
145
|
+
except subprocess.CalledProcessError as e:
|
|
146
|
+
console.print(f"❌ Failed to create/check repository: {e!s}", style="bold red")
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
def try_create_connection() -> subprocess.CompletedProcess[str]:
|
|
150
|
+
cmd = [
|
|
151
|
+
"gcloud",
|
|
152
|
+
"builds",
|
|
153
|
+
"connections",
|
|
154
|
+
"create",
|
|
155
|
+
"github",
|
|
156
|
+
connection_name,
|
|
157
|
+
f"--region={region}",
|
|
158
|
+
f"--project={project_id}",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Display the command being run
|
|
162
|
+
console.print(f"\n🔄 Running command: {' '.join(cmd)}")
|
|
163
|
+
|
|
164
|
+
# Use Popen to get control over stdin
|
|
165
|
+
process = subprocess.Popen(
|
|
166
|
+
cmd,
|
|
167
|
+
stdin=subprocess.PIPE,
|
|
168
|
+
stdout=subprocess.PIPE,
|
|
169
|
+
stderr=subprocess.PIPE,
|
|
170
|
+
text=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Send 'y' followed by enter key to handle both the API enablement prompt and any other prompts
|
|
174
|
+
stdout, stderr = process.communicate(input="y\n")
|
|
175
|
+
|
|
176
|
+
# Create a CompletedProcess-like object for compatibility
|
|
177
|
+
return subprocess.CompletedProcess(
|
|
178
|
+
args=cmd, returncode=process.returncode, stdout=stdout, stderr=stderr
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Try initial connection creation
|
|
182
|
+
result = try_create_connection()
|
|
183
|
+
|
|
184
|
+
if result.returncode == 0:
|
|
185
|
+
console.print("✅ GitHub connection created successfully")
|
|
186
|
+
else:
|
|
187
|
+
stderr = str(result.stderr)
|
|
188
|
+
console.print(stderr)
|
|
189
|
+
|
|
190
|
+
if "ALREADY_EXISTS" in stderr:
|
|
191
|
+
console.print("✅ Using existing GitHub connection")
|
|
192
|
+
else:
|
|
193
|
+
console.print(
|
|
194
|
+
f"❌ Failed to create GitHub connection: {stderr}", style="bold red"
|
|
195
|
+
)
|
|
196
|
+
raise subprocess.CalledProcessError(
|
|
197
|
+
result.returncode, result.args, result.stdout, stderr
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
console.print("\n⚠️ Important:", style="bold yellow")
|
|
201
|
+
console.print(
|
|
202
|
+
"1. Please visit the URL below to authorize Cloud Build (if not already authorized)"
|
|
203
|
+
)
|
|
204
|
+
console.print("2. After authorization, the setup will continue automatically")
|
|
205
|
+
console.print("\nChecking connection status...")
|
|
206
|
+
|
|
207
|
+
# Poll for connection readiness
|
|
208
|
+
max_retries = 30 # 5 minutes total with 10s sleep
|
|
209
|
+
for attempt in range(max_retries):
|
|
210
|
+
try:
|
|
211
|
+
result = run_command(
|
|
212
|
+
[
|
|
213
|
+
"gcloud",
|
|
214
|
+
"builds",
|
|
215
|
+
"connections",
|
|
216
|
+
"describe",
|
|
217
|
+
connection_name,
|
|
218
|
+
f"--region={region}",
|
|
219
|
+
f"--project={project_id}",
|
|
220
|
+
"--format=json",
|
|
221
|
+
],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
status = json.loads(result.stdout).get("installationState", {}).get("stage")
|
|
226
|
+
|
|
227
|
+
if status == "COMPLETE":
|
|
228
|
+
console.print("✅ GitHub connection is authorized and ready")
|
|
229
|
+
|
|
230
|
+
# Get the secret version and app installation ID
|
|
231
|
+
connection_data = json.loads(result.stdout)
|
|
232
|
+
github_config = connection_data.get("githubConfig", {})
|
|
233
|
+
|
|
234
|
+
oauth_token_secret_version = github_config.get(
|
|
235
|
+
"authorizerCredential", {}
|
|
236
|
+
).get("oauthTokenSecretVersion")
|
|
237
|
+
app_installation_id = github_config.get("appInstallationId")
|
|
238
|
+
|
|
239
|
+
if not oauth_token_secret_version or not app_installation_id:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
"Could not find OAuth token secret version or app installation ID in connection details"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Extract just the secret ID from the full path
|
|
245
|
+
# Format: "projects/PROJECT_ID/secrets/SECRET_ID/versions/VERSION"
|
|
246
|
+
secret_id = oauth_token_secret_version.split("/secrets/")[1].split(
|
|
247
|
+
"/versions/"
|
|
248
|
+
)[0]
|
|
249
|
+
|
|
250
|
+
console.print(f"✅ Retrieved OAuth token secret ID: {secret_id}")
|
|
251
|
+
console.print(
|
|
252
|
+
f"✅ Retrieved app installation ID: {app_installation_id}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return secret_id, app_installation_id
|
|
256
|
+
elif status == "PENDING_USER_OAUTH" or status == "PENDING_INSTALL_APP":
|
|
257
|
+
if attempt < max_retries - 1: # Don't print waiting on last attempt
|
|
258
|
+
console.print("⏳ Waiting for GitHub authorization...")
|
|
259
|
+
time.sleep(10)
|
|
260
|
+
continue
|
|
261
|
+
else:
|
|
262
|
+
raise Exception(f"Unexpected connection status: {status}")
|
|
263
|
+
|
|
264
|
+
except subprocess.CalledProcessError as e:
|
|
265
|
+
console.print(
|
|
266
|
+
f"❌ Failed to check connection status: {e}", style="bold red"
|
|
267
|
+
)
|
|
268
|
+
raise
|
|
269
|
+
|
|
270
|
+
raise TimeoutError("GitHub connection authorization timed out after 5 minutes")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
274
|
+
class ProjectConfig:
|
|
275
|
+
staging_project_id: str
|
|
276
|
+
prod_project_id: str
|
|
277
|
+
cicd_project_id: str
|
|
278
|
+
agent: str
|
|
279
|
+
deployment_target: str
|
|
280
|
+
region: str = "us-central1"
|
|
281
|
+
dev_project_id: str | None = None
|
|
282
|
+
project_name: str | None = None
|
|
283
|
+
repository_name: str | None = None
|
|
284
|
+
repository_owner: str | None = None
|
|
285
|
+
host_connection_name: str | None = None
|
|
286
|
+
github_pat: str | None = None
|
|
287
|
+
github_app_installation_id: str | None = None
|
|
288
|
+
git_provider: str = "github"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def print_cicd_summary(
|
|
292
|
+
config: ProjectConfig, github_username: str, repo_url: str, cloud_build_url: str
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Print a summary of the CI/CD setup."""
|
|
295
|
+
console.print("\n🎉 CI/CD Infrastructure Setup Complete!", style="bold green")
|
|
296
|
+
console.print("====================================")
|
|
297
|
+
console.print("\n📊 Resource Summary:")
|
|
298
|
+
console.print(f" • Development Project: {config.dev_project_id}")
|
|
299
|
+
console.print(f" • Staging Project: {config.staging_project_id}")
|
|
300
|
+
console.print(f" • Production Project: {config.prod_project_id}")
|
|
301
|
+
console.print(f" • CICD Project: {config.cicd_project_id}")
|
|
302
|
+
console.print(f" • Repository: {config.repository_name}")
|
|
303
|
+
console.print(f" • Region: {config.region}")
|
|
304
|
+
|
|
305
|
+
console.print("\n🔗 Important Links:")
|
|
306
|
+
console.print(f" • GitHub Repository: {repo_url}")
|
|
307
|
+
console.print(f" • Cloud Build Console: {cloud_build_url}")
|
|
308
|
+
|
|
309
|
+
console.print("\n📝 Next Steps:", style="bold blue")
|
|
310
|
+
console.print("1. Push your code to the repository")
|
|
311
|
+
console.print("2. Create and merge a pull request to trigger CI/CD pipelines")
|
|
312
|
+
console.print("3. Monitor builds in the Cloud Build console")
|
|
313
|
+
console.print(
|
|
314
|
+
"4. After successful staging deployment, approve production deployment in Cloud Build"
|
|
315
|
+
)
|
|
316
|
+
console.print(
|
|
317
|
+
"\n🌟 Enjoy building your new Agent! Happy coding! 🚀", style="bold green"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def ensure_apis_enabled(project_id: str, apis: list[str]) -> None:
|
|
322
|
+
"""Check and enable required APIs and set up necessary permissions.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
project_id: GCP project ID where APIs should be enabled
|
|
326
|
+
apis: List of API service names to check and enable
|
|
327
|
+
"""
|
|
328
|
+
console.print("\n🔍 Checking required APIs...")
|
|
329
|
+
for api in apis:
|
|
330
|
+
try:
|
|
331
|
+
# Check if API is enabled
|
|
332
|
+
result = run_command(
|
|
333
|
+
[
|
|
334
|
+
"gcloud",
|
|
335
|
+
"services",
|
|
336
|
+
"list",
|
|
337
|
+
f"--project={project_id}",
|
|
338
|
+
f"--filter=config.name:{api}",
|
|
339
|
+
"--format=json",
|
|
340
|
+
],
|
|
341
|
+
capture_output=True,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
services = json.loads(result.stdout)
|
|
345
|
+
if not services: # API not enabled
|
|
346
|
+
console.print(f"📡 Enabling {api}...")
|
|
347
|
+
run_command(
|
|
348
|
+
["gcloud", "services", "enable", api, f"--project={project_id}"]
|
|
349
|
+
)
|
|
350
|
+
console.print(f"✅ Enabled {api}")
|
|
351
|
+
else:
|
|
352
|
+
console.print(f"✅ {api} already enabled")
|
|
353
|
+
except subprocess.CalledProcessError as e:
|
|
354
|
+
console.print(f"❌ Failed to check/enable {api}: {e!s}", style="bold red")
|
|
355
|
+
raise
|
|
356
|
+
|
|
357
|
+
# Get the Cloud Build service account
|
|
358
|
+
console.print("\n🔑 Setting up service account permissions...")
|
|
359
|
+
try:
|
|
360
|
+
result = run_command(
|
|
361
|
+
["gcloud", "projects", "get-iam-policy", project_id, "--format=json"],
|
|
362
|
+
capture_output=True,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
project_number = run_command(
|
|
366
|
+
[
|
|
367
|
+
"gcloud",
|
|
368
|
+
"projects",
|
|
369
|
+
"describe",
|
|
370
|
+
project_id,
|
|
371
|
+
"--format=value(projectNumber)",
|
|
372
|
+
],
|
|
373
|
+
capture_output=True,
|
|
374
|
+
).stdout.strip()
|
|
375
|
+
|
|
376
|
+
cloudbuild_sa = (
|
|
377
|
+
f"service-{project_number}@gcp-sa-cloudbuild.iam.gserviceaccount.com"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Grant Secret Manager Admin role to Cloud Build service account
|
|
381
|
+
console.print(f"📦 Granting Secret Manager Admin role to {cloudbuild_sa}...")
|
|
382
|
+
run_command(
|
|
383
|
+
[
|
|
384
|
+
"gcloud",
|
|
385
|
+
"projects",
|
|
386
|
+
"add-iam-policy-binding",
|
|
387
|
+
project_id,
|
|
388
|
+
f"--member=serviceAccount:{cloudbuild_sa}",
|
|
389
|
+
"--role=roles/secretmanager.admin",
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
console.print("✅ Permissions granted to Cloud Build service account")
|
|
393
|
+
|
|
394
|
+
except subprocess.CalledProcessError as e:
|
|
395
|
+
console.print(
|
|
396
|
+
f"❌ Failed to set up service account permissions: {e!s}", style="bold red"
|
|
397
|
+
)
|
|
398
|
+
raise
|
|
399
|
+
|
|
400
|
+
# Add a small delay to allow API enablement and IAM changes to propagate
|
|
401
|
+
time.sleep(10)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def run_command(
|
|
405
|
+
cmd: list[str] | str,
|
|
406
|
+
check: bool = True,
|
|
407
|
+
cwd: Path | None = None,
|
|
408
|
+
capture_output: bool = False,
|
|
409
|
+
shell: bool = False,
|
|
410
|
+
) -> subprocess.CompletedProcess:
|
|
411
|
+
"""Run a command and display it to the user"""
|
|
412
|
+
# Format command for display
|
|
413
|
+
cmd_str = cmd if isinstance(cmd, str) else " ".join(cmd)
|
|
414
|
+
print(f"\n🔄 Running command: {cmd_str}")
|
|
415
|
+
if cwd:
|
|
416
|
+
print(f"📂 In directory: {cwd}")
|
|
417
|
+
|
|
418
|
+
# Run the command
|
|
419
|
+
result = subprocess.run(
|
|
420
|
+
cmd, check=check, cwd=cwd, capture_output=capture_output, text=True, shell=shell
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Display output if captured
|
|
424
|
+
if capture_output and result.stdout:
|
|
425
|
+
print(f"📤 Output:\n{result.stdout.strip()}")
|
|
426
|
+
if capture_output and result.stderr:
|
|
427
|
+
print(f"⚠️ Error output:\n{result.stderr}")
|
|
428
|
+
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def create_github_repository(repository_owner: str, repository_name: str) -> None:
|
|
433
|
+
"""Create GitHub repository if it doesn't exist.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
repository_owner: Owner of the repository
|
|
437
|
+
repository_name: Name of the repository to create
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
# Check if repo exists
|
|
441
|
+
result = run_command(
|
|
442
|
+
[
|
|
443
|
+
"gh",
|
|
444
|
+
"repo",
|
|
445
|
+
"view",
|
|
446
|
+
f"{repository_owner}/{repository_name}",
|
|
447
|
+
"--json",
|
|
448
|
+
"name",
|
|
449
|
+
],
|
|
450
|
+
capture_output=True,
|
|
451
|
+
check=False,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if result.returncode != 0:
|
|
455
|
+
# Repository doesn't exist, create it
|
|
456
|
+
console.print(
|
|
457
|
+
f"\n📦 Creating GitHub repository: {repository_owner}/{repository_name}"
|
|
458
|
+
)
|
|
459
|
+
run_command(
|
|
460
|
+
[
|
|
461
|
+
"gh",
|
|
462
|
+
"repo",
|
|
463
|
+
"create",
|
|
464
|
+
f"{repository_owner}/{repository_name}",
|
|
465
|
+
"--private",
|
|
466
|
+
"--description",
|
|
467
|
+
"Repository created by Terraform",
|
|
468
|
+
]
|
|
469
|
+
)
|
|
470
|
+
console.print("✅ GitHub repository created")
|
|
471
|
+
else:
|
|
472
|
+
console.print("✅ Using existing GitHub repository")
|
|
473
|
+
except subprocess.CalledProcessError as e:
|
|
474
|
+
console.print(f"❌ Failed to create/check repository: {e!s}", style="bold red")
|
|
475
|
+
raise
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class Environment(Enum):
|
|
479
|
+
DEV = "dev"
|
|
480
|
+
STAGING = "staging"
|
|
481
|
+
PROD = "prod"
|
|
482
|
+
CICD = "cicd"
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class E2EDeployment:
|
|
486
|
+
def __init__(self, config: ProjectConfig) -> None:
|
|
487
|
+
self.config = config
|
|
488
|
+
self.projects: dict[Environment, Path] = {}
|
|
489
|
+
|
|
490
|
+
# Generate project name if not provided
|
|
491
|
+
if not self.config.project_name:
|
|
492
|
+
# Create a meaningful default project name based on agent and deployment target
|
|
493
|
+
prefix = f"{self.config.agent}-{self.config.deployment_target}"
|
|
494
|
+
# Clean up prefix to be filesystem compatible
|
|
495
|
+
prefix = re.sub(r"[^a-zA-Z0-9-]", "-", prefix.lower())
|
|
496
|
+
timestamp = int(time.time())
|
|
497
|
+
self.config.project_name = f"{prefix}-{timestamp}"
|
|
498
|
+
|
|
499
|
+
def update_terraform_vars(self, project_dir: Path, is_dev: bool = False) -> None:
|
|
500
|
+
"""Update terraform variables with project configuration"""
|
|
501
|
+
if is_dev:
|
|
502
|
+
# Dev environment only needs one project ID
|
|
503
|
+
tf_vars_path = (
|
|
504
|
+
project_dir / "deployment" / "terraform" / "dev" / "vars" / "env.tfvars"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
with open(tf_vars_path) as f:
|
|
508
|
+
content = f.read()
|
|
509
|
+
|
|
510
|
+
# Replace dev project ID
|
|
511
|
+
content = re.sub(
|
|
512
|
+
r'dev_project_id\s*=\s*"[^"]*"',
|
|
513
|
+
f'dev_project_id = "{self.config.dev_project_id}"',
|
|
514
|
+
content,
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
# Path to production needs staging, prod, and CICD project IDs
|
|
518
|
+
tf_vars_path = (
|
|
519
|
+
project_dir / "deployment" / "terraform" / "vars" / "env.tfvars"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
with open(tf_vars_path) as f:
|
|
523
|
+
content = f.read()
|
|
524
|
+
|
|
525
|
+
# Replace all project IDs
|
|
526
|
+
content = re.sub(
|
|
527
|
+
r'staging_project_id\s*=\s*"[^"]*"',
|
|
528
|
+
f'staging_project_id = "{self.config.staging_project_id}"',
|
|
529
|
+
content,
|
|
530
|
+
)
|
|
531
|
+
content = re.sub(
|
|
532
|
+
r'prod_project_id\s*=\s*"[^"]*"',
|
|
533
|
+
f'prod_project_id = "{self.config.prod_project_id}"',
|
|
534
|
+
content,
|
|
535
|
+
)
|
|
536
|
+
content = re.sub(
|
|
537
|
+
r'cicd_runner_project_id\s*=\s*"[^"]*"',
|
|
538
|
+
f'cicd_runner_project_id = "{self.config.cicd_project_id}"',
|
|
539
|
+
content,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Add host connection and repository name
|
|
543
|
+
content = re.sub(
|
|
544
|
+
r'host_connection_name\s*=\s*"[^"]*"',
|
|
545
|
+
f'host_connection_name = "{self.config.host_connection_name}"',
|
|
546
|
+
content,
|
|
547
|
+
)
|
|
548
|
+
content = re.sub(
|
|
549
|
+
r'repository_name\s*=\s*"[^"]*"',
|
|
550
|
+
f'repository_name = "{self.config.repository_name}"',
|
|
551
|
+
content,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Write updated content
|
|
555
|
+
with open(tf_vars_path, "w") as f:
|
|
556
|
+
f.write(content)
|
|
557
|
+
|
|
558
|
+
def setup_terraform_state(self, project_dir: Path, env: Environment) -> None:
|
|
559
|
+
"""Setup terraform state configuration for dev or prod environment"""
|
|
560
|
+
# Determine terraform directories - we need both for full setup
|
|
561
|
+
tf_dirs = []
|
|
562
|
+
if env == Environment.DEV:
|
|
563
|
+
tf_dirs = [project_dir / "deployment" / "terraform" / "dev"]
|
|
564
|
+
else:
|
|
565
|
+
# For prod/staging, set up both root and dev terraform
|
|
566
|
+
tf_dirs = [
|
|
567
|
+
project_dir / "deployment" / "terraform",
|
|
568
|
+
project_dir / "deployment" / "terraform" / "dev",
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
bucket_name = f"{self.config.cicd_project_id}-terraform-state"
|
|
572
|
+
|
|
573
|
+
# Ensure bucket exists and is accessible
|
|
574
|
+
try:
|
|
575
|
+
result = run_command(
|
|
576
|
+
["gsutil", "ls", "-b", f"gs://{bucket_name}"],
|
|
577
|
+
check=False,
|
|
578
|
+
capture_output=True,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if result.returncode != 0:
|
|
582
|
+
print(f"\n📦 Creating Terraform state bucket: {bucket_name}")
|
|
583
|
+
run_command(
|
|
584
|
+
[
|
|
585
|
+
"gsutil",
|
|
586
|
+
"mb",
|
|
587
|
+
"-p",
|
|
588
|
+
self.config.cicd_project_id,
|
|
589
|
+
"-l",
|
|
590
|
+
self.config.region,
|
|
591
|
+
f"gs://{bucket_name}",
|
|
592
|
+
]
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
run_command(
|
|
596
|
+
["gsutil", "versioning", "set", "on", f"gs://{bucket_name}"]
|
|
597
|
+
)
|
|
598
|
+
except subprocess.CalledProcessError as e:
|
|
599
|
+
print(f"\n❌ Failed to setup state bucket: {e}")
|
|
600
|
+
raise
|
|
601
|
+
|
|
602
|
+
# Create backend.tf in each required directory
|
|
603
|
+
for tf_dir in tf_dirs:
|
|
604
|
+
# Use different state prefixes for dev and prod/staging to keep states separate
|
|
605
|
+
is_dev_dir = str(tf_dir).endswith("/dev")
|
|
606
|
+
state_prefix = "dev" if is_dev_dir else "prod"
|
|
607
|
+
|
|
608
|
+
backend_file = tf_dir / "backend.tf"
|
|
609
|
+
with open(backend_file, "w") as f:
|
|
610
|
+
f.write(f'''terraform {{
|
|
611
|
+
backend "gcs" {{
|
|
612
|
+
bucket = "{bucket_name}"
|
|
613
|
+
prefix = "{state_prefix}"
|
|
614
|
+
}}
|
|
615
|
+
}}
|
|
616
|
+
''')
|
|
617
|
+
print(
|
|
618
|
+
f"\n✅ Terraform state configured in {tf_dir} to use bucket: {bucket_name} with prefix: {state_prefix}"
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def setup_terraform(
|
|
622
|
+
self, project_dir: Path, env: Environment, local_state: bool = False
|
|
623
|
+
) -> None:
|
|
624
|
+
"""Initialize and apply Terraform for the given environment"""
|
|
625
|
+
print(f"\n🏗️ Setting up Terraform for {env.value} environment")
|
|
626
|
+
|
|
627
|
+
# Setup state configuration for all required directories if using remote state
|
|
628
|
+
if not local_state:
|
|
629
|
+
self.setup_terraform_state(project_dir, env)
|
|
630
|
+
|
|
631
|
+
# Determine which directories to process and their corresponding var files
|
|
632
|
+
tf_configs = []
|
|
633
|
+
if env == Environment.DEV:
|
|
634
|
+
tf_configs = [
|
|
635
|
+
(project_dir / "deployment" / "terraform" / "dev", "vars/env.tfvars")
|
|
636
|
+
]
|
|
637
|
+
else:
|
|
638
|
+
# For prod/staging, we need both directories but with their own var files
|
|
639
|
+
tf_configs = [
|
|
640
|
+
(
|
|
641
|
+
project_dir / "deployment" / "terraform",
|
|
642
|
+
"vars/env.tfvars",
|
|
643
|
+
), # Prod vars
|
|
644
|
+
(
|
|
645
|
+
project_dir / "deployment" / "terraform" / "dev",
|
|
646
|
+
"vars/env.tfvars",
|
|
647
|
+
), # Dev vars
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
# Initialize and apply Terraform for each directory
|
|
651
|
+
for tf_dir, var_file in tf_configs:
|
|
652
|
+
print(f"\n🔧 Initializing Terraform in {tf_dir}...")
|
|
653
|
+
if local_state:
|
|
654
|
+
run_command(["terraform", "init", "-backend=false"], cwd=tf_dir)
|
|
655
|
+
else:
|
|
656
|
+
run_command(["terraform", "init"], cwd=tf_dir)
|
|
657
|
+
|
|
658
|
+
print(f"\n🚀 Applying Terraform configuration in {tf_dir}...")
|
|
659
|
+
run_command(
|
|
660
|
+
["terraform", "apply", f"-var-file={var_file}", "-auto-approve"],
|
|
661
|
+
cwd=tf_dir,
|
|
662
|
+
)
|