Skip to content
Open
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
132 changes: 131 additions & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
name: Build and Push Docker image

# CalVer Release Workflow
#
# Automatically creates a CalVer release on every push to main.
# Version format: YYYY.MM.DD (e.g., 2026.01.16)
# If multiple releases happen on the same day, adds sequence: YYYY.MM.DD.2, YYYY.MM.DD.3, etc.
#
# Docker tags created:
# - CalVer tag (e.g., 2026.01.16)
# - Branch name (e.g., main)
# - Git short hash (e.g., main-a1b2c3d)
# - latest (for main branch only)

on:
push:
branches: [main, release-test]
Expand All @@ -9,12 +21,69 @@ jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
contents: write # Need write for creating tags
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for tags

- name: Generate CalVer version
id: calver
run: |
# Get today's date in YYYY.MM.DD format
TODAY=$(date +"%Y.%m.%d")

# Get all existing tags for today
EXISTING_TAGS=$(git tag -l "${TODAY}*" | sort -V)

if [ -z "$EXISTING_TAGS" ]; then
# No tags for today, use base date
VERSION="${TODAY}"
else
# Find the highest sequence number
LAST_TAG=$(echo "$EXISTING_TAGS" | tail -1)

if [[ "$LAST_TAG" == "$TODAY" ]]; then
# First tag was just the date, next is .2
VERSION="${TODAY}.2"
elif [[ "$LAST_TAG" =~ ^${TODAY}\.([0-9]+)$ ]]; then
# Extract sequence number and increment
SEQ="${BASH_REMATCH[1]}"
NEXT_SEQ=$((SEQ + 1))
VERSION="${TODAY}.${NEXT_SEQ}"
else
# Fallback
VERSION="${TODAY}.2"
fi
fi

echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Generated CalVer version: ${VERSION}"

- name: Get git commit info
id: git
run: |
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "full_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "build_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT

- name: Create git tag
if: github.ref == 'refs/heads/main'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Check if tag already exists
if git rev-parse "${{ steps.calver.outputs.version }}" >/dev/null 2>&1; then
echo "Tag ${{ steps.calver.outputs.version }} already exists, skipping"
else
git tag -a "${{ steps.calver.outputs.version }}" -m "Release ${{ steps.calver.outputs.version }}"
git push origin "${{ steps.calver.outputs.version }}"
echo "Created and pushed tag ${{ steps.calver.outputs.version }}"
fi

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand All @@ -33,11 +102,19 @@ jobs:
images: |
public.ecr.aws/r4g1k2s3/vcon-dev/vcon-server
tags: |
# CalVer tag (e.g., 2026.01.16)
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
# Branch name
type=ref,event=branch
# PR number
type=ref,event=pr
# Semver tags (for manual v* tags)
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# Git sha with branch prefix
type=sha,prefix={{branch}}-
# Latest tag for main branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
Expand All @@ -50,3 +127,56 @@ jobs:
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VCON_SERVER_VERSION=${{ steps.calver.outputs.version }}
VCON_SERVER_GIT_COMMIT=${{ steps.git.outputs.short_sha }}
VCON_SERVER_BUILD_TIME=${{ steps.git.outputs.build_time }}

- name: Create GitHub Release
if: github.ref == 'refs/heads/main'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.calver.outputs.version }}
name: Release ${{ steps.calver.outputs.version }}
body: |
## Release ${{ steps.calver.outputs.version }}

**Commit:** ${{ steps.git.outputs.short_sha }}
**Build Time:** ${{ steps.git.outputs.build_time }}

### Docker Images

Pull using CalVer:
```bash
docker pull public.ecr.aws/r4g1k2s3/vcon-dev/vcon-server:${{ steps.calver.outputs.version }}
```

Pull using git hash:
```bash
docker pull public.ecr.aws/r4g1k2s3/vcon-dev/vcon-server:main-${{ steps.git.outputs.short_sha }}
```

Pull latest:
```bash
docker pull public.ecr.aws/r4g1k2s3/vcon-dev/vcon-server:latest
```
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Summary
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| **Version** | ${{ steps.calver.outputs.version }} |" >> $GITHUB_STEP_SUMMARY
echo "| **Git Commit** | ${{ steps.git.outputs.short_sha }} |" >> $GITHUB_STEP_SUMMARY
echo "| **Build Time** | ${{ steps.git.outputs.build_time }} |" >> $GITHUB_STEP_SUMMARY
echo "| **Branch** | ${{ github.ref_name }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Tags" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
10 changes: 10 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
FROM python:3.12.2

# Build arguments for version information (injected by CI/CD)
ARG VCON_SERVER_VERSION=dev
ARG VCON_SERVER_GIT_COMMIT=unknown
ARG VCON_SERVER_BUILD_TIME=unknown

# Set version info as environment variables (available at runtime)
ENV VCON_SERVER_VERSION=${VCON_SERVER_VERSION}
ENV VCON_SERVER_GIT_COMMIT=${VCON_SERVER_GIT_COMMIT}
ENV VCON_SERVER_BUILD_TIME=${VCON_SERVER_BUILD_TIME}

RUN apt-get update && \
apt-get install -y libavdevice-dev ffmpeg

Expand Down
68 changes: 68 additions & 0 deletions server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
from lib.context_utils import store_context_async, extract_otel_trace_context
from lib.logging_utils import init_logger
import redis_mgr
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response

from version import get_version_info, get_version_string, get_version, get_git_commit

# OpenTelemetry trace context extraction is now in lib.context_utils
from settings import (
Expand Down Expand Up @@ -188,10 +193,73 @@ async def on_shutdown() -> None:
CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]
)


# Version header middleware - adds version info to every response
class VersionHeaderMiddleware(BaseHTTPMiddleware):
"""Middleware that adds version information to all API responses.

Adds the following headers to every response:
- X-Vcon-Server-Version: CalVer version (e.g., "2026.01.18")
- X-Vcon-Server-Commit: Git short hash (e.g., "a1b2c3d")

This makes it easy to identify which version of the server handled
a request, useful for debugging, monitoring, and APM tools.
"""

async def dispatch(self, request: StarletteRequest, call_next) -> Response:
response = await call_next(request)
response.headers["X-Vcon-Server-Version"] = get_version()
response.headers["X-Vcon-Server-Commit"] = get_git_commit()
return response


app.add_middleware(VersionHeaderMiddleware)

api_router = APIRouter()
external_router = APIRouter()


# Version endpoint - publicly accessible (no auth required)
@app.get(
"/version",
summary="Get server version",
description="Returns the server version information including CalVer version, git commit, and build time",
tags=["system"],
)
async def version_endpoint() -> JSONResponse:
"""Get the server version information.

Returns version details including:
- CalVer version (e.g., "2026.01.16")
- Git commit hash
- Build timestamp

This endpoint does not require authentication.

Returns:
JSONResponse containing version information
"""
return JSONResponse(content=get_version_info())


@app.get(
"/health",
summary="Health check",
description="Returns server health status and version",
tags=["system"],
)
async def health_check() -> JSONResponse:
"""Health check endpoint.

Returns:
JSONResponse with status and version info
"""
return JSONResponse(content={
"status": "healthy",
"version": get_version_info()
})


class Vcon(BaseModel):
"""Pydantic model representing a vCon (Voice Conversation) record.

Expand Down
15 changes: 15 additions & 0 deletions server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import redis_mgr

from config import get_config
from version import get_version_string, get_version_info
from dlq_utils import get_ingress_list_dlq_name
from lib.context_utils import retrieve_context, store_context_sync, extract_otel_trace_context
from lib.error_tracking import init_error_tracker
Expand Down Expand Up @@ -500,6 +501,20 @@ def main() -> None:
processing loop that pulls vCons from ingress queues and processes
them through their configured chains.
"""
# Print version information on startup
version_info = get_version_info()
logger.info(
"Starting %s",
get_version_string(),
extra={"version_info": version_info}
)
logger.info(
"Version: %s | Commit: %s | Built: %s",
version_info["version"],
version_info["git_commit"],
version_info["build_time"]
)

logger.info("Initializing vCon server")
global config
config = get_config()
Expand Down
84 changes: 84 additions & 0 deletions server/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Version information for the vCon server.

This module provides version information that is injected at Docker build time.
When running locally (outside Docker), it falls back to "dev" values.

Environment Variables (set during Docker build):
VCON_SERVER_VERSION: CalVer version (e.g., "2026.01.16.1")
VCON_SERVER_GIT_COMMIT: Git commit hash (short, e.g., "a1b2c3d")
VCON_SERVER_BUILD_TIME: ISO timestamp of when the image was built

Usage:
from version import get_version, get_version_info

# Get just the version string
version = get_version() # "2026.01.16.1" or "dev"

# Get full version info dict
info = get_version_info()
# {
# "version": "2026.01.16.1",
# "git_commit": "a1b2c3d",
# "build_time": "2026-01-16T10:30:00Z"
# }
"""

import os
from typing import Dict


# Version info from environment (injected at Docker build time)
VERSION = os.environ.get("VCON_SERVER_VERSION", "dev")
GIT_COMMIT = os.environ.get("VCON_SERVER_GIT_COMMIT", "unknown")
BUILD_TIME = os.environ.get("VCON_SERVER_BUILD_TIME", "unknown")


def get_version() -> str:
"""Get the current version string.

Returns:
The CalVer version string (e.g., "2026.01.16.1") or "dev" if not set.
"""
return VERSION


def get_git_commit() -> str:
"""Get the git commit hash.

Returns:
The short git commit hash (e.g., "a1b2c3d") or "unknown" if not set.
"""
return GIT_COMMIT


def get_build_time() -> str:
"""Get the build timestamp.

Returns:
ISO timestamp of when the image was built, or "unknown" if not set.
"""
return BUILD_TIME


def get_version_info() -> Dict[str, str]:
"""Get complete version information as a dictionary.

Returns:
Dictionary containing version, git_commit, and build_time.
"""
return {
"version": VERSION,
"git_commit": GIT_COMMIT,
"build_time": BUILD_TIME,
}


def get_version_string() -> str:
"""Get a formatted version string for display.

Returns:
Formatted string like "vCon Server v2026.01.16.1 (a1b2c3d)"
"""
if VERSION == "dev":
return "vCon Server (development)"
return f"vCon Server v{VERSION} ({GIT_COMMIT})"