Skip to content

Commit 440e4a7

Browse files
author
Sai Shyam
committed
feat: add parse_deployment.py, unit tests, CI workflow, fix scoring consistency
1 parent b673948 commit 440e4a7

10 files changed

Lines changed: 773 additions & 269 deletions

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Run Tests
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.11"
22+
23+
- name: Install test dependencies
24+
run: pip install pytest
25+
26+
- name: Run validation tests
27+
run: python -m pytest tests/ -v
28+
29+
- name: Verify mocked mode runs (no API key required)
30+
run: |
31+
python main.py 1 && echo "Scenario 1 OK"
32+
python main.py 2 && echo "Scenario 2 OK"
33+
python main.py 3 && echo "Scenario 3 OK"
34+
35+
- name: Verify parse_deployment CLI (dry run)
36+
run: python parse_deployment.py --help

README.md

Lines changed: 135 additions & 183 deletions
Large diffs are not rendered by default.

main.py

Lines changed: 214 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
"""
2-
Salesforce DevOps AI Assistant
3-
--------------------------------
4-
Loads a selected DevOps scenario and prints the corresponding
5-
mocked AI analysis output. No external API calls are made.
2+
Salesforce Deployment Failure Analyser
3+
---------------------------------------
4+
Analyses Salesforce DevOps metrics and returns a structured risk assessment.
5+
6+
Modes:
7+
python main.py Interactive menu (mocked)
8+
python main.py 1 | 2 | 3 Preset scenario (mocked)
9+
python main.py 1 --live Preset scenario via Claude API
10+
python main.py --input path/to/data.json --live Custom JSON via Claude API
611
"""
712

13+
import argparse
814
import json
915
import os
1016
import sys
1117

12-
# ── Scenario registry ────────────────────────────────────────────────────────
18+
# ── Scenario registry ────────────────────────────────────────────────────────
1319

1420
SCENARIOS = {
1521
"1": {
@@ -29,89 +35,249 @@
2935
},
3036
}
3137

32-
# ── Helpers ───────────────────────────────────────────────────────────────────
33-
3438
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
3539

40+
CLAUDE_PROMPT = """\
41+
You are a Salesforce DevOps engineer performing a deployment readiness review.
42+
43+
Analyse the input below and return ONLY a valid JSON object.
44+
No explanation, no markdown, no text outside the JSON.
45+
46+
Input:
47+
{input_json}
48+
49+
Rules:
50+
1. risk_score is an integer 0-10:
51+
0-2 = no blockers | 3-5 = quality issues, no blockers | 6-8 = at least one blocker | 9-10 = multiple blockers
52+
2. risk_level must match risk_score: 0-2 = Low, 3-5 = Medium, 6-10 = High
53+
3. Every risks[], root_causes[], recommendations[] item must name a specific component from the input.
54+
4. risks[].severity: one of Critical, High, Medium, Low
55+
5. recommendations[].priority: one of P0 - Immediate, P1 - High, P2 - Medium, P3 - Low
56+
6. Order recommendations highest to lowest priority.
57+
7. Root causes must state the technical reason, not restate the error.
58+
8. If code_coverage < 75, include a Critical risk and P0 recommendation citing the exact value.
59+
9. If failed_deployments is empty, do not fabricate deployment risks.
60+
10. If code_quality_issues.critical is 0, do not fabricate critical PMD risks.
61+
62+
Required output structure:
63+
{{
64+
"risk_score": <integer 0-10>,
65+
"risk_level": <"Low" | "Medium" | "High">,
66+
"risks": [{{"issue": "", "severity": "", "component": ""}}],
67+
"root_causes": [{{"cause": "", "component": ""}}],
68+
"recommendations": [{{"action": "", "priority": ""}}]
69+
}}"""
70+
71+
# ── Input validation ──────────────────────────────────────────────────────────
72+
73+
def validate_input(data: dict) -> list:
74+
"""Return a list of validation error strings. Empty = valid."""
75+
errors = []
76+
77+
required_top = {
78+
"code_coverage": (int, float),
79+
"failed_deployments": list,
80+
"code_quality_issues": dict,
81+
}
82+
for field, types in required_top.items():
83+
if field not in data:
84+
errors.append(f"Missing required field: '{field}'")
85+
elif not isinstance(data[field], types):
86+
errors.append(f"'{field}' must be {[t.__name__ for t in types]}, got {type(data[field]).__name__}")
87+
88+
if isinstance(data.get("code_coverage"), (int, float)):
89+
if not (0 <= data["code_coverage"] <= 100):
90+
errors.append(f"'code_coverage' must be 0–100, got {data['code_coverage']}")
91+
92+
if isinstance(data.get("code_quality_issues"), dict):
93+
for sub in ["pmd_violations", "critical"]:
94+
if sub not in data["code_quality_issues"]:
95+
errors.append(f"'code_quality_issues' missing field: '{sub}'")
96+
elif not isinstance(data["code_quality_issues"][sub], int):
97+
errors.append(f"'code_quality_issues.{sub}' must be int")
98+
99+
if isinstance(data.get("failed_deployments"), list):
100+
for i, item in enumerate(data["failed_deployments"]):
101+
for sub in ["component", "error", "failed_tests"]:
102+
if sub not in item:
103+
errors.append(f"'failed_deployments[{i}]' missing field: '{sub}'")
104+
105+
return errors
106+
107+
108+
# ── Claude API ────────────────────────────────────────────────────────────────
109+
110+
def call_claude(input_data: dict) -> dict:
111+
"""Send input to Claude and return parsed JSON output."""
112+
try:
113+
import anthropic
114+
except ImportError:
115+
print("\n ❌ 'anthropic' package not installed. Run: pip install anthropic\n")
116+
sys.exit(1)
117+
118+
api_key = os.environ.get("ANTHROPIC_API_KEY")
119+
if not api_key:
120+
print("\n ❌ ANTHROPIC_API_KEY environment variable is not set.")
121+
print(" Set it with: set ANTHROPIC_API_KEY=your_key_here (Windows)")
122+
print(" Or: export ANTHROPIC_API_KEY=your_key_here (Mac/Linux)\n")
123+
sys.exit(1)
124+
125+
client = anthropic.Anthropic(api_key=api_key)
126+
prompt = CLAUDE_PROMPT.format(input_json=json.dumps(input_data, indent=2))
127+
128+
print(" 🤖 Calling Claude API...")
129+
message = client.messages.create(
130+
model="claude-sonnet-4-5",
131+
max_tokens=1024,
132+
messages=[{"role": "user", "content": prompt}]
133+
)
134+
135+
raw = message.content[0].text.strip()
136+
try:
137+
return json.loads(raw)
138+
except json.JSONDecodeError:
139+
print(f"\n ❌ Claude returned invalid JSON:\n\n{raw}\n")
140+
sys.exit(1)
141+
142+
143+
# ── Display ───────────────────────────────────────────────────────────────────
36144

37145
def load_json(relative_path: str) -> dict:
38-
"""Load a JSON file relative to the project root."""
39-
full_path = os.path.join(BASE_DIR, relative_path)
40-
with open(full_path, "r", encoding="utf-8") as f:
146+
with open(os.path.join(BASE_DIR, relative_path), "r", encoding="utf-8") as f:
41147
return json.load(f)
42148

43149

44-
def print_section(title: str, items: list, fields: list[str]) -> None:
45-
"""Pretty-print a list section from the output JSON."""
46-
print(f"\n {'─' * 50}")
150+
def print_section(title: str, items: list, fields: list) -> None:
151+
print(f"\n {'─' * 54}")
47152
print(f" {title}")
48-
print(f" {'─' * 50}")
153+
print(f" {'─' * 54}")
49154
for i, item in enumerate(items, start=1):
50155
print(f" [{i}] " + " | ".join(f"{k}: {item.get(k, '')}" for k in fields))
51156

52157

53158
def display_output(output: dict) -> None:
54-
"""Render the AI analysis output in a readable format."""
55-
score = output["risk_score"]
56-
level = output["risk_level"]
57-
58-
# Risk badge
159+
score = output.get("risk_score", "?")
160+
level = output.get("risk_level", "Unknown")
59161
badge = {"High": "🔴", "Medium": "🟡", "Low": "🟢"}.get(level, "⚪")
60162

61-
print("\n" + "═" * 56)
62-
print(" SALESFORCE DEVOPS AI ASSISTANT — Analysis Result")
63-
print("═" * 56)
163+
print("\n" + "═" * 58)
164+
print(" SALESFORCE DEPLOYMENT FAILURE ANALYSER — Result")
165+
print("═" * 58)
64166
print(f" Risk Score : {score}/10")
65167
print(f" Risk Level : {badge} {level}")
66168

67-
print_section("RISKS IDENTIFIED", output["risks"],
169+
print_section("RISKS IDENTIFIED", output.get("risks", []),
68170
["severity", "component", "issue"])
69-
70-
print_section("ROOT CAUSES", output["root_causes"],
171+
print_section("ROOT CAUSES", output.get("root_causes", []),
71172
["component", "cause"])
72-
73-
print_section("RECOMMENDATIONS", output["recommendations"],
173+
print_section("RECOMMENDATIONS", output.get("recommendations", []),
74174
["priority", "action"])
75175

76-
print("\n" + "═" * 56 + "\n")
176+
print("\n" + "═" * 58 + "\n")
177+
178+
179+
# ── Argument parsing ──────────────────────────────────────────────────────────
180+
181+
def parse_args():
182+
parser = argparse.ArgumentParser(
183+
prog="main.py",
184+
description="Salesforce Deployment Failure Analyser — powered by Claude",
185+
formatter_class=argparse.RawDescriptionHelpFormatter,
186+
epilog="""
187+
examples:
188+
python main.py interactive menu (mocked)
189+
python main.py 1 preset failure scenario (mocked)
190+
python main.py 2 --live preset + live Claude API
191+
python main.py --input mydata.json --live custom JSON + Claude API
192+
"""
193+
)
194+
parser.add_argument(
195+
"scenario", nargs="?", choices=["1", "2", "3"],
196+
help="Preset scenario: 1=Failure, 2=Medium, 3=Healthy"
197+
)
198+
parser.add_argument(
199+
"--input", metavar="FILE",
200+
help="Path to a custom DevOps metrics JSON file"
201+
)
202+
parser.add_argument(
203+
"--live", action="store_true",
204+
help="Call Claude API (requires ANTHROPIC_API_KEY env var)"
205+
)
206+
return parser.parse_args()
77207

78208

79209
# ── Main ──────────────────────────────────────────────────────────────────────
80210

81211
def main() -> None:
82-
print("\n╔══════════════════════════════════════════════════════╗")
83-
print("║ Salesforce DevOps AI Assistant (Mocked Mode) ║")
84-
print("╚══════════════════════════════════════════════════════╝\n")
212+
args = parse_args()
85213

86-
# Allow scenario to be passed as a CLI argument
87-
if len(sys.argv) > 1:
88-
choice = sys.argv[1].strip()
89-
else:
90-
print(" Select a scenario to analyse:\n")
91-
for key, scenario in SCENARIOS.items():
92-
print(f" [{key}] {scenario['label']}")
214+
mode_label = "Live — Claude API" if args.live else "Mocked — pre-generated output"
215+
print("\n╔════════════════════════════════════════════════════════╗")
216+
print("║ Salesforce Deployment Failure Analyser ║")
217+
print(f"║ Mode: {mode_label:<47}║")
218+
print("╚════════════════════════════════════════════════════════╝\n")
219+
220+
# ── Custom input file ──────────────────────────────────────────────────
221+
if args.input:
222+
input_path = os.path.abspath(args.input)
223+
if not os.path.exists(input_path):
224+
print(f" ❌ File not found: {input_path}\n")
225+
sys.exit(1)
226+
227+
with open(input_path, "r", encoding="utf-8") as f:
228+
try:
229+
input_data = json.load(f)
230+
except json.JSONDecodeError as e:
231+
print(f" ❌ Invalid JSON in {input_path}:\n {e}\n")
232+
sys.exit(1)
233+
234+
errors = validate_input(input_data)
235+
if errors:
236+
print(" ❌ Input validation failed:")
237+
for err in errors:
238+
print(f" • {err}")
239+
print()
240+
sys.exit(1)
241+
242+
print(f" ✅ Input file: {input_path}")
243+
print("\n 📥 Data:")
244+
print(" " + json.dumps(input_data, indent=4).replace("\n", "\n "))
245+
246+
if not args.live:
247+
print("\n ⚠️ Custom input requires --live to call Claude.")
248+
print(" Run: python main.py --input <file> --live\n")
249+
sys.exit(0)
250+
251+
output_data = call_claude(input_data)
252+
display_output(output_data)
253+
return
254+
255+
# ── Preset scenario ────────────────────────────────────────────────────
256+
choice = args.scenario
257+
if not choice:
258+
print(" Select a scenario:\n")
259+
for key, s in SCENARIOS.items():
260+
print(f" [{key}] {s['label']}")
93261
print()
94262
choice = input(" Enter choice (1 / 2 / 3): ").strip()
95263

96264
if choice not in SCENARIOS:
97-
print(f"\n ❌ Invalid choice '{choice}'. Please enter 1, 2, or 3.\n")
265+
print(f"\n ❌ Invalid choice '{choice}'. Enter 1, 2, or 3.\n")
98266
sys.exit(1)
99267

100268
scenario = SCENARIOS[choice]
101-
print(f"\n ✅ Scenario : {scenario['label']}")
269+
print(f" ✅ Scenario: {scenario['label']}")
102270

103-
# Load input
104271
input_data = load_json(scenario["input"])
105-
print(f"\n 📥 Input Data:")
272+
print("\n 📥 Input:")
106273
print(" " + json.dumps(input_data, indent=4).replace("\n", "\n "))
107274

108-
# Simulate Claude processing (mocked — no API call)
109-
print("\n 🤖 Claude is analysing... (mocked)")
110-
111-
# Load pre-generated output
112-
output_data = load_json(scenario["output"])
275+
if args.live:
276+
output_data = call_claude(input_data)
277+
else:
278+
print("\n 🤖 Loading mocked Claude analysis...")
279+
output_data = load_json(scenario["output"])
113280

114-
# Display results
115281
display_output(output_data)
116282

117283

0 commit comments

Comments
 (0)