improvements to release process.

This commit is contained in:
Dries Peeters
2025-09-19 09:00:02 +02:00
parent 42087d4212
commit a60aa3df58
7 changed files with 1589 additions and 1 deletions

251
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,251 @@
name: Continuous Integration
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
env:
PYTHON_VERSION: '3.11'
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install flake8 black isort mypy
- name: Run black (code formatting)
run: black --check --diff app/ migrations/ scripts/
- name: Run isort (import sorting)
run: isort --check-only --diff app/ migrations/ scripts/
- name: Run flake8 (linting)
run: flake8 app/ migrations/ scripts/ --max-line-length=88 --extend-ignore=E203,W503
- name: Run mypy (type checking)
run: mypy app/ --ignore-missing-imports
test-database-migrations:
runs-on: ubuntu-latest
strategy:
matrix:
db_type: [postgresql, sqlite]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Test PostgreSQL migrations
if: matrix.db_type == 'postgresql'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
run: |
echo "Testing PostgreSQL migrations..."
flask db upgrade
python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('✅ PostgreSQL migration successful')"
flask db downgrade base
flask db upgrade
echo "✅ PostgreSQL migration rollback/upgrade test passed"
- name: Test SQLite migrations
if: matrix.db_type == 'sqlite'
env:
DATABASE_URL: sqlite:///test.db
FLASK_APP: app.py
run: |
echo "Testing SQLite migrations..."
flask db upgrade
python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('✅ SQLite migration successful')"
flask db downgrade base
flask db upgrade
echo "✅ SQLite migration rollback/upgrade test passed"
test-docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Test Docker build
run: |
docker build -t timetracker-test:latest .
echo "✅ Docker build successful"
- name: Test Docker container startup
run: |
# Start container in background
docker run -d --name test-container -p 8080:8080 \
-e DATABASE_URL="sqlite:///test.db" \
timetracker-test:latest
# Wait for container to be ready
for i in {1..30}; do
if curl -f http://localhost:8080/_health >/dev/null 2>&1; then
echo "✅ Container health check passed"
break
fi
echo "Waiting for container to be ready... ($i/30)"
sleep 2
done
# Show container logs for debugging
docker logs test-container
# Stop container
docker stop test-container
docker rm test-container
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install security tools
run: |
pip install safety bandit
- name: Run safety (dependency vulnerability scan)
run: safety check --file requirements.txt
- name: Run bandit (security linting)
run: bandit -r app/ -f json -o bandit-report.json || true
- name: Upload security report
uses: actions/upload-artifact@v4
if: always()
with:
name: security-report
path: bandit-report.json
validate-version-management:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Test version manager script
run: |
python scripts/version-manager.py status
python scripts/version-manager.py suggest
- name: Validate version format
run: |
# Test various version formats
python -c "
import sys
sys.path.append('scripts')
from version_manager import VersionManager
vm = VersionManager()
test_versions = ['v1.2.3', '1.2.3', 'v1.2', 'build-123', 'rc1', 'beta1', 'alpha1']
for version in test_versions:
if vm.validate_version_format(version):
print(f'✅ {version} is valid')
else:
print(f'❌ {version} is invalid')
sys.exit(1)
"
create-pr-preview:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
needs: [lint-and-format, test-database-migrations, test-docker-build]
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes('CI Pipeline Status'));
const commentBody = \`## 🔍 CI Pipeline Status
**All checks passed!** ✅
**Completed Checks:**
- ✅ Code formatting and linting
- ✅ Database migration tests (PostgreSQL & SQLite)
- ✅ Docker build and startup test
- ✅ Security vulnerability scan
- ✅ Version management validation
**Ready for review and merge** 🚀
---
*This comment was automatically generated by the CI pipeline.*\`;
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}

View File

@@ -31,6 +31,8 @@ jobs:
include:
- name: amd64
platform: linux/amd64
- name: arm64
platform: linux/arm64
steps:
- name: Checkout repository

281
.github/workflows/migration-check.yml vendored Normal file
View File

@@ -0,0 +1,281 @@
name: Database Migration Validation
on:
pull_request:
paths:
- 'app/models/**'
- 'migrations/**'
- 'requirements.txt'
push:
branches: [ main ]
paths:
- 'app/models/**'
- 'migrations/**'
jobs:
validate-migrations:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Check for migration changes
id: migration_check
run: |
# Check if there are changes to models or migrations
if git diff --name-only HEAD~1 | grep -E "(app/models/|migrations/)" > /dev/null; then
echo "migration_changes=true" >> $GITHUB_OUTPUT
echo "📋 Migration-related changes detected"
else
echo "migration_changes=false" >> $GITHUB_OUTPUT
echo " No migration-related changes detected"
fi
- name: Validate migration consistency
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
run: |
echo "🔍 Validating migration consistency..."
# Initialize fresh database
flask db upgrade
# Generate a new migration from current models
flask db migrate -m "Test migration consistency" --rev-id test_consistency
# Check if the generated migration is empty (no changes needed)
MIGRATION_FILE=$(find migrations/versions -name "*test_consistency*.py" | head -1)
if [ -f "$MIGRATION_FILE" ]; then
# Check if migration has actual changes
if grep -q "op\." "$MIGRATION_FILE"; then
echo "❌ Migration inconsistency detected!"
echo "The database schema doesn't match the models."
echo "Generated migration file: $MIGRATION_FILE"
cat "$MIGRATION_FILE"
exit 1
else
echo "✅ Migration consistency validated - no schema drift detected"
# Clean up test migration
rm "$MIGRATION_FILE"
fi
else
echo "✅ No migration file generated - models are in sync"
fi
- name: Test migration rollback safety
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
run: |
echo "🔄 Testing migration rollback safety..."
# Get current migration
CURRENT_MIGRATION=$(flask db current)
echo "Current migration: $CURRENT_MIGRATION"
if [ -n "$CURRENT_MIGRATION" ] && [ "$CURRENT_MIGRATION" != "None" ]; then
# Try to rollback one step
echo "Testing rollback..."
flask db downgrade -1
# Try to upgrade back
echo "Testing re-upgrade..."
flask db upgrade
echo "✅ Migration rollback test passed"
else
echo " No migrations to test rollback on"
fi
- name: Test migration with sample data
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
run: |
echo "📊 Testing migration with sample data..."
# Create sample data
python -c "
from app import create_app, db
from app.models.user import User
from app.models.project import Project
import datetime
app = create_app()
with app.app_context():
# Create test user
user = User(
username='test_user',
email='test@example.com',
role='user'
)
user.set_password('test_password')
db.session.add(user)
# Create test project
project = Project(
name='Test Project',
description='Test project for migration validation',
user_id=1
)
db.session.add(project)
db.session.commit()
print('✅ Sample data created successfully')
"
# Verify data integrity after migration
python -c "
from app import create_app, db
from app.models.user import User
from app.models.project import Project
app = create_app()
with app.app_context():
user_count = User.query.count()
project_count = Project.query.count()
print(f'Users: {user_count}, Projects: {project_count}')
if user_count > 0 and project_count > 0:
print('✅ Data integrity verified after migration')
else:
print('❌ Data integrity check failed')
exit(1)
"
- name: Generate migration report
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
run: |
echo "📋 Generating migration report..."
# Get migration history
echo "## Migration History" > migration_report.md
echo "" >> migration_report.md
flask db history --verbose >> migration_report.md
# Get current schema info
echo "" >> migration_report.md
echo "## Current Schema" >> migration_report.md
echo "" >> migration_report.md
python -c "
from app import create_app, db
from sqlalchemy import inspect
app = create_app()
with app.app_context():
inspector = inspect(db.engine)
tables = inspector.get_table_names()
print('### Tables:')
for table in sorted(tables):
print(f'- {table}')
columns = inspector.get_columns(table)
for column in columns:
print(f' - {column[\"name\"]}: {column[\"type\"]}')
" >> migration_report.md
cat migration_report.md
- name: Upload migration report
if: steps.migration_check.outputs.migration_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: migration-report
path: migration_report.md
comment-on-pr:
runs-on: ubuntu-latest
needs: validate-migrations
if: github.event_name == 'pull_request' && always()
steps:
- name: Comment migration status on PR
uses: actions/github-script@v7
with:
script: |
const success = '${{ needs.validate-migrations.result }}' === 'success';
const migrationChanges = '${{ needs.validate-migrations.outputs.migration_changes }}' === 'true';
let commentBody = '## 🗄️ Database Migration Validation\n\n';
if (migrationChanges) {
if (success) {
commentBody += '✅ **Migration validation passed!**\n\n';
commentBody += '**Completed checks:**\n';
commentBody += '- ✅ Migration consistency validation\n';
commentBody += '- ✅ Rollback safety test\n';
commentBody += '- ✅ Data integrity verification\n\n';
commentBody += '**The database migrations are safe to apply.** 🚀\n';
} else {
commentBody += '❌ **Migration validation failed!**\n\n';
commentBody += '**Issues detected:**\n';
commentBody += '- Migration consistency problems\n';
commentBody += '- Rollback safety issues\n';
commentBody += '- Data integrity concerns\n\n';
commentBody += '**Please review the migration files and fix the issues before merging.** ⚠️\n';
}
} else {
commentBody += ' **No migration-related changes detected.**\n\n';
commentBody += 'This PR does not modify database models or migrations.\n';
}
commentBody += '\n---\n*This comment was automatically generated by the Migration Validation workflow.*';
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Database Migration Validation')
);
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}

260
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,260 @@
name: Release Management
on:
release:
types: [published, edited]
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.2.3)'
required: true
type: string
pre_release:
description: 'Mark as pre-release'
required: false
type: boolean
default: false
generate_changelog:
description: 'Auto-generate changelog'
required: false
type: boolean
default: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: drytrix/timetracker
jobs:
validate-release:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.validate.outputs.version }}
is_prerelease: ${{ steps.validate.outputs.is_prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate release version
id: validate
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="${{ github.event.inputs.version }}"
IS_PRERELEASE="${{ github.event.inputs.pre_release }}"
else
VERSION="${{ github.event.release.tag_name }}"
IS_PRERELEASE="${{ github.event.release.prerelease }}"
fi
# Validate semantic version format
if [[ ! "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ Invalid version format: $VERSION"
echo "Expected format: v1.2.3 or v1.2.3-alpha.1"
exit 1
fi
echo "✅ Version validated: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
run-tests:
runs-on: ubuntu-latest
needs: validate-release
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run database migrations test
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
run: |
flask db upgrade
python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('✅ Database connection successful')"
- name: Run tests
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
run: |
if [ -d "tests" ]; then
pytest tests/ -v --cov=app --cov-report=xml
else
echo "⚠️ No tests directory found, skipping tests"
fi
build-and-push:
runs-on: ubuntu-latest
needs: [validate-release, run-tests]
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ needs.validate-release.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
generate-changelog:
runs-on: ubuntu-latest
needs: validate-release
if: github.event.inputs.generate_changelog == 'true' || github.event_name == 'release'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
id: changelog
run: |
# Get the previous release tag
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
CURRENT_TAG="${{ needs.validate-release.outputs.version }}"
if [ -n "$PREVIOUS_TAG" ]; then
echo "## Changes since $PREVIOUS_TAG" > changelog.md
echo "" >> changelog.md
# Get commits since last tag
git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD >> changelog.md
else
echo "## Initial Release" > changelog.md
echo "" >> changelog.md
echo "- Initial release of TimeTracker" >> changelog.md
fi
# Upload changelog as artifact
cat changelog.md
- name: Upload changelog
uses: actions/upload-artifact@v4
with:
name: changelog
path: changelog.md
update-documentation:
runs-on: ubuntu-latest
needs: [validate-release, build-and-push]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update version in documentation
run: |
VERSION="${{ needs.validate-release.outputs.version }}"
# Update README.md with new version
if grep -q "Version:" README.md; then
sed -i "s/Version: .*/Version: $VERSION/" README.md
else
echo "Version: $VERSION" >> README.md
fi
# Update docker-compose examples with new version
find . -name "docker-compose*.yml" -exec sed -i "s|ghcr.io/drytrix/timetracker:.*|ghcr.io/drytrix/timetracker:$VERSION|g" {} \;
- name: Commit version updates
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "docs: update version references to ${{ needs.validate-release.outputs.version }}"
git push
fi
notify-deployment:
runs-on: ubuntu-latest
needs: [validate-release, build-and-push, update-documentation]
if: always() && needs.build-and-push.result == 'success'
steps:
- name: Create deployment summary
run: |
echo "# 🚀 Release Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ needs.validate-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Pre-release:** ${{ needs.validate-release.outputs.is_prerelease }}" >> $GITHUB_STEP_SUMMARY
echo "**Docker Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 📦 Deployment Commands" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Run" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "docker run -d -p 8080:8080 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Compose" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`yaml" >> $GITHUB_STEP_SUMMARY
echo "services:" >> $GITHUB_STEP_SUMMARY
echo " app:" >> $GITHUB_STEP_SUMMARY
echo " image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo " ports:" >> $GITHUB_STEP_SUMMARY
echo " - \"8080:8080\"" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY