mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-04-30 09:19:46 -05:00
81532fcd55
Refactor GitHub Actions workflows to support a release candidate (RC) branch workflow instead of direct develop->main flow. Changes: - cd-development.yml: Trigger on PRs to RC branches (not push to develop) * Updated summary to show PR context (source/target branches) * Build development images when code is promoted to RC - cd-release.yml: Trigger on PRs from RC to main/master * Added path filters for code changes only * Enables release validation before merge to main - ci-comprehensive.yml: Run tests on PRs to RC branches * Full test suite now runs for PRs to main, master, and RC branches * Ensures code quality before RC promotion New workflow: develop (push) -> no actions develop -> rc (PR) -> run tests + development build rc -> main (PR) -> run tests + release build Supports both single RC branch (rc) and versioned RC branches (rc/*) Breaking change: Development builds no longer trigger on push to develop. They now require a PR to an RC branch.
511 lines
17 KiB
YAML
511 lines
17 KiB
YAML
name: Comprehensive CI Pipeline
|
||
|
||
# This workflow runs comprehensive tests on pull requests
|
||
#
|
||
# Test Strategy:
|
||
# - Smoke tests (fast, critical) run first
|
||
# - Unit, integration, security, and code quality tests run in parallel
|
||
# - Full test suite with PostgreSQL runs for PRs to main/master and RC branches
|
||
# - Docker build test ensures the image builds correctly
|
||
# - Test summary posted as PR comment
|
||
#
|
||
# All tests must pass before a PR can be merged
|
||
#
|
||
# Workflow triggers:
|
||
# - PRs to RC branches (from develop) - validates code before RC build
|
||
# - PRs to main/master (from RC) - validates code before release
|
||
|
||
on:
|
||
pull_request:
|
||
branches: [ main, master, 'rc', 'rc/**' ]
|
||
|
||
env:
|
||
PYTHON_VERSION: '3.11'
|
||
POSTGRES_VERSION: '16'
|
||
|
||
jobs:
|
||
# ============================================================================
|
||
# Smoke Tests - Fast, critical tests that run first
|
||
# ============================================================================
|
||
smoke-tests:
|
||
name: Smoke Tests (Quick)
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: ${{ env.PYTHON_VERSION }}
|
||
cache: 'pip'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements.txt
|
||
pip install -r requirements-test.txt
|
||
pip install -e .
|
||
|
||
- name: Run smoke tests
|
||
env:
|
||
PYTHONPATH: ${{ github.workspace }}
|
||
run: |
|
||
pytest -m smoke -v --tb=short --no-cov
|
||
|
||
- name: Upload smoke test results
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: smoke-test-results
|
||
path: |
|
||
.pytest_cache/
|
||
test-results/
|
||
|
||
# ============================================================================
|
||
# Unit Tests - Fast, isolated tests
|
||
# ============================================================================
|
||
unit-tests:
|
||
name: Unit Tests
|
||
runs-on: ubuntu-latest
|
||
needs: smoke-tests
|
||
timeout-minutes: 10
|
||
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
test-group: [models, routes, api, utils]
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: ${{ env.PYTHON_VERSION }}
|
||
cache: 'pip'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements.txt
|
||
pip install -r requirements-test.txt
|
||
pip install -e .
|
||
|
||
- name: Run unit tests - ${{ matrix.test-group }}
|
||
env:
|
||
PYTHONPATH: ${{ github.workspace }}
|
||
run: |
|
||
if [ "${{ matrix.test-group }}" == "api" ]; then
|
||
pytest -m "api and integration" -v --cov=app --cov-report=xml --cov-report=html
|
||
else
|
||
pytest -m "unit and ${{ matrix.test-group }}" -v --cov=app --cov-report=xml --cov-report=html
|
||
fi
|
||
|
||
- name: Upload coverage to Codecov
|
||
uses: codecov/codecov-action@v4
|
||
with:
|
||
files: ./coverage.xml
|
||
flags: unit-${{ matrix.test-group }}
|
||
name: unit-${{ matrix.test-group }}
|
||
|
||
- name: Upload test results
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: unit-test-results-${{ matrix.test-group }}
|
||
path: |
|
||
htmlcov/
|
||
coverage.xml
|
||
|
||
# ============================================================================
|
||
# Integration Tests - Medium speed, component interaction tests
|
||
# ============================================================================
|
||
integration-tests:
|
||
name: Integration Tests
|
||
runs-on: ubuntu-latest
|
||
needs: smoke-tests
|
||
timeout-minutes: 15
|
||
|
||
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 code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: ${{ env.PYTHON_VERSION }}
|
||
cache: 'pip'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements.txt
|
||
pip install -r requirements-test.txt
|
||
pip install -e .
|
||
|
||
- name: Run integration tests
|
||
env:
|
||
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
|
||
FLASK_APP: app.py
|
||
FLASK_ENV: testing
|
||
PYTHONPATH: ${{ github.workspace }}
|
||
run: |
|
||
pytest -m integration -v --cov=app --cov-report=xml --cov-report=html
|
||
|
||
- name: Upload coverage to Codecov
|
||
uses: codecov/codecov-action@v4
|
||
with:
|
||
files: ./coverage.xml
|
||
flags: integration
|
||
name: integration-tests
|
||
|
||
- name: Upload test results
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: integration-test-results
|
||
path: |
|
||
htmlcov/
|
||
coverage.xml
|
||
|
||
# ============================================================================
|
||
# Security Tests
|
||
# ============================================================================
|
||
security-tests:
|
||
name: Security Tests
|
||
runs-on: ubuntu-latest
|
||
needs: smoke-tests
|
||
timeout-minutes: 10
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: ${{ env.PYTHON_VERSION }}
|
||
cache: 'pip'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements.txt
|
||
pip install -r requirements-test.txt
|
||
pip install -e .
|
||
|
||
- name: Run security tests
|
||
env:
|
||
PYTHONPATH: ${{ github.workspace }}
|
||
run: |
|
||
pytest -m security -v --tb=short
|
||
|
||
- name: Run Safety dependency check
|
||
run: |
|
||
safety check --file requirements.txt --json > safety-report.json || true
|
||
|
||
- name: Upload security reports
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: security-reports
|
||
path: |
|
||
safety-report.json
|
||
|
||
# ============================================================================
|
||
# Code Quality
|
||
# ============================================================================
|
||
code-quality:
|
||
name: Code Quality Checks
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: ${{ env.PYTHON_VERSION }}
|
||
cache: 'pip'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements-test.txt
|
||
|
||
- name: Run flake8
|
||
run: |
|
||
flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
||
flake8 app/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||
|
||
# ============================================================================
|
||
# Docker Build Test
|
||
# ============================================================================
|
||
docker-build:
|
||
name: Docker Build Test
|
||
runs-on: ubuntu-latest
|
||
needs: [unit-tests, integration-tests]
|
||
timeout-minutes: 20
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Build Docker image
|
||
run: |
|
||
docker build -t timetracker-test:pr-${{ github.event.pull_request.number || 'dev' }} .
|
||
|
||
- name: Test Docker container startup
|
||
run: |
|
||
docker run -d --name test-container \
|
||
-p 8080:8080 \
|
||
-e DATABASE_URL="sqlite:////app/test.db" \
|
||
-e SECRET_KEY="test-secret-key-for-ci-only-$(openssl rand -hex 32)" \
|
||
-e FLASK_ENV="development" \
|
||
timetracker-test:pr-${{ github.event.pull_request.number || 'dev' }}
|
||
|
||
# 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... ($i/30)"
|
||
sleep 2
|
||
done
|
||
|
||
# Show logs
|
||
docker logs test-container
|
||
|
||
# Final health check
|
||
curl -f http://localhost:8080/_health || exit 1
|
||
|
||
# Cleanup
|
||
docker stop test-container
|
||
docker rm test-container
|
||
|
||
# ============================================================================
|
||
# Full Test Suite (runs on all PRs to main/master/rc)
|
||
# ============================================================================
|
||
full-test-suite:
|
||
name: Full Test Suite with PostgreSQL
|
||
runs-on: ubuntu-latest
|
||
needs: [smoke-tests, unit-tests, integration-tests]
|
||
if: github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'master' || startsWith(github.base_ref, 'rc'))
|
||
timeout-minutes: 30
|
||
|
||
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 code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: ${{ env.PYTHON_VERSION }}
|
||
cache: 'pip'
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
pip install -r requirements.txt
|
||
pip install -r requirements-test.txt
|
||
pip install -e .
|
||
|
||
- name: Validate database migrations
|
||
env:
|
||
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
|
||
FLASK_APP: app.py
|
||
FLASK_ENV: testing
|
||
run: |
|
||
echo "🔍 Validating database migrations..."
|
||
|
||
# Check if there are migration-related changes
|
||
if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E "(app/models/|migrations/)" > /dev/null; then
|
||
echo "📋 Migration-related changes detected"
|
||
|
||
# Initialize fresh database
|
||
flask db upgrade
|
||
|
||
# Test migration rollback
|
||
CURRENT_MIGRATION=$(flask db current)
|
||
echo "Current migration: $CURRENT_MIGRATION"
|
||
|
||
if [ -n "$CURRENT_MIGRATION" ] && [ "$CURRENT_MIGRATION" != "None" ]; then
|
||
echo "Testing migration operations..."
|
||
flask db upgrade head
|
||
echo "✅ Migration validation passed"
|
||
fi
|
||
|
||
# Test with sample data
|
||
python -c "
|
||
from app import create_app, db
|
||
from app.models.user import User
|
||
from app.models.project import Project
|
||
from app.models.client import Client
|
||
|
||
app = create_app()
|
||
with app.app_context():
|
||
user = User(username='test_user', role='user')
|
||
db.session.add(user)
|
||
db.session.commit()
|
||
|
||
client = Client(name='Test Client', description='Test client')
|
||
db.session.add(client)
|
||
db.session.commit()
|
||
|
||
project = Project(name='Test Project', client_id=client.id, description='Test project')
|
||
db.session.add(project)
|
||
db.session.commit()
|
||
print('✅ Sample data created and validated successfully')
|
||
"
|
||
else
|
||
echo "ℹ️ No migration-related changes detected"
|
||
fi
|
||
|
||
- name: Run full test suite
|
||
env:
|
||
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
|
||
FLASK_APP: app.py
|
||
FLASK_ENV: testing
|
||
PYTHONPATH: ${{ github.workspace }}
|
||
run: |
|
||
pytest -v --cov=app --cov-report=xml --cov-report=html --cov-report=term \
|
||
--junitxml=junit.xml
|
||
|
||
- name: Upload full coverage
|
||
uses: codecov/codecov-action@v4
|
||
with:
|
||
files: ./coverage.xml
|
||
flags: full-suite
|
||
name: full-test-suite
|
||
|
||
- name: Upload full test results
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: full-test-results
|
||
path: |
|
||
htmlcov/
|
||
coverage.xml
|
||
junit.xml
|
||
|
||
- name: Publish full test results
|
||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||
if: always()
|
||
with:
|
||
files: junit.xml
|
||
check_name: Full Test Suite Results
|
||
|
||
# ============================================================================
|
||
# Test Summary and PR Comment
|
||
# ============================================================================
|
||
test-summary:
|
||
name: Test Summary
|
||
runs-on: ubuntu-latest
|
||
needs: [smoke-tests, unit-tests, integration-tests, security-tests, code-quality, docker-build, full-test-suite]
|
||
if: always() && github.event_name == 'pull_request'
|
||
permissions:
|
||
contents: read
|
||
pull-requests: write
|
||
issues: write
|
||
|
||
steps:
|
||
- name: Generate test summary
|
||
uses: actions/github-script@v7
|
||
with:
|
||
script: |
|
||
const jobs = [
|
||
{ name: 'Smoke Tests', result: '${{ needs.smoke-tests.result }}' },
|
||
{ name: 'Unit Tests', result: '${{ needs.unit-tests.result }}' },
|
||
{ name: 'Integration Tests', result: '${{ needs.integration-tests.result }}' },
|
||
{ name: 'Security Tests', result: '${{ needs.security-tests.result }}' },
|
||
{ name: 'Code Quality', result: '${{ needs.code-quality.result }}' },
|
||
{ name: 'Docker Build', result: '${{ needs.docker-build.result }}' },
|
||
{ name: 'Full Test Suite', result: '${{ needs.full-test-suite.result }}' }
|
||
];
|
||
|
||
const passed = jobs.filter(j => j.result === 'success').length;
|
||
const failed = jobs.filter(j => j.result === 'failure').length;
|
||
const total = jobs.length;
|
||
|
||
let emoji = failed === 0 ? '✅' : '❌';
|
||
let status = failed === 0 ? 'All tests passed!' : `${failed} test suite(s) failed`;
|
||
|
||
let commentBody = `## ${emoji} CI Test Results\n\n`;
|
||
commentBody += `**Overall Status:** ${status}\n\n`;
|
||
commentBody += `**Test Results:** ${passed}/${total} passed\n\n`;
|
||
commentBody += `### Test Suites:\n\n`;
|
||
|
||
for (const job of jobs) {
|
||
const icon = job.result === 'success' ? '✅' :
|
||
job.result === 'failure' ? '❌' :
|
||
job.result === 'skipped' ? '⏭️' : '⏸️';
|
||
commentBody += `- ${icon} ${job.name}: **${job.result}**\n`;
|
||
}
|
||
|
||
commentBody += `\n---\n`;
|
||
commentBody += `*Commit: ${context.sha.substring(0, 7)}*\n`;
|
||
commentBody += `*Workflow: [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`;
|
||
|
||
// Find existing comment
|
||
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 Test Results')
|
||
);
|
||
|
||
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,
|
||
});
|
||
}
|
||
|