Skip to content
Merged
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
5 changes: 3 additions & 2 deletions infrastructure/k8s/production/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,16 @@ spec:
cpu: "500m"
livenessProbe:
httpGet:
path: /health
# The Next.js app's health endpoint (apps/web/src/app/api/route.ts)
path: /api
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
path: /api
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Expand Down
37 changes: 21 additions & 16 deletions scripts/deployment/one-click-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

set -euo pipefail

# All paths below are relative to the repository root
cd "$(dirname "${BASH_SOURCE[0]}")/../.."

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
Expand Down Expand Up @@ -63,15 +66,15 @@ if ! kubectl cluster-info &> /dev/null; then
fi
success "Kubernetes cluster is accessible"

# Check if required files exist
# Check if required files exist.
# Every entry must resolve from the repo root; this is enforced by
# tests/unit/test_one_click_deploy_precheck.py so the list cannot go stale.
REQUIRED_FILES=(
"Dockerfile.production"
"apps/web/Dockerfile"
"package.json"
"k8s/production/deployment.yaml"
"k8s/production/service.yaml"
"k8s/monitoring/monitoring.yaml"
"mcp_server.py"
"learning_app_processor.py"
"infrastructure/k8s/production/deployment.yaml"
"infrastructure/k8s/production/service.yaml"
"infrastructure/k8s/monitoring/monitoring.yaml"
)

for file in "${REQUIRED_FILES[@]}"; do
Expand All @@ -86,7 +89,7 @@ log "Step 2: Running Integration Tests"

if [[ -d "venv" ]]; then
source venv/bin/activate
if python3 tests/integration/test_runner.py; then
if python3 -m pytest tests/integration -v; then
success "Integration tests passed"
else
error "Integration tests failed. Please fix issues before deployment."
Expand All @@ -101,7 +104,9 @@ log "Step 3: Building Docker Image"
DOCKER_TAG="enhanced-framework:$(date +%Y%m%d-%H%M%S)"
LATEST_TAG="enhanced-framework:latest"

if docker build -f Dockerfile.production -t "$DOCKER_TAG" -t "$LATEST_TAG" .; then
# enhanced-framework is the Next.js frontend (apps/web); the Python backend is
# deployed separately as the mcp-server container in the production manifest.
if docker build -f apps/web/Dockerfile -t "$DOCKER_TAG" -t "$LATEST_TAG" .; then
success "Docker image built successfully: $DOCKER_TAG"
else
error "Docker image build failed"
Expand All @@ -111,21 +116,21 @@ fi
log "Step 4: Validating Kubernetes Manifests"

# Validate deployment manifest
if kubectl apply --dry-run=client -f k8s/production/deployment.yaml; then
if kubectl apply --dry-run=client -f infrastructure/k8s/production/deployment.yaml; then
success "Deployment manifest is valid"
else
error "Deployment manifest validation failed"
fi

# Validate service manifest
if kubectl apply --dry-run=client -f k8s/production/service.yaml; then
if kubectl apply --dry-run=client -f infrastructure/k8s/production/service.yaml; then
success "Service manifest is valid"
else
error "Service manifest validation failed"
fi

# Validate monitoring manifest
if kubectl apply --dry-run=client -f k8s/monitoring/monitoring.yaml; then
if kubectl apply --dry-run=client -f infrastructure/k8s/monitoring/monitoring.yaml; then
success "Monitoring manifest is valid"
else
error "Monitoring manifest validation failed"
Expand All @@ -144,14 +149,14 @@ fi
log "Step 6: Deploying to Kubernetes"

# Deploy application
if kubectl apply -f k8s/production/ -n "$NAMESPACE"; then
if kubectl apply -f infrastructure/k8s/production/ -n "$NAMESPACE"; then
success "Application deployed successfully"
else
error "Application deployment failed"
fi

# Deploy monitoring
if kubectl apply -f k8s/monitoring/ -n "$NAMESPACE"; then
if kubectl apply -f infrastructure/k8s/monitoring/ -n "$NAMESPACE"; then
success "Monitoring stack deployed successfully"
else
error "Monitoring deployment failed"
Expand Down Expand Up @@ -199,9 +204,9 @@ if [[ "$SERVICE_IP" == "localhost" ]]; then
kubectl port-forward -n "$NAMESPACE" service/"$DEPLOYMENT_NAME" 8080:80 &
PORT_FORWARD_PID=$!
sleep 5
HEALTH_URL="http://localhost:8080/health"
HEALTH_URL="http://localhost:8080/api"
else
HEALTH_URL="http://$SERVICE_IP/health"
HEALTH_URL="http://$SERVICE_IP/api"
fi

# Perform health check
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_one_click_deploy_precheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Guard against rot in scripts/deployment/one-click-deploy.sh.

The script's precheck aborts the whole deployment if any entry in its
REQUIRED_FILES array is missing, so a stale path makes the script
non-functional (see #1127, where two entries pointed at files that had
never existed and three more used pre-move ``k8s/...`` paths). This test
parses the array straight out of the script and asserts every entry
resolves from the repository root.
"""

from __future__ import annotations

import re
from pathlib import Path

PROJECT_ROOT = Path(__file__).parent.parent.parent
DEPLOY_SCRIPT = PROJECT_ROOT / "scripts" / "deployment" / "one-click-deploy.sh"


def _required_files(script_text: str) -> list[str]:
match = re.search(r"REQUIRED_FILES=\((.*?)\)", script_text, re.DOTALL)
assert match, "REQUIRED_FILES array not found in one-click-deploy.sh"
return re.findall(r'"([^"]+)"', match.group(1))


def test_required_files_all_exist():
assert DEPLOY_SCRIPT.exists(), f"{DEPLOY_SCRIPT} not found"

entries = _required_files(DEPLOY_SCRIPT.read_text())
assert entries, "REQUIRED_FILES is empty; the precheck validates nothing"

missing = [entry for entry in entries if not (PROJECT_ROOT / entry).is_file()]
assert not missing, (
"REQUIRED_FILES in one-click-deploy.sh lists paths that do not exist "
f"relative to the repo root: {missing}. The script exits on the first "
"missing entry, so every path must resolve."
)


def test_manifest_paths_in_script_exist():
"""Every ``-f <path>`` the script passes to kubectl/docker must resolve."""
script_text = DEPLOY_SCRIPT.read_text()
paths = re.findall(r"-f\s+((?:infrastructure|apps|k8s)/[\w./-]+)", script_text)
assert paths, "No manifest/Dockerfile paths found in one-click-deploy.sh"

missing = [p for p in paths if not (PROJECT_ROOT / p).exists()]
assert not missing, (
f"one-click-deploy.sh references nonexistent paths: {missing}"
)
Loading