Merge pull request #116 from christianlouis/copilot/automate-status-metadata

Automate build metadata generation for /status endpoint
This commit is contained in:
Christian Krakau-Louis
2026-02-07 21:38:07 +01:00
committed by GitHub
11 changed files with 566 additions and 27 deletions
+5
View File
@@ -27,6 +27,11 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v3
- name: Generate Build Metadata
run: |
chmod +x scripts/generate_build_metadata.sh
./scripts/generate_build_metadata.sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+5
View File
@@ -13,6 +13,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Generate Build Metadata
run: |
chmod +x scripts/generate_build_metadata.sh
./scripts/generate_build_metadata.sh
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
+4
View File
@@ -194,3 +194,7 @@ cython_debug/
# PyPI configuration file
.pypirc
# Build metadata files - generated at build time
GIT_SHA
RUNTIME_INFO
+1 -1
View File
@@ -1 +1 @@
2025-04-11
2026-02-07
+6 -2
View File
@@ -18,11 +18,15 @@ COPY --from=builder /usr/local/bin /usr/local/bin
# Copy application code
COPY ./app /app/app
COPY ./VERSION /app/VERSION
COPY ./frontend /app/frontend
COPY ./BUILD_DATE /app/BUILD_DATE
COPY ./LICENSE /app/LICENSE
# Copy build metadata files (generated at build time)
COPY ./VERSION /app/VERSION
COPY ./BUILD_DATE /app/BUILD_DATE
COPY ./GIT_SHA /app/GIT_SHA
COPY ./RUNTIME_INFO /app/RUNTIME_INFO
# Create runtime_info directory
RUN mkdir -p /app/runtime_info
+1
View File
@@ -30,6 +30,7 @@ The project includes a **UI** for uploading and managing files, and an API docum
- [API Documentation](docs/API.md) - API reference
- [Deployment Guide](docs/DeploymentGuide.md) - How to deploy DocuElevate
- [Configuration Guide](docs/ConfigurationGuide.md) - Available configuration options
- [Build Metadata](docs/BuildMetadata.md) - Automated version and build information
- [Development Guide](CONTRIBUTING.md) - How to contribute to DocuElevate
- [Troubleshooting](docs/Troubleshooting.md) - Common issues and solutions
+28
View File
@@ -213,6 +213,34 @@ class Settings(BaseSettings):
# Default version if not found
return "0.3.2-dev"
@property
def git_sha(self) -> str:
"""Get Git commit SHA from environment or file."""
# First try to get from environment
env_sha = os.environ.get("GIT_COMMIT_SHA")
if env_sha:
return env_sha
# Then try to get from GIT_SHA file
git_sha_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "GIT_SHA")
if os.path.exists(git_sha_file):
with open(git_sha_file, "r") as f:
return f.read().strip()
# Default if not found
return "unknown"
@property
def runtime_info(self) -> str:
"""Get runtime information from file."""
runtime_info_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "RUNTIME_INFO")
if os.path.exists(runtime_info_file):
with open(runtime_info_file, "r") as f:
return f.read().strip()
# Return basic info if file not found
return f"Version: {self.version}\nBuild Date: {self.build_date}\nGit SHA: {self.git_sha}"
class Config:
env_file = ".env"
+7 -24
View File
@@ -5,7 +5,6 @@ from fastapi import Request
from fastapi.responses import JSONResponse
from datetime import datetime
import os
import subprocess
import logging
from app.views.base import APIRouter, templates, require_login, settings
@@ -46,42 +45,26 @@ async def status_dashboard(request: Request):
except Exception:
container_info['id'] = 'Unknown'
# Try to get Git commit SHA from runtime info
# Get Git commit SHA from settings
try:
# First check runtime info directory
if os.path.exists('/app/runtime_info/GIT_SHA'):
with open('/app/runtime_info/GIT_SHA', 'r') as f:
git_sha = f.read().strip()
# Then try environment variable
else:
git_sha = os.environ.get('GIT_COMMIT_SHA', '')
# If still not found, try the original file location
if not git_sha and os.path.exists('/.git-commit-sha'):
with open('/.git-commit-sha', 'r') as f:
git_sha = f.read().strip()
git_sha = settings.git_sha
container_info['git_sha'] = git_sha[:7] if git_sha and git_sha != 'unknown' else 'Unknown'
except Exception:
container_info['git_sha'] = 'Unknown'
# Try to get runtime information
try:
if os.path.exists('/app/runtime_info/RUNTIME_INFO'):
with open('/app/runtime_info/RUNTIME_INFO', 'r') as f:
container_info['runtime_info'] = f.read().strip()
container_info['runtime_info'] = settings.runtime_info
except Exception:
pass
else:
container_info['is_docker'] = False
# If not in Docker, try to get Git info directly
# If not in Docker, get Git info from settings
try:
git_sha = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
stderr=subprocess.DEVNULL,
text=True).strip()[:7]
container_info['git_sha'] = git_sha
except (subprocess.SubprocessError, FileNotFoundError):
git_sha = settings.git_sha
container_info['git_sha'] = git_sha[:7] if git_sha and git_sha != 'unknown' else 'Unknown'
except Exception:
container_info['git_sha'] = 'Unknown'
except Exception:
container_info = {'is_docker': False, 'id': 'Unknown', 'git_sha': 'Unknown'}
+328
View File
@@ -0,0 +1,328 @@
# Build Metadata Automation
## Overview
DocuElevate automatically generates and embeds build metadata (version, build date, Git commit SHA) into the application at build time. This metadata is displayed in the `/status` endpoint and helps track which version of the application is running in production.
## What is Automated
The following metadata is automatically generated and included in every build:
1. **Build Date** - The UTC date when the build was created (format: YYYY-MM-DD)
2. **Git Commit SHA** - The short Git commit hash (7 characters) of the code being built
3. **Runtime Information** - A comprehensive summary including version, dates, commit info, and more
4. **Application Version** - Read from the `VERSION` file (manually updated for releases)
## How It Works
### Build Time Generation
When the Docker image is built (locally or in CI/CD), the following happens:
1. **GitHub Actions runs the metadata script** (`scripts/generate_build_metadata.sh`) before building the Docker image
2. **The script generates three files:**
- `BUILD_DATE` - Contains the build date in YYYY-MM-DD format
- `GIT_SHA` - Contains the short Git commit hash
- `RUNTIME_INFO` - Contains detailed build information
3. **Docker copies these files** into the image during the build process
4. **The application reads the files** at runtime via `app/config.py` properties
### File Structure
```
DocuElevate/
├── scripts/
│ └── generate_build_metadata.sh # Script that generates metadata
├── VERSION # Manually maintained version number
├── BUILD_DATE # Generated at build time (gitignored)
├── GIT_SHA # Generated at build time (gitignored)
└── RUNTIME_INFO # Generated at build time (gitignored)
```
### Generated Files Format
**BUILD_DATE:**
```
2026-02-07
```
**GIT_SHA:**
```
6812d0d
```
**RUNTIME_INFO:**
```
DocuElevate Build Information
==============================
Version: 0.4.5-dev
Build Date: 2026-02-07
Git Commit: 6812d0d2c42a4782e78840b052f0e6da24ec8543
Git Short SHA: 6812d0d
Git Branch: main
Commit Date: 2026-02-07T19:32:04Z
Build Timestamp: 2026-02-07T19:34:10Z
==============================
```
## Configuration Properties
The `app/config.py` Settings class provides these properties for accessing build metadata:
### `settings.version` (property)
Returns the application version with the following priority:
1. `APP_VERSION` environment variable
2. Contents of `VERSION` file
3. Default: `"0.3.2-dev"`
### `settings.build_date` (property)
Returns the build date with the following priority:
1. `BUILD_DATE` environment variable
2. Contents of `BUILD_DATE` file
3. Default: `"Unknown build date"`
### `settings.git_sha` (property)
Returns the Git commit SHA with the following priority:
1. `GIT_COMMIT_SHA` environment variable
2. Contents of `GIT_SHA` file
3. Default: `"unknown"`
### `settings.runtime_info` (property)
Returns detailed runtime information:
1. Contents of `RUNTIME_INFO` file
2. Default: Basic info string with version, build date, and Git SHA
## Usage in Application
### In Python Code
```python
from app.config import settings
# Get version
version = settings.version # "0.4.5-dev"
# Get build date
build_date = settings.build_date # "2026-02-07"
# Get Git SHA
git_sha = settings.git_sha # "6812d0d"
# Get full runtime info
runtime_info = settings.runtime_info # Full multi-line string
```
### In Templates
The `/status` endpoint uses these properties to display metadata:
```python
# app/views/status.py
return templates.TemplateResponse(
"status_dashboard.html",
{
"app_version": settings.version,
"build_date": settings.build_date,
"container_info": {
"git_sha": settings.git_sha[:7],
"runtime_info": settings.runtime_info,
}
}
)
```
## GitHub Actions Integration
The build metadata is generated in two GitHub Actions workflows:
### docker-build.yaml
```yaml
- name: Generate Build Metadata
run: |
chmod +x scripts/generate_build_metadata.sh
./scripts/generate_build_metadata.sh
```
### docker-ci.yml
```yaml
- name: Generate Build Metadata
run: |
chmod +x scripts/generate_build_metadata.sh
./scripts/generate_build_metadata.sh
```
These steps run **before** the Docker build step, ensuring the metadata files exist when Docker copies them into the image.
## Local Development
### Manual Generation
To generate build metadata locally:
```bash
# Run the script
./scripts/generate_build_metadata.sh
# Output:
# Generating build metadata...
# ✓ BUILD_DATE: 2026-02-07
# ✓ GIT_SHA: 6812d0d
# ✓ VERSION: 0.4.5-dev
# ✓ RUNTIME_INFO generated
```
### Local Docker Build
When building Docker images locally:
```bash
# Generate metadata first
./scripts/generate_build_metadata.sh
# Then build the Docker image
docker build -t docuelevate:local .
# Or use docker-compose
docker-compose build
```
### Testing Without Docker
The application will still work without the generated files:
- `BUILD_DATE` will show "Unknown build date"
- `GIT_SHA` will show "unknown"
- The app will use defaults from `app/config.py`
## Environment Variable Override
You can override any metadata value using environment variables:
```bash
# Override version
export APP_VERSION="1.0.0-custom"
# Override build date
export BUILD_DATE="2026-01-15"
# Override Git SHA
export GIT_COMMIT_SHA="abc1234"
# Run the application
python -m uvicorn app.main:app
```
This is useful for:
- Custom builds
- Testing different versions
- Development environments
## Updating the Version
The `VERSION` file must be **manually updated** for releases:
```bash
# Update version for a new release
echo "0.5.0" > VERSION
# Commit the change
git add VERSION
git commit -m "Bump version to 0.5.0"
git tag v0.5.0
git push origin main --tags
```
The build metadata script will automatically include this version in all builds.
## Troubleshooting
### Problem: Build metadata shows "unknown"
**Solution:** Ensure the script runs before Docker build:
```bash
./scripts/generate_build_metadata.sh
docker build -t docuelevate .
```
### Problem: Git SHA is "unknown"
**Cause:** Building outside of a Git repository
**Solution:**
- Clone the repository properly with `.git` directory
- Or set `GIT_COMMIT_SHA` environment variable
### Problem: Build date is outdated
**Cause:** Using cached Docker layers
**Solution:**
```bash
# Rebuild without cache
docker build --no-cache -t docuelevate .
```
### Problem: Files not copied to Docker image
**Cause:** Files listed in `.dockerignore`
**Solution:**
- Check `.dockerignore` doesn't block `BUILD_DATE`, `GIT_SHA`, or `RUNTIME_INFO`
- The `VERSION` file should always be committed to git
## Best Practices
1. **Always run the script before building** - The CI/CD pipeline does this automatically
2. **Don't commit generated files** - `GIT_SHA` and `RUNTIME_INFO` are in `.gitignore`
3. **Update VERSION manually** - Only update for actual releases
4. **Use semantic versioning** - Follow `MAJOR.MINOR.PATCH` format
5. **Tag releases in Git** - Create Git tags for version releases
## CI/CD Pipeline Flow
```
┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions Workflow │
├─────────────────────────────────────────────────────────────┤
│ 1. Checkout Code │
│ 2. Generate Build Metadata (run script) │
│ - Creates BUILD_DATE │
│ - Creates GIT_SHA │
│ - Creates RUNTIME_INFO │
│ 3. Build Docker Image │
│ - Copies VERSION (from git) │
│ - Copies BUILD_DATE (generated) │
│ - Copies GIT_SHA (generated) │
│ - Copies RUNTIME_INFO (generated) │
│ 4. Push to Docker Hub / GHCR │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Running Container │
├─────────────────────────────────────────────────────────────┤
│ Application reads metadata from files in /app: │
│ - /app/VERSION │
│ - /app/BUILD_DATE │
│ - /app/GIT_SHA │
│ - /app/RUNTIME_INFO │
│ │
│ Displays in /status endpoint │
└─────────────────────────────────────────────────────────────┘
```
## Future Enhancements
Potential improvements to the build metadata system:
1. **Automated version bumping** - Automatically increment version based on commits
2. **Changelog generation** - Auto-generate changelog from Git history
3. **Build number tracking** - Track sequential build numbers
4. **Deployment tracking** - Record when/where each build was deployed
5. **Performance metrics** - Include build time, image size, etc.
## Related Documentation
- [Deployment Guide](./DeploymentGuide.md) - How to deploy DocuElevate
- [Configuration Guide](./ConfigurationGuide.md) - All configuration options
- [API Documentation](./API.md) - API endpoints including `/status`
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
#
# Generate build metadata files for DocuElevate
#
# This script generates metadata files that are read by the application
# at runtime to display version, build date, and Git commit information
# in the /status endpoint.
#
# Files generated:
# - BUILD_DATE: UTC timestamp of when the build was created
# - GIT_SHA: Short Git commit SHA (7 characters)
# - RUNTIME_INFO: Combined build information
#
# Usage:
# ./scripts/generate_build_metadata.sh
#
# The script should be run during Docker build or CI/CD pipeline.
set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Navigate to project root
cd "${PROJECT_ROOT}"
echo "Generating build metadata..."
# Generate BUILD_DATE in ISO 8601 format (UTC)
BUILD_DATE=$(date -u '+%Y-%m-%d')
echo "${BUILD_DATE}" > BUILD_DATE
echo "✓ BUILD_DATE: ${BUILD_DATE}"
# Generate GIT_SHA (short commit hash)
if git rev-parse --git-dir > /dev/null 2>&1; then
GIT_SHA=$(git rev-parse --short=7 HEAD)
echo "${GIT_SHA}" > GIT_SHA
echo "✓ GIT_SHA: ${GIT_SHA}"
# Get full commit SHA for reference
GIT_FULL_SHA=$(git rev-parse HEAD)
# Get commit date
GIT_COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)
# Get branch name (if available)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
else
echo "⚠ Warning: Not a git repository, using 'unknown' for Git metadata"
echo "unknown" > GIT_SHA
GIT_SHA="unknown"
GIT_FULL_SHA="unknown"
GIT_COMMIT_DATE="unknown"
GIT_BRANCH="unknown"
fi
# Read VERSION file
if [ -f "VERSION" ]; then
VERSION=$(cat VERSION | tr -d '\n')
echo "✓ VERSION: ${VERSION}"
else
echo "⚠ Warning: VERSION file not found"
VERSION="unknown"
fi
# Generate RUNTIME_INFO with combined metadata
cat > RUNTIME_INFO << EOF
DocuElevate Build Information
==============================
Version: ${VERSION}
Build Date: ${BUILD_DATE}
Git Commit: ${GIT_FULL_SHA}
Git Short SHA: ${GIT_SHA}
Git Branch: ${GIT_BRANCH}
Commit Date: ${GIT_COMMIT_DATE}
Build Timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')
==============================
EOF
echo "✓ RUNTIME_INFO generated"
echo ""
echo "Build metadata generation complete!"
echo ""
echo "Files created/updated:"
echo " - BUILD_DATE"
echo " - GIT_SHA"
echo " - RUNTIME_INFO"
echo ""
+92
View File
@@ -80,6 +80,98 @@ class TestConfigurationValidation:
assert config.session_secret is None
@pytest.mark.unit
class TestBuildMetadataConfiguration:
"""Tests for build metadata configuration."""
def test_version_from_environment(self, monkeypatch):
"""Test that version is read from APP_VERSION environment variable."""
monkeypatch.setenv("APP_VERSION", "1.2.3-test")
config = Settings(
database_url="sqlite:///test.db",
redis_url="redis://localhost:6379",
openai_api_key="test",
azure_ai_key="test",
azure_region="test",
azure_endpoint="https://test.example.com",
gotenberg_url="http://localhost:3000",
workdir="/tmp",
auth_enabled=False
)
assert config.version == "1.2.3-test"
def test_build_date_from_environment(self, monkeypatch):
"""Test that build_date is read from BUILD_DATE environment variable."""
monkeypatch.setenv("BUILD_DATE", "2026-01-15")
config = Settings(
database_url="sqlite:///test.db",
redis_url="redis://localhost:6379",
openai_api_key="test",
azure_ai_key="test",
azure_region="test",
azure_endpoint="https://test.example.com",
gotenberg_url="http://localhost:3000",
workdir="/tmp",
auth_enabled=False
)
assert config.build_date == "2026-01-15"
def test_git_sha_from_environment(self, monkeypatch):
"""Test that git_sha is read from GIT_COMMIT_SHA environment variable."""
monkeypatch.setenv("GIT_COMMIT_SHA", "abc1234")
config = Settings(
database_url="sqlite:///test.db",
redis_url="redis://localhost:6379",
openai_api_key="test",
azure_ai_key="test",
azure_region="test",
azure_endpoint="https://test.example.com",
gotenberg_url="http://localhost:3000",
workdir="/tmp",
auth_enabled=False
)
assert config.git_sha == "abc1234"
def test_git_sha_default(self, monkeypatch, tmp_path):
"""Test that git_sha defaults to 'unknown' when not set."""
# Mock the file system to ensure no GIT_SHA file exists
import app.config
monkeypatch.setattr(app.config.os.path, 'dirname', lambda x: str(tmp_path))
config = Settings(
database_url="sqlite:///test.db",
redis_url="redis://localhost:6379",
openai_api_key="test",
azure_ai_key="test",
azure_region="test",
azure_endpoint="https://test.example.com",
gotenberg_url="http://localhost:3000",
workdir="/tmp",
auth_enabled=False
)
# When no file or env var exists, should return "unknown"
assert config.git_sha == "unknown"
def test_runtime_info_property(self):
"""Test that runtime_info returns build information."""
config = Settings(
database_url="sqlite:///test.db",
redis_url="redis://localhost:6379",
openai_api_key="test",
azure_ai_key="test",
azure_region="test",
azure_endpoint="https://test.example.com",
gotenberg_url="http://localhost:3000",
workdir="/tmp",
auth_enabled=False
)
runtime_info = config.runtime_info
# Should contain version, build_date, and git_sha in some form
assert "Version:" in runtime_info or config.version in runtime_info
@pytest.mark.unit
class TestNotificationConfiguration:
"""Tests for notification configuration parsing."""