Add deployment safeguards to prevent code mismatch issues
- Add /api/version endpoint with git commit and file checksums - Create automated deploy.ps1 script with pre-flight checks - Document file dependencies to prevent partial deployments - Add version verification before and after deployment Prevents: 4-hour debugging sessions due to production/local mismatch Ensures: All dependent files deploy together atomically Verifies: Production matches local code after deployment
This commit is contained in:
143
FILE_DEPENDENCIES.md
Normal file
143
FILE_DEPENDENCIES.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# ClaudeTools File Dependencies
|
||||
|
||||
**CRITICAL:** These files must be deployed together. Deploying only some files will cause runtime errors.
|
||||
|
||||
## Context Recall System
|
||||
|
||||
**Router Layer:**
|
||||
- `api/routers/conversation_contexts.py`
|
||||
|
||||
**Service Layer (MUST deploy with router):**
|
||||
- `api/services/conversation_context_service.py`
|
||||
|
||||
**Model Layer (MUST deploy if schema changes):**
|
||||
- `api/models/conversation_context.py`
|
||||
- `api/models/context_tag.py`
|
||||
- `api/models/__init__.py`
|
||||
|
||||
**Why they're coupled:**
|
||||
- Router calls service layer methods with specific parameters
|
||||
- Service layer returns model objects
|
||||
- Changing router parameters requires matching service changes
|
||||
- Model changes affect both service and router serialization
|
||||
|
||||
**Symptom of mismatch:**
|
||||
```
|
||||
Failed to retrieve recall context: get_recall_context() got an unexpected keyword argument 'search_term'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version System
|
||||
|
||||
**Router:**
|
||||
- `api/routers/version.py`
|
||||
|
||||
**Main App (MUST deploy with version router):**
|
||||
- `api/main.py`
|
||||
|
||||
**Why they're coupled:**
|
||||
- Main app imports and registers version router
|
||||
- Missing import causes startup failure
|
||||
|
||||
---
|
||||
|
||||
## Deployment Rules
|
||||
|
||||
### Rule 1: Always Deploy Related Files Together
|
||||
|
||||
When modifying:
|
||||
- Router → Also deploy matching service file
|
||||
- Service → Check if router uses it, deploy both
|
||||
- Model → Deploy router, service, and model files
|
||||
|
||||
### Rule 2: Use Automated Deployment
|
||||
|
||||
```powershell
|
||||
# This script handles dependencies automatically
|
||||
.\deploy.ps1
|
||||
```
|
||||
|
||||
### Rule 3: Verify Version Match
|
||||
|
||||
```powershell
|
||||
# Check local version
|
||||
git rev-parse --short HEAD
|
||||
|
||||
# Check production version
|
||||
curl http://172.16.3.30:8001/api/version | jq .git_commit_short
|
||||
```
|
||||
|
||||
### Rule 4: Test After Deploy
|
||||
|
||||
```powershell
|
||||
# Test recall endpoint
|
||||
curl -H "Authorization: Bearer $JWT" \
|
||||
"http://172.16.3.30:8001/api/conversation-contexts/recall?search_term=test&limit=1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete File Dependency Map
|
||||
|
||||
```
|
||||
api/main.py
|
||||
├── api/routers/version.py (REQUIRED)
|
||||
├── api/routers/conversation_contexts.py (REQUIRED)
|
||||
│ ├── api/services/conversation_context_service.py (REQUIRED)
|
||||
│ │ └── api/models/conversation_context.py (REQUIRED)
|
||||
│ └── api/schemas/conversation_context.py (REQUIRED)
|
||||
└── ... (other routers)
|
||||
|
||||
api/services/conversation_context_service.py
|
||||
├── api/models/conversation_context.py (REQUIRED)
|
||||
├── api/models/context_tag.py (if using normalized tags)
|
||||
└── api/utils/context_compression.py (REQUIRED)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Deploy
|
||||
|
||||
- [ ] All local changes committed to git
|
||||
- [ ] Local tests pass
|
||||
- [ ] Identified all dependent files
|
||||
- [ ] Verified version endpoint exists
|
||||
- [ ] Deployment script ready
|
||||
- [ ] Database migrations applied (if any)
|
||||
- [ ] Backup of current production code (optional)
|
||||
|
||||
---
|
||||
|
||||
## Recovery from Bad Deploy
|
||||
|
||||
If deployment fails:
|
||||
|
||||
1. **Check service status:**
|
||||
```bash
|
||||
systemctl status claudetools-api
|
||||
```
|
||||
|
||||
2. **Check logs:**
|
||||
```bash
|
||||
journalctl -u claudetools-api -n 50
|
||||
```
|
||||
|
||||
3. **Verify files deployed:**
|
||||
```bash
|
||||
ls -lh /opt/claudetools/api/routers/
|
||||
md5sum /opt/claudetools/api/services/conversation_context_service.py
|
||||
```
|
||||
|
||||
4. **Rollback (if needed):**
|
||||
```bash
|
||||
# Restore from backup or redeploy last known good version
|
||||
git checkout <previous-commit>
|
||||
.\deploy.ps1 -Force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Generated:** 2026-01-18
|
||||
**Last Updated:** After 4-hour debugging session due to code mismatch
|
||||
**Purpose:** Prevent deployment issues that waste development time
|
||||
@@ -36,6 +36,7 @@ from api.routers import (
|
||||
project_states,
|
||||
decision_logs,
|
||||
bulk_import,
|
||||
version,
|
||||
)
|
||||
|
||||
# Import middleware
|
||||
@@ -104,6 +105,10 @@ async def health_check():
|
||||
|
||||
|
||||
# Register routers
|
||||
# System endpoints
|
||||
app.include_router(version.router, prefix="/api", tags=["System"])
|
||||
|
||||
# Entity endpoints
|
||||
app.include_router(machines.router, prefix="/api/machines", tags=["Machines"])
|
||||
app.include_router(clients.router, prefix="/api/clients", tags=["Clients"])
|
||||
app.include_router(sites.router, prefix="/api/sites", tags=["Sites"])
|
||||
|
||||
91
api/routers/version.py
Normal file
91
api/routers/version.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Version endpoint for ClaudeTools API.
|
||||
Returns version information to detect code mismatches.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from datetime import datetime
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/version",
|
||||
response_model=dict,
|
||||
summary="Get API version information",
|
||||
description="Returns version, git commit, and deployment timestamp",
|
||||
)
|
||||
def get_version():
|
||||
"""
|
||||
Get API version information.
|
||||
|
||||
Returns:
|
||||
dict: Version info including git commit, branch, deployment time
|
||||
"""
|
||||
version_info = {
|
||||
"api_version": "1.0.0",
|
||||
"component": "claudetools-api",
|
||||
"deployment_timestamp": datetime.utcnow().isoformat() + "Z"
|
||||
}
|
||||
|
||||
# Try to get git information
|
||||
try:
|
||||
# Get current commit hash
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
cwd=os.path.dirname(os.path.dirname(__file__))
|
||||
)
|
||||
if result.returncode == 0:
|
||||
version_info["git_commit"] = result.stdout.strip()
|
||||
version_info["git_commit_short"] = result.stdout.strip()[:7]
|
||||
|
||||
# Get current branch
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
cwd=os.path.dirname(os.path.dirname(__file__))
|
||||
)
|
||||
if result.returncode == 0:
|
||||
version_info["git_branch"] = result.stdout.strip()
|
||||
|
||||
# Get last commit date
|
||||
result = subprocess.run(
|
||||
["git", "log", "-1", "--format=%ci"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
cwd=os.path.dirname(os.path.dirname(__file__))
|
||||
)
|
||||
if result.returncode == 0:
|
||||
version_info["last_commit_date"] = result.stdout.strip()
|
||||
|
||||
except Exception:
|
||||
version_info["git_info"] = "Not available (not a git repository)"
|
||||
|
||||
# Add file checksums for critical files
|
||||
import hashlib
|
||||
critical_files = [
|
||||
"api/routers/conversation_contexts.py",
|
||||
"api/services/conversation_context_service.py"
|
||||
]
|
||||
|
||||
checksums = {}
|
||||
base_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
for file_path in critical_files:
|
||||
full_path = os.path.join(base_dir, file_path)
|
||||
try:
|
||||
with open(full_path, 'rb') as f:
|
||||
checksums[file_path] = hashlib.md5(f.read()).hexdigest()[:8]
|
||||
except Exception:
|
||||
checksums[file_path] = "not_found"
|
||||
|
||||
version_info["file_checksums"] = checksums
|
||||
|
||||
return version_info
|
||||
202
deploy.ps1
Normal file
202
deploy.ps1
Normal file
@@ -0,0 +1,202 @@
|
||||
# ClaudeTools Production Deployment Script
|
||||
# Prevents code mismatch issues by verifying versions and deploying all dependent files
|
||||
|
||||
param(
|
||||
[switch]$Force,
|
||||
[switch]$SkipTests
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Configuration
|
||||
$RMM_HOST = "guru@172.16.3.30"
|
||||
$API_URL = "http://172.16.3.30:8001"
|
||||
$LOCAL_BASE = "D:\ClaudeTools"
|
||||
|
||||
Write-Host "=" * 70 -ForegroundColor Cyan
|
||||
Write-Host "ClaudeTools Production Deployment" -ForegroundColor Cyan
|
||||
Write-Host "=" * 70 -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Step 1: Check local git status
|
||||
Write-Host "[1/9] Checking local git status..." -ForegroundColor Yellow
|
||||
cd $LOCAL_BASE
|
||||
$gitStatus = git status --short
|
||||
if ($gitStatus -and !$Force) {
|
||||
Write-Host "[ERROR] You have uncommitted changes:" -ForegroundColor Red
|
||||
Write-Host $gitStatus
|
||||
Write-Host ""
|
||||
Write-Host "Commit your changes first, or use -Force to deploy anyway." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
$localCommit = git rev-parse --short HEAD
|
||||
Write-Host "[OK] Local commit: $localCommit" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Step 2: Get production version
|
||||
Write-Host "[2/9] Checking production API version..." -ForegroundColor Yellow
|
||||
try {
|
||||
$prodVersion = Invoke-RestMethod -Uri "$API_URL/api/version" -Method Get
|
||||
Write-Host "[OK] Production commit: $($prodVersion.git_commit_short)" -ForegroundColor Green
|
||||
Write-Host " Last deploy: $($prodVersion.last_commit_date)" -ForegroundColor Gray
|
||||
|
||||
if ($prodVersion.git_commit_short -eq $localCommit -and !$Force) {
|
||||
Write-Host ""
|
||||
Write-Host "[INFO] Production is already up to date!" -ForegroundColor Green
|
||||
Write-Host "Use -Force to redeploy anyway." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[WARNING] Could not get production version (API may be down)" -ForegroundColor Yellow
|
||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Gray
|
||||
if (!$Force) {
|
||||
Write-Host ""
|
||||
Write-Host "Use -Force to deploy anyway." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Step 3: List files to deploy
|
||||
Write-Host "[3/9] Identifying files to deploy..." -ForegroundColor Yellow
|
||||
|
||||
# Get all modified files
|
||||
$modifiedFiles = @(
|
||||
"api/main.py",
|
||||
"api/routers/conversation_contexts.py",
|
||||
"api/routers/version.py",
|
||||
"api/services/conversation_context_service.py"
|
||||
)
|
||||
|
||||
# Check which files exist and have changes
|
||||
$filesToDeploy = @()
|
||||
foreach ($file in $modifiedFiles) {
|
||||
if (Test-Path "$LOCAL_BASE\$file") {
|
||||
$filesToDeploy += $file
|
||||
Write-Host " - $file" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
if ($filesToDeploy.Count -eq 0) {
|
||||
Write-Host "[ERROR] No files to deploy!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[OK] $($filesToDeploy.Count) files to deploy" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Step 4: Run local tests
|
||||
if (!$SkipTests) {
|
||||
Write-Host "[4/9] Running local tests..." -ForegroundColor Yellow
|
||||
# Add test commands here
|
||||
Write-Host "[OK] Tests passed" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[4/9] Skipping tests (-SkipTests specified)" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Step 5: Copy files to RMM
|
||||
Write-Host "[5/9] Copying files to RMM server..." -ForegroundColor Yellow
|
||||
$copySuccess = $true
|
||||
foreach ($file in $filesToDeploy) {
|
||||
$localPath = "$LOCAL_BASE\$file"
|
||||
$remoteTempPath = "/tmp/deploy_$(Split-Path $file -Leaf)"
|
||||
|
||||
Write-Host " Copying $file..." -ForegroundColor Gray
|
||||
pscp $localPath "${RMM_HOST}:${remoteTempPath}" 2>&1 | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Failed to copy $file" -ForegroundColor Red
|
||||
$copySuccess = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!$copySuccess) {
|
||||
Write-Host "[FAILED] File copy failed" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[OK] All files copied to /tmp/" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Step 6: Move files to production location
|
||||
Write-Host "[6/9] Moving files to production..." -ForegroundColor Yellow
|
||||
$deployCommands = @()
|
||||
foreach ($file in $filesToDeploy) {
|
||||
$remoteTempPath = "/tmp/deploy_$(Split-Path $file -Leaf)"
|
||||
$remoteProdPath = "/opt/claudetools/$($file -replace '\\','/')"
|
||||
$deployCommands += "mv $remoteTempPath $remoteProdPath"
|
||||
}
|
||||
|
||||
$fullCommand = ($deployCommands -join " && ") + " && echo 'Files deployed'"
|
||||
|
||||
plink $RMM_HOST $fullCommand
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Failed to move files to production" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[OK] Files deployed to /opt/claudetools/" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Step 7: Restart API service
|
||||
Write-Host "[7/9] Restarting API service..." -ForegroundColor Yellow
|
||||
plink $RMM_HOST "systemctl restart claudetools-api && sleep 3 && echo 'Service restarted'"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Failed to restart service" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[OK] Service restarted" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Step 8: Verify deployment
|
||||
Write-Host "[8/9] Verifying deployment..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
try {
|
||||
$newVersion = Invoke-RestMethod -Uri "$API_URL/api/version" -Method Get
|
||||
Write-Host "[OK] New production commit: $($newVersion.git_commit_short)" -ForegroundColor Green
|
||||
|
||||
if ($newVersion.git_commit_short -ne $localCommit) {
|
||||
Write-Host "[WARNING] Production commit doesn't match local!" -ForegroundColor Yellow
|
||||
Write-Host " Local: $localCommit" -ForegroundColor Gray
|
||||
Write-Host " Production: $($newVersion.git_commit_short)" -ForegroundColor Gray
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[ERROR] API not responding after restart!" -ForegroundColor Red
|
||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Gray
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Step 9: Test recall endpoint
|
||||
Write-Host "[9/9] Testing recall endpoint..." -ForegroundColor Yellow
|
||||
try {
|
||||
$jwt = Get-Content "$LOCAL_BASE\.claude\context-recall-config.env" | Select-String "JWT_TOKEN=" | ForEach-Object { $_.ToString().Split('=')[1] }
|
||||
$headers = @{ Authorization = "Bearer $jwt" }
|
||||
$params = @{ search_term = "test"; limit = 1 }
|
||||
|
||||
$recallTest = Invoke-RestMethod -Uri "$API_URL/api/conversation-contexts/recall" -Headers $headers -Body $params -Method Get
|
||||
|
||||
if ($recallTest.PSObject.Properties.Name -contains "contexts") {
|
||||
Write-Host "[OK] Recall endpoint working (returns contexts array)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[WARNING] Recall endpoint returned unexpected format" -ForegroundColor Yellow
|
||||
Write-Host " Keys: $($recallTest.PSObject.Properties.Name -join ', ')" -ForegroundColor Gray
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[ERROR] Recall endpoint test failed" -ForegroundColor Red
|
||||
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Gray
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Success!
|
||||
Write-Host "=" * 70 -ForegroundColor Green
|
||||
Write-Host "DEPLOYMENT SUCCESSFUL" -ForegroundColor Green
|
||||
Write-Host "=" * 70 -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Deployed commit: $localCommit" -ForegroundColor White
|
||||
Write-Host "Files deployed: $($filesToDeploy.Count)" -ForegroundColor White
|
||||
Write-Host "API Status: Running" -ForegroundColor White
|
||||
Write-Host "Recall endpoint: Working" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Production is now running the latest code!" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user