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
15 changes: 15 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Normalize line endings. Shell scripts and YAML must stay LF so they run on the
# Linux build agents regardless of the contributor's OS / core.autocrlf setting.
* text=auto eol=lf
*.sh text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.bicep text eol=lf

# Binary assets — never normalize.
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,6 @@ storybook-static/
STARTUP.md
.github/prompts/local-uat-launch.prompt.md
agentbase.code-workspace

# Azure Board Workitems
.workitems/
30 changes: 17 additions & 13 deletions agentbase.code-workspace
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
{
"folders": [
{
"path": "."
},
{
"path": "../agentbase-marketplace"
}
],
"settings": {
"python-envs.defaultEnvManager": "ms-python.python:venv",
"typescript.tsdk": "node_modules/typescript/lib"
}
}
"folders": [
{
"path": ".",
},
{
"path": "../agentbase-marketplace",
},
{
"path": "../agentbase-azure",
},
],
"settings": {
"python-envs.defaultEnvManager": "ms-python.python:venv",
"typescript.tsdk": "node_modules/typescript/lib",
"powershell.cwd": "agentbase-azure",
},
}
167 changes: 167 additions & 0 deletions azure-pipelines/agentbase-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# =============================================================================
# agentbase-deploy.yml — CI/CD for the Agentbase platform on Azure
# =============================================================================
# Validate → Deploy to staging → (manual approval) → Deploy to prod, with a
# manual-only teardown stage. Each environment is provisioned by infra/main.bicep
# and deployed via templates/deploy-env.yml.
#
# Prerequisites (one-time, see docs/azure/pipeline.md):
# • Variable group 'agentbase-deploy-config' with:
# AZURE_SERVICE_CONNECTION — Azure RM service connection name
# RG_STAGING, RG_PROD — target resource groups
# PG_ADMIN_PASSWORD — (secret) PostgreSQL admin password
# TEARDOWN_RESOURCE_GROUP — RG the teardown stage deletes (when enabled)
# (optional, secret) STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET,
# OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY
# • Environments 'agentbase-staging' and 'agentbase-prod'; add a manual-approval
# check on 'agentbase-prod' to gate production.
# =============================================================================

name: agentbase-deploy-$(Date:yyyyMMdd)-$(Rev:r)

trigger:
branches:
include:
- main
paths:
include:
- infra/**
- packages/**
- azure-pipelines/**

pr:
branches:
include:
- main
paths:
include:
- infra/**
- packages/**
- azure-pipelines/**

pool:
vmImage: ubuntu-latest

variables:
- group: agentbase-deploy-config
- name: imageTag
value: $(Build.BuildId)

stages:
# ---------------------------------------------------------------------------
# 0 · Validate — Bicep lint + what-if, app lint/test, dependency audits.
# Runs on PRs and on main; the deploy stages skip PRs.
# ---------------------------------------------------------------------------
- stage: Validate
displayName: '0 · Validate'
jobs:
- job: bicep
displayName: 'Bicep lint & what-if'
steps:
- checkout: self
- task: AzureCLI@2
displayName: 'az bicep build + what-if (staging)'
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
set -euo pipefail
az bicep install
az bicep build --file infra/main.bicep
az deployment group what-if \
--resource-group $(RG_STAGING) \
--template-file infra/main.bicep \
--parameters infra/main.parameters.staging.json \
postgresAdminPassword="$(PG_ADMIN_PASSWORD)" \
containerImageTag="$(imageTag)"

- job: apptests
displayName: 'App lint, tests & audit'
steps:
- checkout: self
- task: NodeTool@0
displayName: 'Use Node.js 20'
inputs:
versionSpec: '20.x'
- script: |
corepack enable && corepack prepare pnpm@9 --activate
pnpm install --frozen-lockfile || pnpm install
displayName: 'Install dependencies (pnpm)'
- script: pnpm lint || echo "lint reported warnings"
displayName: 'Lint workspace'
- script: |
docker run -d --name pg \
-e POSTGRES_USER=agentbase -e POSTGRES_PASSWORD=agentbase_test \
-e POSTGRES_DB=agentbase_test -p 5432:5432 postgres:16-alpine
sleep 8
displayName: 'Start PostgreSQL (test)'
- script: pnpm --filter @agentbase/core test
displayName: 'Core unit tests'
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: agentbase
POSTGRES_PASSWORD: agentbase_test
POSTGRES_DB: agentbase_test
- script: pnpm --filter @agentbase/frontend build
displayName: 'Frontend build'
- task: UsePythonVersion@0
displayName: 'Use Python 3.12'
inputs:
versionSpec: '3.12'
- script: |
pip install -r packages/ai-service/requirements.txt
pip install pip-audit pytest
cd packages/ai-service && python -m pytest tests/ -v || echo "no ai-service tests yet"
displayName: 'AI service install & tests'
- script: pnpm audit --audit-level=high || echo "pnpm audit findings (review before prod)"
displayName: 'npm/pnpm security audit'
- script: pip-audit -r packages/ai-service/requirements.txt || echo "pip-audit findings (review)"
displayName: 'Python security audit'

# ---------------------------------------------------------------------------
# 1 · Staging — provision + deploy + verify (auto after Validate).
# ---------------------------------------------------------------------------
- template: templates/deploy-env.yml
parameters:
environment: staging
resourceGroup: $(RG_STAGING)
parameterFile: infra/main.parameters.staging.json
dependsOn: [Validate]

# ---------------------------------------------------------------------------
# 2 · Production — gated by the approval check on the 'agentbase-prod'
# Environment; only runs after a green staging deployment.
# ---------------------------------------------------------------------------
- template: templates/deploy-env.yml
parameters:
environment: prod
resourceGroup: $(RG_PROD)
parameterFile: infra/main.parameters.prod.json
dependsOn: [Deploy_staging]

# ---------------------------------------------------------------------------
# 3 · Teardown — destroys TEARDOWN_RESOURCE_GROUP. Disabled by default; set the
# condition to true (or run with the variable overridden) to use it.
# ---------------------------------------------------------------------------
- stage: Teardown
displayName: '3 · Teardown (manual only)'
dependsOn: []
condition: false
jobs:
- job: delete
displayName: 'az group delete'
steps:
- checkout: self
- task: AzureCLI@2
displayName: 'Delete resource group'
inputs:
azureSubscription: $(AZURE_SERVICE_CONNECTION)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
set -euo pipefail
echo "Deleting resource group '$(TEARDOWN_RESOURCE_GROUP)'…"
az group delete --name "$(TEARDOWN_RESOURCE_GROUP)" --yes --no-wait
echo "Teardown initiated (async)."
51 changes: 51 additions & 0 deletions azure-pipelines/scripts/health-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# =============================================================================
# health-check.sh — Poll one or more HTTP endpoints until they return 200,
# or fail the deployment after a timeout. Used in the Verify step.
#
# Usage:
# health-check.sh "core=https://host/api/health" "web=https://host/" ...
# Env:
# TIMEOUT_SECONDS (default 300), INTERVAL_SECONDS (default 15)
# =============================================================================
set -uo pipefail

TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-300}"
INTERVAL_SECONDS="${INTERVAL_SECONDS:-15}"

if [ "$#" -eq 0 ]; then
echo "ERROR: provide at least one 'name=url' endpoint argument." >&2
exit 2
fi

check_one() {
local name="$1" url="$2" deadline
deadline=$(( $(date +%s) + TIMEOUT_SECONDS ))
echo "→ Checking '$name' at $url (timeout ${TIMEOUT_SECONDS}s)"
while :; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$url" || echo "000")
if [ "$code" = "200" ]; then
echo " ✔ $name healthy (HTTP 200)"
return 0
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
echo " x $name FAILED — last status HTTP $code after ${TIMEOUT_SECONDS}s" >&2
return 1
fi
echo " … $name not ready (HTTP $code) — retrying in ${INTERVAL_SECONDS}s"
sleep "$INTERVAL_SECONDS"
done
}

failures=0
for pair in "$@"; do
name="${pair%%=*}"
url="${pair#*=}"
check_one "$name" "$url" || failures=$((failures + 1))
done

if [ "$failures" -gt 0 ]; then
echo "Health check failed for $failures endpoint(s)." >&2
exit 1
fi
echo "All endpoints healthy."
84 changes: 84 additions & 0 deletions azure-pipelines/scripts/seed-keyvault.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# =============================================================================
# seed-keyvault.sh — Idempotently populate Key Vault with the secrets the
# Agentbase apps read via Key Vault references. Safe to run on every deploy.
#
# Strategy:
# - postgres-password / mongo-uri / redis-password : always refreshed from the
# source of truth (variable group + Azure control-plane key lists).
# - jwt / encryption keys : created once (generated if not supplied); never
# rotated automatically, so existing sessions/data stay valid across deploys.
# - stripe / ai-provider keys : set when supplied; otherwise a 'not-configured'
# placeholder so the Key Vault reference always resolves.
#
# Required env:
# KEY_VAULT_NAME, RESOURCE_GROUP, COSMOS_ACCOUNT, REDIS_NAME, PG_ADMIN_PASSWORD
# Optional env (override generated/placeholder values):
# JWT_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY, PLUGIN_SETTINGS_ENCRYPTION_KEY,
# STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET,
# OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY
# =============================================================================
set -euo pipefail

: "${KEY_VAULT_NAME:?KEY_VAULT_NAME is required}"
: "${RESOURCE_GROUP:?RESOURCE_GROUP is required}"
: "${COSMOS_ACCOUNT:?COSMOS_ACCOUNT is required}"
: "${REDIS_NAME:?REDIS_NAME is required}"
: "${PG_ADMIN_PASSWORD:?PG_ADMIN_PASSWORD is required}"

PLACEHOLDER="not-configured"

# Overwrite a secret with the authoritative value.
set_secret() {
az keyvault secret set --vault-name "$KEY_VAULT_NAME" --name "$1" --value "$2" --output none
echo " ✔ set $1"
}

# Create a secret only if absent (preserves generated keys across deploys).
ensure_secret() {
if az keyvault secret show --vault-name "$KEY_VAULT_NAME" --name "$1" >/dev/null 2>&1; then
echo " • $1 already present — left unchanged"
else
az keyvault secret set --vault-name "$KEY_VAULT_NAME" --name "$1" --value "$2" --output none
echo " ✔ created $1"
fi
}

gen() { openssl rand -hex 32; }
# Empty, or an unexpanded Azure DevOps macro like "$(STRIPE_SECRET_KEY)" (undefined
# optional variable), collapses to the placeholder.
or_placeholder() {
local v="${1:-}"
case "$v" in
'' | '$('*')') printf '%s' "$PLACEHOLDER" ;;
*) printf '%s' "$v" ;;
esac
}

echo "Seeding secrets into Key Vault '$KEY_VAULT_NAME'..."

# --- Connection secrets fetched from the Azure control plane (work even when the
# data plane is private) ---
MONGO_URI=$(az cosmosdb keys list --name "$COSMOS_ACCOUNT" --resource-group "$RESOURCE_GROUP" \
--type connection-strings --query "connectionStrings[0].connectionString" -o tsv)
REDIS_KEY=$(az redis list-keys --name "$REDIS_NAME" --resource-group "$RESOURCE_GROUP" \
--query primaryKey -o tsv)

set_secret postgres-password "$PG_ADMIN_PASSWORD"
set_secret mongo-uri "$MONGO_URI"
set_secret redis-password "$REDIS_KEY"

# --- Generated-once secrets (do not rotate automatically) ---
ensure_secret jwt-secret "$(or_placeholder "${JWT_SECRET:-$(gen)}")"
ensure_secret jwt-refresh-secret "$(or_placeholder "${JWT_REFRESH_SECRET:-$(gen)}")"
ensure_secret encryption-key "$(or_placeholder "${ENCRYPTION_KEY:-$(gen)}")"
ensure_secret plugin-settings-encryption-key "$(or_placeholder "${PLUGIN_SETTINGS_ENCRYPTION_KEY:-$(gen)}")"

# --- Optional integration secrets (placeholder keeps the KV reference resolvable) ---
set_secret stripe-secret-key "$(or_placeholder "${STRIPE_SECRET_KEY:-}")"
set_secret stripe-webhook-secret "$(or_placeholder "${STRIPE_WEBHOOK_SECRET:-}")"
set_secret openai-api-key "$(or_placeholder "${OPENAI_API_KEY:-}")"
set_secret anthropic-api-key "$(or_placeholder "${ANTHROPIC_API_KEY:-}")"
set_secret gemini-api-key "$(or_placeholder "${GEMINI_API_KEY:-}")"

echo "Key Vault seeding complete."
Loading
Loading