diagram-to-iac 1.4.0__py3-none-any.whl → 1.6.0__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.
@@ -0,0 +1,123 @@
1
+ """
2
+ Configuration loader for diagram-to-iac tests.
3
+
4
+ This module provides utilities to load test configuration from the main config.yaml file,
5
+ making test repository URLs and other test settings configurable and centralized.
6
+ """
7
+
8
+ import os
9
+ import yaml
10
+ from pathlib import Path
11
+ from typing import Dict, Any
12
+
13
+
14
+ def load_test_config() -> Dict[str, Any]:
15
+ """
16
+ Load test configuration from the main config.yaml file.
17
+
18
+ Returns:
19
+ Dict containing test configuration settings
20
+ """
21
+ # Find the config file relative to this module
22
+ # The config.yaml is at src/diagram_to_iac/config.yaml
23
+ # This file is at src/diagram_to_iac/core/test_config.py
24
+ # So we need to go up one directory
25
+ current_dir = Path(__file__).parent
26
+ config_path = current_dir.parent / "config.yaml"
27
+
28
+ if not config_path.exists():
29
+ raise FileNotFoundError(f"Config file not found at {config_path}")
30
+
31
+ with open(config_path, 'r') as f:
32
+ config = yaml.safe_load(f)
33
+
34
+ return config.get('test', {})
35
+
36
+
37
+ def get_test_repo_url() -> str:
38
+ """
39
+ Get the test repository URL from configuration.
40
+
41
+ Returns:
42
+ The configured test repository URL
43
+ """
44
+ config = load_test_config()
45
+ return config.get('github', {}).get('test_repo_url', 'https://github.com/amartyamandal/test_iac_agent_private.git')
46
+
47
+
48
+ def get_test_repo_owner() -> str:
49
+ """
50
+ Get the test repository owner from configuration.
51
+
52
+ Returns:
53
+ The configured test repository owner
54
+ """
55
+ config = load_test_config()
56
+ return config.get('github', {}).get('test_repo_owner', 'amartyamandal')
57
+
58
+
59
+ def get_test_repo_name() -> str:
60
+ """
61
+ Get the test repository name from configuration.
62
+
63
+ Returns:
64
+ The configured test repository name
65
+ """
66
+ config = load_test_config()
67
+ return config.get('github', {}).get('test_repo_name', 'test_iac_agent_private')
68
+
69
+
70
+ def get_public_test_repo_url() -> str:
71
+ """
72
+ Get the public test repository URL from configuration.
73
+
74
+ Returns:
75
+ The configured public test repository URL
76
+ """
77
+ config = load_test_config()
78
+ return config.get('github', {}).get('public_test_repo_url', 'https://github.com/amartyamandal/test_iac_agent_public.git')
79
+
80
+
81
+ def should_skip_integration_tests() -> bool:
82
+ """
83
+ Check if integration tests should be skipped when no GitHub token is available.
84
+
85
+ Returns:
86
+ True if integration tests should be skipped without a token
87
+ """
88
+ config = load_test_config()
89
+ return config.get('settings', {}).get('skip_integration_tests_without_token', True)
90
+
91
+
92
+ def should_use_real_github_api() -> bool:
93
+ """
94
+ Check if tests should use real GitHub API calls.
95
+
96
+ Returns:
97
+ True if real GitHub API calls should be used
98
+ """
99
+ config = load_test_config()
100
+ return config.get('settings', {}).get('use_real_github_api', False)
101
+
102
+
103
+ def should_mock_network_calls() -> bool:
104
+ """
105
+ Check if network calls should be mocked by default.
106
+
107
+ Returns:
108
+ True if network calls should be mocked
109
+ """
110
+ config = load_test_config()
111
+ return config.get('settings', {}).get('mock_network_calls', True)
112
+
113
+
114
+ # Convenience function for backwards compatibility
115
+ def get_test_github_repo() -> str:
116
+ """
117
+ Get the test GitHub repository URL.
118
+ Alias for get_test_repo_url() for backwards compatibility.
119
+
120
+ Returns:
121
+ The configured test repository URL
122
+ """
123
+ return get_test_repo_url()
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  from datetime import datetime, timezone
5
6
  from pathlib import Path
6
7
  import threading
@@ -11,7 +12,17 @@ class LogBus:
11
12
  """Simple JSONL logging service."""
12
13
 
13
14
  def __init__(self, log_dir: str | Path | None = None) -> None:
14
- self.log_dir = Path(log_dir) if log_dir else Path(__file__).resolve().parents[3] / "logs"
15
+ if log_dir:
16
+ self.log_dir = Path(log_dir)
17
+ else:
18
+ # Check for workspace-based path first (for containers)
19
+ workspace_base = os.environ.get('WORKSPACE_BASE', '/workspace')
20
+ if os.path.exists(workspace_base):
21
+ self.log_dir = Path(workspace_base) / "logs"
22
+ else:
23
+ # Fallback to package-relative path
24
+ self.log_dir = Path(__file__).resolve().parents[3] / "logs"
25
+
15
26
  self.log_dir.mkdir(parents=True, exist_ok=True)
16
27
  timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
17
28
  self.log_path = self.log_dir / f"run-{timestamp}.jsonl"
@@ -0,0 +1,240 @@
1
+ # Issue Frontmatter Template for R2D Umbrella Issues
2
+ # =====================================================
3
+ # This template provides the structured metadata format for GitHub issues
4
+ # created by the DevOps-in-a-Box R2D Action.
5
+
6
+ # Standard R2D Issue Metadata
7
+ r2d_metadata:
8
+ # Core identifiers
9
+ run_key: "{run_key}"
10
+ repo_url: "{repo_url}"
11
+ commit_sha: "{commit_sha}"
12
+ job_name: "{job_name}"
13
+
14
+ # Timestamps
15
+ created_at: "{created_at}"
16
+ updated_at: "{updated_at}"
17
+
18
+ # Status tracking
19
+ status: "{status}"
20
+ wait_reason: "{wait_reason}"
21
+
22
+ # Associated resources
23
+ umbrella_issue_id: "{umbrella_issue_id}"
24
+ linked_pr: "{linked_pr}"
25
+ branch_name: "{branch_name}"
26
+ thread_id: "{thread_id}"
27
+
28
+ # Artifacts and outputs
29
+ artifacts_path: "{artifacts_path}"
30
+ terraform_summary: "{terraform_summary}"
31
+
32
+ # Retry information
33
+ retry_count: {retry_count}
34
+ predecessor_run: "{predecessor_run}"
35
+
36
+ # Agent status summary
37
+ agent_statuses:
38
+ supervisor:
39
+ status: "{supervisor_status}"
40
+ last_updated: "{supervisor_last_updated}"
41
+ git_agent:
42
+ status: "{git_agent_status}"
43
+ last_updated: "{git_agent_last_updated}"
44
+ terraform_agent:
45
+ status: "{terraform_agent_status}"
46
+ last_updated: "{terraform_agent_last_updated}"
47
+ shell_agent:
48
+ status: "{shell_agent_status}"
49
+ last_updated: "{shell_agent_last_updated}"
50
+
51
+ # Issue Template Configuration
52
+ issue_template:
53
+ title: "🚀 R2D Deployment: {repo_name} ({short_sha})"
54
+ labels:
55
+ - "r2d-deployment"
56
+ - "automated"
57
+ - "infrastructure"
58
+ assignees:
59
+ - "github-copilot" # Auto-assign to GitHub Copilot for AI assistance
60
+
61
+ # Issue body sections
62
+ body_sections:
63
+ header:
64
+ title: "## 🤖 DevOps-in-a-Box: Automated R2D Deployment"
65
+ content: |
66
+ This is an automated deployment issue created by the DevOps-in-a-Box R2D Action.
67
+
68
+ **Repository:** {repo_url}
69
+ **Commit:** `{commit_sha}`
70
+ **Job:** {job_name}
71
+ **Started:** {created_at}
72
+
73
+ status:
74
+ title: "## 📊 Deployment Status"
75
+ content: |
76
+ - **Overall Status:** {status}
77
+ - **Current Phase:** {current_phase}
78
+ - **Progress:** {progress_percentage}%
79
+
80
+ {status_details}
81
+
82
+ agents:
83
+ title: "## 🤖 Agent Status"
84
+ content: |
85
+ | Agent | Status | Last Updated | Notes |
86
+ |-------|--------|--------------|-------|
87
+ | Supervisor | {supervisor_status} | {supervisor_last_updated} | {supervisor_notes} |
88
+ | Git Agent | {git_agent_status} | {git_agent_last_updated} | {git_agent_notes} |
89
+ | Terraform Agent | {terraform_agent_status} | {terraform_agent_last_updated} | {terraform_agent_notes} |
90
+ | Shell Agent | {shell_agent_status} | {shell_agent_last_updated} | {shell_agent_notes} |
91
+
92
+ resources:
93
+ title: "## 🔗 Related Resources"
94
+ content: |
95
+ - **Branch:** {branch_name}
96
+ - **Thread ID:** {thread_id}
97
+ - **Artifacts:** {artifacts_path}
98
+ {linked_resources}
99
+
100
+ terraform:
101
+ title: "## ⚡ Terraform Operations"
102
+ content: |
103
+ ```
104
+ {terraform_summary}
105
+ ```
106
+
107
+ logs:
108
+ title: "## 📋 Deployment Logs"
109
+ content: |
110
+ <details>
111
+ <summary>Click to view detailed logs</summary>
112
+
113
+ ```
114
+ {deployment_logs}
115
+ ```
116
+ </details>
117
+
118
+ commands:
119
+ title: "## 🔧 Available Commands"
120
+ content: |
121
+ You can control this deployment by commenting on this issue:
122
+
123
+ - `retry` - Retry the current step
124
+ - `cancel` - Cancel the deployment
125
+ - `status` - Get current status
126
+ - `logs` - Show recent logs
127
+ - `help` - Show all available commands
128
+
129
+ footer:
130
+ title: "## 🤖 Automation Info"
131
+ content: |
132
+ This issue is managed by the DevOps-in-a-Box R2D Action.
133
+
134
+ **Run Key:** `{run_key}`
135
+ **Retry Count:** {retry_count}
136
+ **Predecessor:** {predecessor_run}
137
+
138
+ ---
139
+ *Powered by [DevOps-in-a-Box](https://github.com/amartyamandal/diagram-to-iac)*
140
+
141
+ # Status messages for different phases
142
+ status_messages:
143
+ created: "🆕 Deployment created and queued"
144
+ in_progress: "🚀 Deployment in progress"
145
+ waiting_for_pat: "⏳ Waiting for Personal Access Token configuration"
146
+ waiting_for_pr: "🔄 Waiting for Pull Request review and merge"
147
+ completed: "✅ Deployment completed successfully"
148
+ failed: "❌ Deployment failed"
149
+ cancelled: "🛑 Deployment cancelled"
150
+
151
+ # Progress indicators
152
+ progress_indicators:
153
+ created: 10
154
+ in_progress: 50
155
+ waiting_for_pat: 30
156
+ waiting_for_pr: 80
157
+ completed: 100
158
+ failed: 0
159
+ cancelled: 0
160
+
161
+ # Command responses
162
+ command_responses:
163
+ retry: |
164
+ 🔄 **Retry Requested**
165
+
166
+ The deployment will be retried from the current step.
167
+ This may take a few minutes to start.
168
+
169
+ cancel: |
170
+ 🛑 **Cancellation Requested**
171
+
172
+ The deployment has been marked for cancellation.
173
+ Any running operations will be stopped gracefully.
174
+
175
+ status: |
176
+ 📊 **Current Status**
177
+
178
+ - **Phase:** {current_phase}
179
+ - **Progress:** {progress_percentage}%
180
+ - **Last Updated:** {last_updated}
181
+
182
+ {detailed_status}
183
+
184
+ help: |
185
+ 🔧 **Available Commands**
186
+
187
+ Comment on this issue with any of these commands:
188
+
189
+ - `retry` - Retry the current step
190
+ - `cancel` - Cancel the deployment
191
+ - `status` - Get current status
192
+ - `logs` - Show recent logs
193
+ - `help` - Show this help message
194
+
195
+ **Note:** Only repository members can execute commands.
196
+
197
+ # Error templates
198
+ error_templates:
199
+ terraform_auth_error: |
200
+ ❌ **Terraform Authentication Error**
201
+
202
+ The deployment failed because Terraform Cloud authentication is not properly configured.
203
+
204
+ **Required Action:**
205
+ 1. Ensure your `TFE_TOKEN` secret is set in repository settings
206
+ 2. Verify the token has access to the required workspace
207
+ 3. Comment `retry` to restart the deployment
208
+
209
+ git_auth_error: |
210
+ ❌ **Git Authentication Error**
211
+
212
+ The deployment failed because Git operations require authentication.
213
+
214
+ **Required Action:**
215
+ 1. Ensure your `GITHUB_TOKEN` has sufficient permissions
216
+ 2. Verify repository access settings
217
+ 3. Comment `retry` to restart the deployment
218
+
219
+ missing_terraform_files: |
220
+ ❌ **Missing Terraform Files**
221
+
222
+ The deployment failed because no Terraform files were found in the repository.
223
+
224
+ **Required Action:**
225
+ 1. Ensure your repository contains `.tf` files
226
+ 2. Check that files are in the expected directory structure
227
+ 3. Update the repository and create a new issue
228
+
229
+ policy_violation: |
230
+ ❌ **Security Policy Violation**
231
+
232
+ The deployment was blocked by security policies.
233
+
234
+ **Details:**
235
+ {policy_details}
236
+
237
+ **Required Action:**
238
+ 1. Review and fix the security issues listed above
239
+ 2. Update your Terraform code
240
+ 3. Create a new deployment issue
@@ -3,8 +3,14 @@ import os
3
3
  from openai import OpenAI
4
4
  from anthropic import Anthropic
5
5
  import requests
6
- import google.generativeai as genai
7
- import googleapiclient.discovery
6
+ try:
7
+ import google.generativeai as genai
8
+ except ImportError:
9
+ genai = None
10
+ try:
11
+ import googleapiclient.discovery
12
+ except ImportError:
13
+ googleapiclient = None
8
14
  from concurrent.futures import ThreadPoolExecutor, TimeoutError
9
15
 
10
16
  # Import centralized configuration
@@ -45,6 +51,10 @@ def test_openai_api():
45
51
 
46
52
  def test_gemini_api():
47
53
  try:
54
+ if genai is None:
55
+ print("❌ Gemini API error: google-generativeai package not installed.")
56
+ return False
57
+
48
58
  google_api_key = os.environ.get("GOOGLE_API_KEY")
49
59
  if not google_api_key:
50
60
  print("❌ Gemini API error: GOOGLE_API_KEY environment variable not set.")
@@ -0,0 +1,102 @@
1
+ # Git Tools specific configuration
2
+ # Inherits common settings from src/diagram_to_iac/config.yaml
3
+ # Only tool-specific settings and overrides are defined here
4
+
5
+ git_executor:
6
+ # Default clone settings
7
+ default_clone_depth: 1
8
+
9
+ # Authentication failure detection patterns
10
+ auth_failure_patterns:
11
+ - "Authentication failed"
12
+ - "Permission denied"
13
+ - "Could not read from remote repository"
14
+ - "fatal: unable to access"
15
+ - "403 Forbidden"
16
+ - "401 Unauthorized"
17
+ - "Please make sure you have the correct access rights"
18
+
19
+ # Repository path generation
20
+ repo_path_template: "{workspace}/{repo_name}"
21
+ sanitize_repo_names: true
22
+
23
+ # Workspace cleanup configuration
24
+ auto_cleanup_existing_repos: true # Automatically remove existing repos before cloning
25
+ cleanup_safety_check: true # Verify path is within workspace before removal
26
+ backup_existing_repos: false # Create backup before removal (future feature)
27
+
28
+ # Git-specific logging configuration
29
+ enable_detailed_logging: true
30
+ log_git_commands: true
31
+ log_auth_failures: true
32
+
33
+ # Memory integration
34
+ store_operations_in_memory: true
35
+ store_command_output: true
36
+
37
+ # GitHub CLI configuration
38
+ github_cli:
39
+ default_timeout: 60 # GitHub CLI operations timeout
40
+ require_auth_check: true
41
+ auth_failure_patterns:
42
+ - "authentication failed"
43
+ - "bad credentials"
44
+ - "token does not have permission"
45
+ - "Must have admin rights to Repository"
46
+ - "HTTP 401"
47
+ - "HTTP 403"
48
+ issue_creation_patterns:
49
+ - "title is required"
50
+ - "body is required"
51
+ - "repository not found"
52
+ success_patterns:
53
+ - "https://github.com/"
54
+ - "/issues/"
55
+
56
+ # Error messages following our pattern
57
+ error_messages:
58
+ invalid_repo_url: "Git executor: Invalid repository URL '{repo_url}'"
59
+ workspace_not_accessible: "Git executor: Workspace directory '{workspace}' is not accessible"
60
+ clone_failed: "Git executor: Failed to clone repository '{repo_url}'"
61
+ auth_failed: "Git executor: Authentication failed for repository '{repo_url}'"
62
+ timeout_exceeded: "Git executor: Git operation timed out after {timeout} seconds"
63
+ shell_executor_error: "Git executor: Shell executor error: {error}"
64
+ repo_already_exists: "Git executor: Repository '{repo_name}' already exists in workspace"
65
+ repo_cleanup_started: "Git executor: Cleaning up existing repository '{repo_name}' at '{repo_path}'"
66
+ repo_cleanup_completed: "Git executor: Successfully cleaned up existing repository '{repo_name}'"
67
+ repo_cleanup_failed: "Git executor: Failed to clean up existing repository '{repo_name}': {error}"
68
+ cleanup_safety_violation: "Git executor: Repository path '{repo_path}' is outside workspace - cleanup denied for security"
69
+
70
+ # GitHub CLI error messages
71
+ gh_auth_failed: "GitHub CLI: Authentication failed - please check GITHUB_TOKEN"
72
+ gh_repo_not_found: "GitHub CLI: Repository '{repo_url}' not found or access denied"
73
+ gh_issue_creation_failed: "GitHub CLI: Failed to create issue in '{repo_url}'"
74
+ gh_invalid_repo_format: "GitHub CLI: Invalid repository format '{repo_url}' - expected owner/repo"
75
+ gh_command_failed: "GitHub CLI: Command failed with exit code {exit_code}"
76
+
77
+ # Success messages following our pattern
78
+ success_messages:
79
+ clone_started: "Git executor: Starting clone of '{repo_url}'"
80
+ clone_completed: "Git executor: Successfully cloned '{repo_url}' to '{repo_path}' in {duration:.2f}s"
81
+ repo_path_resolved: "Git executor: Repository path resolved to '{repo_path}'"
82
+ repo_cleanup_success: "Git executor: Successfully cleaned up existing repository before cloning"
83
+
84
+ # GitHub CLI success messages
85
+ gh_issue_created: "GitHub CLI: Successfully created issue '{title}' in '{repo_url}'"
86
+ gh_auth_verified: "GitHub CLI: Authentication verified for GitHub operations"
87
+ gh_repo_accessible: "GitHub CLI: Repository '{repo_url}' is accessible"
88
+
89
+ # Status codes for structured responses
90
+ status_codes:
91
+ success: "SUCCESS"
92
+ auth_failed: "AUTH_FAILED"
93
+ error: "ERROR"
94
+ timeout: "TIMEOUT"
95
+ already_exists: "ALREADY_EXISTS"
96
+
97
+ # GitHub CLI status codes
98
+ gh_success: "GH_SUCCESS"
99
+ gh_auth_failed: "GH_AUTH_FAILED"
100
+ gh_repo_not_found: "GH_REPO_NOT_FOUND"
101
+ gh_permission_denied: "GH_PERMISSION_DENIED"
102
+ gh_error: "GH_ERROR"
@@ -37,44 +37,77 @@ except ImportError:
37
37
  # Path inside container where the encoded YAML is mounted (dev only)
38
38
  _YAML_PATH = pathlib.Path("/run/secrets.yaml")
39
39
 
40
- # Expected secrets based on secrets_example.yaml
41
- EXPECTED_SECRETS = [
42
- "DOCKERHUB_API_KEY",
43
- "DOCKERHUB_USERNAME",
44
- "TF_API_KEY",
45
- "PYPI_API_KEY",
46
- "OPENAI_API_KEY",
47
- "GOOGLE_API_KEY",
48
- "ANTHROPIC_API_KEY",
49
- "GROK_API_KEY",
50
- "REPO_API_KEY"
51
- ]
52
-
53
- # Required secrets that must be present (others are optional)
54
- REQUIRED_SECRETS = [
55
- "REPO_API_KEY" # GITHUB_TOKEN is required for repo operations
56
- ]
57
-
58
- # Optional AI API secrets (at least one should be present for AI functionality)
59
- AI_API_SECRETS = [
60
- "OPENAI_API_KEY",
61
- "GOOGLE_API_KEY",
62
- "ANTHROPIC_API_KEY",
63
- "GROK_API_KEY"
64
- ]
40
+ def _load_config_secrets():
41
+ """Load secret configuration from config.yaml."""
42
+ try:
43
+ # Try to load from config.yaml
44
+ config_path = pathlib.Path(__file__).parent.parent / "config.yaml"
45
+ if config_path.exists() and yaml:
46
+ config_data = yaml.safe_load(config_path.read_text())
47
+ security_config = config_data.get("security", {})
48
+
49
+ required_secrets = security_config.get("required_secrets", ["REPO_API_KEY"])
50
+ optional_secrets = security_config.get("optional_secrets", [])
51
+ secret_mappings = security_config.get("secret_mappings", {})
52
+
53
+ # Combine required and optional secrets
54
+ expected_secrets = required_secrets + optional_secrets
55
+
56
+ # Create full mapping (defaults to same name if not mapped)
57
+ full_mapping = {}
58
+ for secret in expected_secrets:
59
+ full_mapping[secret] = secret_mappings.get(secret, secret)
60
+
61
+ # AI API secrets (hardcoded as they're specific to AI functionality)
62
+ ai_secrets = [
63
+ "OPENAI_API_KEY",
64
+ "GOOGLE_API_KEY",
65
+ "ANTHROPIC_API_KEY",
66
+ "GROK_API_KEY"
67
+ ]
68
+
69
+ return expected_secrets, required_secrets, ai_secrets, full_mapping
70
+ except Exception as e:
71
+ print(f"⚠️ Warning: Could not load secret config from config.yaml: {e}")
72
+
73
+ # Fallback to hardcoded values
74
+ expected_secrets = [
75
+ "DOCKERHUB_API_KEY",
76
+ "DOCKERHUB_USERNAME",
77
+ "TF_API_KEY",
78
+ "PYPI_API_KEY",
79
+ "OPENAI_API_KEY",
80
+ "GOOGLE_API_KEY",
81
+ "ANTHROPIC_API_KEY",
82
+ "GROK_API_KEY",
83
+ "REPO_API_KEY"
84
+ ]
85
+
86
+ required_secrets = ["REPO_API_KEY"]
87
+
88
+ ai_secrets = [
89
+ "OPENAI_API_KEY",
90
+ "GOOGLE_API_KEY",
91
+ "ANTHROPIC_API_KEY",
92
+ "GROK_API_KEY"
93
+ ]
94
+
95
+ secret_mapping = {
96
+ "REPO_API_KEY": "GITHUB_TOKEN",
97
+ "TF_API_KEY": "TFE_TOKEN",
98
+ "DOCKERHUB_API_KEY": "DOCKERHUB_API_KEY",
99
+ "DOCKERHUB_USERNAME": "DOCKERHUB_USERNAME",
100
+ "PYPI_API_KEY": "PYPI_API_KEY",
101
+ "OPENAI_API_KEY": "OPENAI_API_KEY",
102
+ "GOOGLE_API_KEY": "GOOGLE_API_KEY",
103
+ "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY",
104
+ "GROK_API_KEY": "GROK_API_KEY"
105
+ }
106
+
107
+ return expected_secrets, required_secrets, ai_secrets, secret_mapping
65
108
 
66
- # Map internal secret names to environment variable names
67
- SECRET_ENV_MAPPING = {
68
- "REPO_API_KEY": "GITHUB_TOKEN",
69
- "TF_API_KEY": "TFE_TOKEN",
70
- "DOCKERHUB_API_KEY": "DOCKERHUB_API_KEY",
71
- "DOCKERHUB_USERNAME": "DOCKERHUB_USERNAME",
72
- "PYPI_API_KEY": "PYPI_API_KEY",
73
- "OPENAI_API_KEY": "OPENAI_API_KEY",
74
- "GOOGLE_API_KEY": "GOOGLE_API_KEY",
75
- "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY",
76
- "GROK_API_KEY": "GROK_API_KEY"
77
- }
109
+ # Load configuration at module level
110
+ EXPECTED_SECRETS, REQUIRED_SECRETS, AI_API_SECRETS, SECRET_ENV_MAPPING = _load_config_secrets()
78
111
 
79
112
 
80
113
  def _decode_b64(enc: str) -> str:
@@ -107,12 +140,35 @@ def _is_dev_environment() -> bool:
107
140
  )
108
141
 
109
142
 
143
+ def _is_ci_environment() -> bool:
144
+ """Check if running in CI environment."""
145
+ return os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true"
146
+
147
+
110
148
  def _get_env_secrets() -> Dict[str, Optional[str]]:
111
149
  """Get secrets from environment variables."""
112
150
  env_secrets = {}
151
+ is_ci = _is_ci_environment()
152
+
113
153
  for secret_key in EXPECTED_SECRETS:
114
154
  env_name = SECRET_ENV_MAPPING.get(secret_key, secret_key)
115
- raw_value = os.environ.get(env_name)
155
+ raw_value = None
156
+
157
+ if is_ci:
158
+ # In CI, check for _ENCODED variant first (since that's how they're provided)
159
+ encoded_env_name = f"{secret_key}_ENCODED" # Use secret_key directly for CI
160
+ raw_value = os.environ.get(encoded_env_name)
161
+
162
+ # If not found with secret_key, try with env_name
163
+ if raw_value is None:
164
+ encoded_env_name = f"{env_name}_ENCODED"
165
+ raw_value = os.environ.get(encoded_env_name)
166
+
167
+ # If still not found, try the direct environment variable name
168
+ if raw_value is None:
169
+ raw_value = os.environ.get(env_name)
170
+
171
+ # If we found a value, process it
116
172
  if raw_value:
117
173
  # Check if value is already decoded, use as-is; otherwise decode it
118
174
  if (secret_key == "TF_API_KEY" and ".atlasv1." in raw_value) or \
@@ -126,6 +182,7 @@ def _get_env_secrets() -> Dict[str, Optional[str]]:
126
182
  env_secrets[secret_key] = _decode_b64(raw_value)
127
183
  else:
128
184
  env_secrets[secret_key] = None
185
+
129
186
  return env_secrets
130
187
 
131
188