mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-05 20:09:52 -05:00
improvements to release process.
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -31,6 +31,8 @@ jobs:
|
||||
include:
|
||||
- name: amd64
|
||||
platform: linux/amd64
|
||||
- name: arm64
|
||||
platform: linux/arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,360 @@
|
||||
# TimeTracker Release Process Guide
|
||||
|
||||
This document outlines the comprehensive release process for TimeTracker, including automated workflows, manual steps, and best practices.
|
||||
|
||||
## 🚀 Quick Release Guide
|
||||
|
||||
### Automated Release (Recommended)
|
||||
|
||||
```bash
|
||||
# 1. Create a complete release with changelog and GitHub release
|
||||
./scripts/version-manager.sh release --version v1.2.3 --changelog --github-release
|
||||
|
||||
# 2. For pre-releases
|
||||
./scripts/version-manager.sh release --version v1.2.3-rc.1 --pre-release --changelog --github-release
|
||||
```
|
||||
|
||||
### Manual Release Steps
|
||||
|
||||
1. **Prepare Release**
|
||||
2. **Create Tag**
|
||||
3. **Generate Changelog**
|
||||
4. **Create GitHub Release**
|
||||
5. **Verify Deployment**
|
||||
|
||||
## 📋 Detailed Release Process
|
||||
|
||||
### 1. Pre-Release Checklist
|
||||
|
||||
Before starting any release, ensure:
|
||||
|
||||
- [ ] **All tests pass** in CI/CD
|
||||
- [ ] **Database migrations tested** and documented
|
||||
- [ ] **Docker images build** successfully
|
||||
- [ ] **Documentation updated** with new features
|
||||
- [ ] **Breaking changes documented** (if any)
|
||||
- [ ] **Security vulnerabilities addressed**
|
||||
- [ ] **Performance regressions checked**
|
||||
|
||||
### 2. Release Preparation
|
||||
|
||||
#### Check Current Status
|
||||
```bash
|
||||
# Check current version and status
|
||||
./scripts/version-manager.sh status
|
||||
|
||||
# Check for uncommitted changes
|
||||
git status
|
||||
|
||||
# Ensure you're on main branch
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
#### Version Selection
|
||||
Follow [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **Major** (v2.0.0): Breaking changes, major new features
|
||||
- **Minor** (v1.1.0): New features, backward compatible
|
||||
- **Patch** (v1.0.1): Bug fixes, backward compatible
|
||||
- **Pre-release** (v1.0.0-rc.1): Release candidates, beta versions
|
||||
|
||||
#### Suggested Version
|
||||
```bash
|
||||
# Get version suggestion
|
||||
./scripts/version-manager.sh suggest
|
||||
```
|
||||
|
||||
### 3. Release Types
|
||||
|
||||
#### 3.1 Standard Release
|
||||
|
||||
```bash
|
||||
# Create standard release
|
||||
./scripts/version-manager.sh release \
|
||||
--version v1.2.3 \
|
||||
--message "Release 1.2.3 with new features and bug fixes" \
|
||||
--changelog \
|
||||
--github-release
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
1. Creates and pushes git tag
|
||||
2. Generates changelog from commits
|
||||
3. Creates GitHub release with changelog
|
||||
4. Triggers Docker image build via GitHub Actions
|
||||
|
||||
#### 3.2 Pre-Release
|
||||
|
||||
```bash
|
||||
# Create pre-release (RC, beta, alpha)
|
||||
./scripts/version-manager.sh release \
|
||||
--version v1.2.3-rc.1 \
|
||||
--message "Release candidate for 1.2.3" \
|
||||
--pre-release \
|
||||
--changelog \
|
||||
--github-release
|
||||
```
|
||||
|
||||
#### 3.3 Hotfix Release
|
||||
|
||||
```bash
|
||||
# Create hotfix from main branch
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Apply hotfix
|
||||
git cherry-pick <hotfix-commit>
|
||||
|
||||
# Create hotfix release
|
||||
./scripts/version-manager.sh release \
|
||||
--version v1.2.4 \
|
||||
--message "Hotfix: Critical security update" \
|
||||
--changelog \
|
||||
--github-release
|
||||
```
|
||||
|
||||
### 4. Manual Release Steps
|
||||
|
||||
If you prefer manual control over the release process:
|
||||
|
||||
#### Step 1: Create Tag
|
||||
```bash
|
||||
# Create annotated tag
|
||||
git tag -a v1.2.3 -m "Release 1.2.3"
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
#### Step 2: Generate Changelog
|
||||
```bash
|
||||
# Generate changelog
|
||||
python scripts/generate-changelog.py v1.2.3 --output CHANGELOG.md
|
||||
|
||||
# Review and edit changelog if needed
|
||||
nano CHANGELOG.md
|
||||
```
|
||||
|
||||
#### Step 3: Create GitHub Release
|
||||
```bash
|
||||
# Using GitHub CLI
|
||||
gh release create v1.2.3 \
|
||||
--title "TimeTracker v1.2.3" \
|
||||
--notes-file CHANGELOG.md
|
||||
|
||||
# Or via GitHub web interface
|
||||
# Go to: https://github.com/your-repo/releases/new
|
||||
```
|
||||
|
||||
### 5. Post-Release Verification
|
||||
|
||||
#### 5.1 Verify GitHub Actions
|
||||
- Check that Docker build workflow completed successfully
|
||||
- Verify Docker images are published to GHCR
|
||||
- Confirm all CI/CD checks passed
|
||||
|
||||
#### 5.2 Test Docker Images
|
||||
```bash
|
||||
# Test the released image
|
||||
docker run -d --name test-release -p 8080:8080 \
|
||||
ghcr.io/drytrix/timetracker:v1.2.3
|
||||
|
||||
# Verify health
|
||||
curl -f http://localhost:8080/_health
|
||||
|
||||
# Clean up
|
||||
docker stop test-release && docker rm test-release
|
||||
```
|
||||
|
||||
#### 5.3 Update Documentation
|
||||
- [ ] Update README.md version references
|
||||
- [ ] Update deployment documentation
|
||||
- [ ] Update Docker Compose examples
|
||||
- [ ] Notify users of new release
|
||||
|
||||
### 6. Release Workflow Automation
|
||||
|
||||
The release process triggers several automated workflows:
|
||||
|
||||
#### 6.1 Release Workflow (`release.yml`)
|
||||
**Triggered by:** GitHub release creation or manual dispatch
|
||||
|
||||
**Steps:**
|
||||
1. **Validate Release** - Ensures version format is correct
|
||||
2. **Run Tests** - Full test suite with database migrations
|
||||
3. **Build & Push Docker** - Multi-architecture Docker images
|
||||
4. **Generate Changelog** - Automated changelog generation
|
||||
5. **Update Documentation** - Version references in docs
|
||||
6. **Notify Deployment** - Summary and deployment instructions
|
||||
|
||||
#### 6.2 CI Workflow (`ci.yml`)
|
||||
**Triggered by:** Push to main/develop, pull requests
|
||||
|
||||
**Steps:**
|
||||
1. **Lint & Format** - Code quality checks
|
||||
2. **Test Database Migrations** - PostgreSQL & SQLite testing
|
||||
3. **Test Docker Build** - Container build and startup verification
|
||||
4. **Security Scan** - Dependency and code security scanning
|
||||
5. **Version Management Validation** - Version manager script testing
|
||||
|
||||
#### 6.3 Migration Check Workflow (`migration-check.yml`)
|
||||
**Triggered by:** Changes to models or migrations
|
||||
|
||||
**Steps:**
|
||||
1. **Validate Migrations** - Schema consistency and rollback safety
|
||||
2. **Test with Sample Data** - Data integrity verification
|
||||
3. **Generate Migration Report** - Detailed migration analysis
|
||||
|
||||
### 7. Emergency Procedures
|
||||
|
||||
#### 7.1 Rollback Release
|
||||
```bash
|
||||
# Delete tag locally and remotely
|
||||
git tag -d v1.2.3
|
||||
git push origin --delete v1.2.3
|
||||
|
||||
# Delete GitHub release
|
||||
gh release delete v1.2.3
|
||||
|
||||
# Revert commits if needed
|
||||
git revert <commit-hash>
|
||||
```
|
||||
|
||||
#### 7.2 Fix Broken Release
|
||||
```bash
|
||||
# Create hotfix
|
||||
git checkout v1.2.3
|
||||
git cherry-pick <fix-commit>
|
||||
|
||||
# Create new patch release
|
||||
./scripts/version-manager.sh release \
|
||||
--version v1.2.4 \
|
||||
--message "Hotfix for v1.2.3 issues" \
|
||||
--changelog \
|
||||
--github-release
|
||||
```
|
||||
|
||||
### 8. Release Schedule
|
||||
|
||||
#### Recommended Schedule
|
||||
- **Major releases**: Every 6-12 months
|
||||
- **Minor releases**: Every 1-2 months
|
||||
- **Patch releases**: As needed for critical fixes
|
||||
- **Pre-releases**: 1-2 weeks before major/minor releases
|
||||
|
||||
#### Release Windows
|
||||
- **Regular releases**: Tuesday-Thursday (better for issue resolution)
|
||||
- **Hotfixes**: Any day (emergency only)
|
||||
- **Pre-releases**: Friday (allows weekend testing)
|
||||
|
||||
### 9. Communication
|
||||
|
||||
#### Internal Team
|
||||
- [ ] Notify team before release
|
||||
- [ ] Share release notes
|
||||
- [ ] Coordinate deployment timing
|
||||
- [ ] Plan post-release monitoring
|
||||
|
||||
#### External Users
|
||||
- [ ] Update release notes on GitHub
|
||||
- [ ] Update documentation website
|
||||
- [ ] Notify via social media/newsletters
|
||||
- [ ] Update Docker Hub descriptions
|
||||
|
||||
### 10. Quality Gates
|
||||
|
||||
Every release must pass:
|
||||
|
||||
- [ ] **All automated tests** (unit, integration, E2E)
|
||||
- [ ] **Database migration tests** (up and down)
|
||||
- [ ] **Docker build verification** (multi-architecture)
|
||||
- [ ] **Security scans** (dependencies and code)
|
||||
- [ ] **Performance benchmarks** (no significant regression)
|
||||
- [ ] **Documentation review** (accuracy and completeness)
|
||||
|
||||
### 11. Troubleshooting
|
||||
|
||||
#### Common Issues
|
||||
|
||||
**Issue**: Docker build fails
|
||||
```bash
|
||||
# Check Docker build locally
|
||||
docker build -t test-build .
|
||||
|
||||
# Check workflow logs in GitHub Actions
|
||||
```
|
||||
|
||||
**Issue**: Migration validation fails
|
||||
```bash
|
||||
# Test migrations locally
|
||||
flask db upgrade
|
||||
flask db downgrade
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
**Issue**: Version tag already exists
|
||||
```bash
|
||||
# Check existing tags
|
||||
git tag -l
|
||||
|
||||
# Delete if needed
|
||||
git tag -d v1.2.3
|
||||
git push origin --delete v1.2.3
|
||||
```
|
||||
|
||||
### 12. Tools and Dependencies
|
||||
|
||||
#### Required Tools
|
||||
- **Git** - Version control
|
||||
- **GitHub CLI** (`gh`) - GitHub release management
|
||||
- **Docker** - Container testing
|
||||
- **Python 3.11+** - Script execution
|
||||
- **Flask** - Database migration testing
|
||||
|
||||
#### Installation
|
||||
```bash
|
||||
# Install GitHub CLI
|
||||
# macOS: brew install gh
|
||||
# Ubuntu: sudo apt install gh
|
||||
# Windows: winget install GitHub.CLI
|
||||
|
||||
# Authenticate
|
||||
gh auth login
|
||||
|
||||
# Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 13. Metrics and Monitoring
|
||||
|
||||
Track release metrics:
|
||||
- **Release frequency** - How often releases are made
|
||||
- **Lead time** - Time from commit to release
|
||||
- **Failure rate** - Percentage of failed releases
|
||||
- **Recovery time** - Time to fix broken releases
|
||||
- **User adoption** - Docker pull statistics
|
||||
|
||||
### 14. Continuous Improvement
|
||||
|
||||
Regular review of:
|
||||
- [ ] Release process efficiency
|
||||
- [ ] Automation opportunities
|
||||
- [ ] Quality gate effectiveness
|
||||
- [ ] User feedback incorporation
|
||||
- [ ] Tool and workflow updates
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documentation
|
||||
|
||||
- [Version Management System](VERSION_MANAGEMENT.md)
|
||||
- [Database Migrations](../migrations/README.md)
|
||||
- [Docker Setup](DOCKER_PUBLIC_SETUP.md)
|
||||
- [Contributing Guidelines](CONTRIBUTING.md)
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For release process issues:
|
||||
1. Check this documentation
|
||||
2. Review GitHub Actions logs
|
||||
3. Test locally with provided commands
|
||||
4. Create issue with detailed error information
|
||||
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Automated Changelog Generator for TimeTracker
|
||||
Generates changelogs from git commits and GitHub issues/PRs
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import argparse
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
import requests
|
||||
|
||||
class ChangelogGenerator:
|
||||
def __init__(self, repo_path: str = "."):
|
||||
self.repo_path = repo_path
|
||||
self.github_token = os.getenv('GITHUB_TOKEN')
|
||||
self.repo_url = self._get_repo_url()
|
||||
|
||||
def _get_repo_url(self) -> Optional[str]:
|
||||
"""Get GitHub repository URL from git remote"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'remote', 'get-url', 'origin'],
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
url = result.stdout.strip()
|
||||
# Convert SSH URL to HTTPS if needed
|
||||
if url.startswith('git@github.com:'):
|
||||
url = url.replace('git@github.com:', 'https://github.com/')
|
||||
if url.endswith('.git'):
|
||||
url = url[:-4]
|
||||
return url
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get repository URL: {e}")
|
||||
return None
|
||||
|
||||
def _run_git_command(self, command: List[str]) -> str:
|
||||
"""Run a git command and return output"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git'] + command,
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Git command failed: {e}")
|
||||
return ""
|
||||
|
||||
def get_latest_tag(self) -> str:
|
||||
"""Get the latest git tag"""
|
||||
output = self._run_git_command(['describe', '--tags', '--abbrev=0'])
|
||||
return output if output else "HEAD~50" # Fallback to last 50 commits
|
||||
|
||||
def get_commits_since_tag(self, since_tag: str) -> List[Dict[str, str]]:
|
||||
"""Get commits since the specified tag"""
|
||||
if since_tag == "HEAD~50":
|
||||
command = ['log', '--pretty=format:%H|%s|%an|%ad|%b', '--date=short', '-50']
|
||||
else:
|
||||
command = ['log', f'{since_tag}..HEAD', '--pretty=format:%H|%s|%an|%ad|%b', '--date=short']
|
||||
|
||||
output = self._run_git_command(command)
|
||||
commits = []
|
||||
|
||||
if output:
|
||||
for line in output.split('\n'):
|
||||
if '|' in line:
|
||||
parts = line.split('|', 4)
|
||||
if len(parts) >= 4:
|
||||
commits.append({
|
||||
'hash': parts[0][:8],
|
||||
'subject': parts[1],
|
||||
'author': parts[2],
|
||||
'date': parts[3],
|
||||
'body': parts[4] if len(parts) > 4 else ''
|
||||
})
|
||||
|
||||
return commits
|
||||
|
||||
def categorize_commits(self, commits: List[Dict[str, str]]) -> Dict[str, List[Dict[str, str]]]:
|
||||
"""Categorize commits by type"""
|
||||
categories = {
|
||||
'Features': [],
|
||||
'Bug Fixes': [],
|
||||
'Improvements': [],
|
||||
'Documentation': [],
|
||||
'Refactoring': [],
|
||||
'Dependencies': [],
|
||||
'Database': [],
|
||||
'Docker': [],
|
||||
'CI/CD': [],
|
||||
'Other': []
|
||||
}
|
||||
|
||||
# Patterns for categorization
|
||||
patterns = {
|
||||
'Features': [r'^feat(\(.+\))?:', r'^add:', r'^implement:', r'^new:'],
|
||||
'Bug Fixes': [r'^fix(\(.+\))?:', r'^bug:', r'^hotfix:', r'^patch:'],
|
||||
'Improvements': [r'^improve:', r'^enhance:', r'^update:', r'^upgrade:'],
|
||||
'Documentation': [r'^docs?(\(.+\))?:', r'^readme:', r'^doc:'],
|
||||
'Refactoring': [r'^refactor(\(.+\))?:', r'^cleanup:', r'^reorganize:'],
|
||||
'Dependencies': [r'^deps?(\(.+\))?:', r'^bump:', r'^requirements:'],
|
||||
'Database': [r'^db:', r'^migration:', r'^schema:', r'^alembic:'],
|
||||
'Docker': [r'^docker:', r'^dockerfile:', r'^compose:'],
|
||||
'CI/CD': [r'^ci:', r'^cd:', r'^workflow:', r'^action:', r'^build:']
|
||||
}
|
||||
|
||||
for commit in commits:
|
||||
subject = commit['subject'].lower()
|
||||
categorized = False
|
||||
|
||||
for category, category_patterns in patterns.items():
|
||||
for pattern in category_patterns:
|
||||
if re.match(pattern, subject):
|
||||
categories[category].append(commit)
|
||||
categorized = True
|
||||
break
|
||||
if categorized:
|
||||
break
|
||||
|
||||
if not categorized:
|
||||
categories['Other'].append(commit)
|
||||
|
||||
return categories
|
||||
|
||||
def extract_breaking_changes(self, commits: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
||||
"""Extract breaking changes from commits"""
|
||||
breaking_changes = []
|
||||
|
||||
for commit in commits:
|
||||
# Look for BREAKING CHANGE in commit message
|
||||
full_message = f"{commit['subject']} {commit['body']}"
|
||||
if 'BREAKING CHANGE' in full_message or 'breaking:' in commit['subject'].lower():
|
||||
breaking_changes.append(commit)
|
||||
|
||||
return breaking_changes
|
||||
|
||||
def get_github_prs_and_issues(self, commits: List[Dict[str, str]]) -> Dict[str, List[Dict]]:
|
||||
"""Get GitHub PRs and issues mentioned in commits"""
|
||||
prs = []
|
||||
issues = []
|
||||
|
||||
if not self.github_token or not self.repo_url:
|
||||
return {'prs': prs, 'issues': issues}
|
||||
|
||||
# Extract PR/issue numbers from commit messages
|
||||
pr_pattern = r'#(\d+)'
|
||||
mentioned_numbers = set()
|
||||
|
||||
for commit in commits:
|
||||
matches = re.findall(pr_pattern, f"{commit['subject']} {commit['body']}")
|
||||
mentioned_numbers.update(matches)
|
||||
|
||||
# Fetch details from GitHub API
|
||||
repo_parts = self.repo_url.replace('https://github.com/', '').split('/')
|
||||
if len(repo_parts) >= 2:
|
||||
owner, repo = repo_parts[0], repo_parts[1]
|
||||
|
||||
headers = {'Authorization': f'token {self.github_token}'}
|
||||
|
||||
for number in mentioned_numbers:
|
||||
try:
|
||||
# Try to fetch as PR first
|
||||
pr_url = f'https://api.github.com/repos/{owner}/{repo}/pulls/{number}'
|
||||
response = requests.get(pr_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
pr_data = response.json()
|
||||
prs.append({
|
||||
'number': number,
|
||||
'title': pr_data['title'],
|
||||
'url': pr_data['html_url'],
|
||||
'author': pr_data['user']['login']
|
||||
})
|
||||
else:
|
||||
# Try as issue
|
||||
issue_url = f'https://api.github.com/repos/{owner}/{repo}/issues/{number}'
|
||||
response = requests.get(issue_url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
issue_data = response.json()
|
||||
issues.append({
|
||||
'number': number,
|
||||
'title': issue_data['title'],
|
||||
'url': issue_data['html_url'],
|
||||
'author': issue_data['user']['login']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not fetch GitHub data for #{number}: {e}")
|
||||
|
||||
return {'prs': prs, 'issues': issues}
|
||||
|
||||
def generate_changelog(self, version: str, since_tag: str = None) -> str:
|
||||
"""Generate complete changelog"""
|
||||
if not since_tag:
|
||||
since_tag = self.get_latest_tag()
|
||||
|
||||
print(f"Generating changelog for version {version} since {since_tag}...")
|
||||
|
||||
# Get commits
|
||||
commits = self.get_commits_since_tag(since_tag)
|
||||
print(f"Found {len(commits)} commits")
|
||||
|
||||
if not commits:
|
||||
return f"# {version}\n\n*No changes since {since_tag}*\n"
|
||||
|
||||
# Categorize commits
|
||||
categorized = self.categorize_commits(commits)
|
||||
|
||||
# Extract breaking changes
|
||||
breaking_changes = self.extract_breaking_changes(commits)
|
||||
|
||||
# Get GitHub data
|
||||
github_data = self.get_github_prs_and_issues(commits)
|
||||
|
||||
# Generate changelog content
|
||||
changelog = self._format_changelog(
|
||||
version, since_tag, categorized, breaking_changes, github_data
|
||||
)
|
||||
|
||||
return changelog
|
||||
|
||||
def _format_changelog(
|
||||
self,
|
||||
version: str,
|
||||
since_tag: str,
|
||||
categorized: Dict[str, List[Dict[str, str]]],
|
||||
breaking_changes: List[Dict[str, str]],
|
||||
github_data: Dict[str, List[Dict]]
|
||||
) -> str:
|
||||
"""Format the changelog content"""
|
||||
|
||||
changelog = f"# {version}\n\n"
|
||||
changelog += f"*Released on {datetime.now().strftime('%Y-%m-%d')}*\n\n"
|
||||
|
||||
# Summary
|
||||
total_commits = sum(len(commits) for commits in categorized.values())
|
||||
changelog += f"**{total_commits} changes** since {since_tag}\n\n"
|
||||
|
||||
# Breaking changes (if any)
|
||||
if breaking_changes:
|
||||
changelog += "## ⚠️ Breaking Changes\n\n"
|
||||
for commit in breaking_changes:
|
||||
changelog += f"- {commit['subject']} ([{commit['hash']}]"
|
||||
if self.repo_url:
|
||||
changelog += f"({self.repo_url}/commit/{commit['hash']}))\n"
|
||||
else:
|
||||
changelog += ")\n"
|
||||
changelog += "\n"
|
||||
|
||||
# Features and improvements
|
||||
feature_categories = ['Features', 'Improvements', 'Bug Fixes']
|
||||
for category in feature_categories:
|
||||
if categorized[category]:
|
||||
icon = {'Features': '✨', 'Improvements': '🚀', 'Bug Fixes': '🐛'}[category]
|
||||
changelog += f"## {icon} {category}\n\n"
|
||||
|
||||
for commit in categorized[category]:
|
||||
# Clean up commit subject
|
||||
subject = re.sub(r'^(feat|fix|improve|enhance|update)(\(.+\))?:\s*', '', commit['subject'], flags=re.IGNORECASE)
|
||||
changelog += f"- {subject} ([{commit['hash']}]"
|
||||
if self.repo_url:
|
||||
changelog += f"({self.repo_url}/commit/{commit['hash']}))\n"
|
||||
else:
|
||||
changelog += ")\n"
|
||||
changelog += "\n"
|
||||
|
||||
# Technical changes
|
||||
tech_categories = ['Database', 'Docker', 'CI/CD', 'Refactoring', 'Dependencies']
|
||||
tech_changes = any(categorized[cat] for cat in tech_categories)
|
||||
|
||||
if tech_changes:
|
||||
changelog += "## 🔧 Technical Changes\n\n"
|
||||
for category in tech_categories:
|
||||
if categorized[category]:
|
||||
changelog += f"### {category}\n"
|
||||
for commit in categorized[category]:
|
||||
subject = re.sub(r'^(db|docker|ci|cd|refactor|deps?)(\(.+\))?:\s*', '', commit['subject'], flags=re.IGNORECASE)
|
||||
changelog += f"- {subject} ([{commit['hash']}]"
|
||||
if self.repo_url:
|
||||
changelog += f"({self.repo_url}/commit/{commit['hash']}))\n"
|
||||
else:
|
||||
changelog += ")\n"
|
||||
changelog += "\n"
|
||||
|
||||
# Documentation
|
||||
if categorized['Documentation']:
|
||||
changelog += "## 📚 Documentation\n\n"
|
||||
for commit in categorized['Documentation']:
|
||||
subject = re.sub(r'^docs?(\(.+\))?:\s*', '', commit['subject'], flags=re.IGNORECASE)
|
||||
changelog += f"- {subject} ([{commit['hash']}]"
|
||||
if self.repo_url:
|
||||
changelog += f"({self.repo_url}/commit/{commit['hash']}))\n"
|
||||
else:
|
||||
changelog += ")\n"
|
||||
changelog += "\n"
|
||||
|
||||
# Other changes
|
||||
if categorized['Other']:
|
||||
changelog += "## 📋 Other Changes\n\n"
|
||||
for commit in categorized['Other']:
|
||||
changelog += f"- {commit['subject']} ([{commit['hash']}]"
|
||||
if self.repo_url:
|
||||
changelog += f"({self.repo_url}/commit/{commit['hash']}))\n"
|
||||
else:
|
||||
changelog += ")\n"
|
||||
changelog += "\n"
|
||||
|
||||
# GitHub PRs and Issues
|
||||
if github_data['prs'] or github_data['issues']:
|
||||
changelog += "## 🔗 Related\n\n"
|
||||
|
||||
if github_data['prs']:
|
||||
changelog += "**Pull Requests:**\n"
|
||||
for pr in github_data['prs']:
|
||||
changelog += f"- [{pr['title']}]({pr['url']}) by @{pr['author']}\n"
|
||||
changelog += "\n"
|
||||
|
||||
if github_data['issues']:
|
||||
changelog += "**Issues:**\n"
|
||||
for issue in github_data['issues']:
|
||||
changelog += f"- [{issue['title']}]({issue['url']}) by @{issue['author']}\n"
|
||||
changelog += "\n"
|
||||
|
||||
# Contributors
|
||||
contributors = set()
|
||||
for category_commits in categorized.values():
|
||||
for commit in category_commits:
|
||||
contributors.add(commit['author'])
|
||||
|
||||
if contributors:
|
||||
changelog += "## 👥 Contributors\n\n"
|
||||
changelog += f"Thanks to all contributors: {', '.join(f'@{c}' for c in sorted(contributors))}\n\n"
|
||||
|
||||
return changelog
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate changelog for TimeTracker')
|
||||
parser.add_argument('version', help='Version for the changelog (e.g., v1.2.3)')
|
||||
parser.add_argument('--since', help='Generate changelog since this tag/commit')
|
||||
parser.add_argument('--output', '-o', help='Output file (default: CHANGELOG.md)')
|
||||
parser.add_argument('--append', action='store_true', help='Append to existing changelog')
|
||||
parser.add_argument('--repo-path', default='.', help='Repository path')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
generator = ChangelogGenerator(args.repo_path)
|
||||
changelog = generator.generate_changelog(args.version, args.since)
|
||||
|
||||
if args.output:
|
||||
output_file = args.output
|
||||
else:
|
||||
output_file = os.path.join(args.repo_path, 'CHANGELOG.md')
|
||||
|
||||
# Write changelog
|
||||
mode = 'a' if args.append else 'w'
|
||||
with open(output_file, mode, encoding='utf-8') as f:
|
||||
if args.append and os.path.exists(output_file):
|
||||
f.write('\n\n---\n\n')
|
||||
f.write(changelog)
|
||||
|
||||
print(f"Changelog written to {output_file}")
|
||||
|
||||
# Also output to stdout for GitHub Actions
|
||||
print("\n" + "="*50)
|
||||
print("GENERATED CHANGELOG:")
|
||||
print("="*50)
|
||||
print(changelog)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -199,13 +199,16 @@ class VersionManager:
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Version Manager for TimeTracker')
|
||||
parser.add_argument('action', choices=['tag', 'build', 'list', 'info', 'status', 'suggest'],
|
||||
parser.add_argument('action', choices=['tag', 'build', 'list', 'info', 'status', 'suggest', 'release', 'changelog'],
|
||||
help='Action to perform')
|
||||
parser.add_argument('--version', '-v', help='Version string (e.g., v1.2.3, build-123)')
|
||||
parser.add_argument('--message', '-m', help='Tag message')
|
||||
parser.add_argument('--build-number', '-b', type=int, help='Build number for build tags')
|
||||
parser.add_argument('--no-push', action='store_true', help='Don\'t push tag to remote')
|
||||
parser.add_argument('--tag', '-t', help='Tag to show info for (for info action)')
|
||||
parser.add_argument('--pre-release', action='store_true', help='Mark as pre-release')
|
||||
parser.add_argument('--changelog', action='store_true', help='Generate changelog')
|
||||
parser.add_argument('--github-release', action='store_true', help='Create GitHub release')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -240,6 +243,57 @@ def main():
|
||||
else:
|
||||
print("No current version found")
|
||||
print("Suggested first version: v1.0.0")
|
||||
|
||||
elif args.action == 'release':
|
||||
if not args.version:
|
||||
print("Error: Version required for release action")
|
||||
print("Use --version or -v to specify version")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🚀 Creating release {args.version}...")
|
||||
|
||||
# Create tag
|
||||
if vm.create_tag(args.version, args.message, push=not args.no_push):
|
||||
print(f"✅ Tag {args.version} created successfully")
|
||||
|
||||
# Generate changelog if requested
|
||||
if args.changelog:
|
||||
print("📋 Generating changelog...")
|
||||
changelog_cmd = f"python scripts/generate-changelog.py {args.version}"
|
||||
if vm.run_command(changelog_cmd, capture_output=False):
|
||||
print("✅ Changelog generated successfully")
|
||||
else:
|
||||
print("⚠️ Changelog generation failed")
|
||||
|
||||
# Create GitHub release if requested
|
||||
if args.github_release:
|
||||
print("🐙 Creating GitHub release...")
|
||||
github_cmd = f"gh release create {args.version}"
|
||||
if args.pre_release:
|
||||
github_cmd += " --prerelease"
|
||||
if args.changelog and os.path.exists("CHANGELOG.md"):
|
||||
github_cmd += " --notes-file CHANGELOG.md"
|
||||
elif args.message:
|
||||
github_cmd += f" --notes '{args.message}'"
|
||||
|
||||
if vm.run_command(github_cmd, capture_output=False):
|
||||
print("✅ GitHub release created successfully")
|
||||
else:
|
||||
print("⚠️ GitHub release creation failed (make sure 'gh' CLI is installed and authenticated)")
|
||||
else:
|
||||
print("❌ Failed to create tag")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.action == 'changelog':
|
||||
current_tag = vm.get_latest_tag()
|
||||
version = args.version or vm.suggest_next_version(current_tag)
|
||||
|
||||
print(f"📋 Generating changelog for {version}...")
|
||||
changelog_cmd = f"python scripts/generate-changelog.py {version}"
|
||||
if vm.run_command(changelog_cmd, capture_output=False):
|
||||
print("✅ Changelog generated successfully")
|
||||
else:
|
||||
print("❌ Changelog generation failed")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user