From c7743091e314e4ccf91e5a3bf553c09d1182f2d4 Mon Sep 17 00:00:00 2001 From: "Gabe@w7dev" Date: Mon, 26 Jan 2026 08:12:58 +0000 Subject: [PATCH 1/3] ci: add GitHub Actions workflows for phase-1 readiness Add four new workflows: - schema-validation.yml: migration drift detection and cycle testing - dependency-check.yml: weekly pip-audit vulnerability scanning - phase-snapshot.yml: audit snapshots for phase-* branches - cd-release.yml: automated releases with release-please Add release-please configuration files for semantic versioning. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/cd-release.yml | 96 +++++++++ .github/workflows/dependency-check.yml | 147 ++++++++++++++ .github/workflows/phase-snapshot.yml | 253 ++++++++++++++++++++++++ .github/workflows/schema-validation.yml | 142 +++++++++++++ .release-please-manifest.json | 3 + release-please-config.json | 16 ++ 6 files changed, 657 insertions(+) create mode 100644 .github/workflows/cd-release.yml create mode 100644 .github/workflows/dependency-check.yml create mode 100644 .github/workflows/phase-snapshot.yml create mode 100644 .github/workflows/schema-validation.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml new file mode 100644 index 00000000..ec71bed3 --- /dev/null +++ b/.github/workflows/cd-release.yml @@ -0,0 +1,96 @@ +name: CD Release + +on: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + PYTHON_VERSION: "3.12" + UV_VERSION: "0.5" + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + version: ${{ steps.release.outputs.version }} + upload_url: ${{ steps.release.outputs.upload_url }} + + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + build-package: + name: Build Python Package + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install build dependencies + run: uv pip install build + + - name: Build package + run: | + python -m build + ls -la dist/ + + - name: Upload release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Upload all distribution files to the release + for file in dist/*; do + echo "Uploading $file..." + gh release upload "${{ needs.release-please.outputs.tag_name }}" "$file" --clobber + done + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-${{ needs.release-please.outputs.version }} + path: dist/ + retention-days: 90 + + - name: Generate release summary + run: | + echo "## Release ${{ needs.release-please.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tag: \`${{ needs.release-please.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Built Artifacts" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ls -la dist/ >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Artifacts uploaded to [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/${{ needs.release-please.outputs.tag_name }})" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml new file mode 100644 index 00000000..3c552b93 --- /dev/null +++ b/.github/workflows/dependency-check.yml @@ -0,0 +1,147 @@ +name: Dependency Security Check + +on: + schedule: + # Run weekly on Sunday at 00:00 UTC + - cron: '0 0 * * 0' + workflow_dispatch: + inputs: + fail_on_severity: + description: 'Minimum severity to fail on' + required: false + default: 'high' + type: choice + options: + - low + - medium + - high + - critical + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.12" + UV_VERSION: "0.5" + +jobs: + vulnerability-scan: + name: Python Vulnerability Scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: uv sync --frozen --all-extras --dev + + - name: Install pip-audit + run: uv pip install pip-audit + + - name: Run pip-audit (JSON output) + id: audit_json + continue-on-error: true + run: | + # Export requirements from uv lock file + uv export --format requirements-txt --all-extras > requirements-audit.txt + + # Run pip-audit with JSON output for artifact + uv run pip-audit \ + --requirement requirements-audit.txt \ + --format json \ + --output audit-results.json \ + --desc on \ + || true + + echo "Audit complete. Results saved to audit-results.json" + + - name: Run pip-audit (SARIF output) + id: audit_sarif + continue-on-error: true + run: | + # Run pip-audit with SARIF output for GitHub Security tab + uv run pip-audit \ + --requirement requirements-audit.txt \ + --format sarif \ + --output audit-results.sarif \ + --desc on \ + || true + + - name: Upload SARIF to GitHub Security + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: audit-results.sarif + category: dependency-vulnerability-scan + continue-on-error: true + + - name: Upload JSON audit artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: dependency-audit-${{ github.run_id }} + path: | + audit-results.json + requirements-audit.txt + retention-days: 90 + + - name: Analyze vulnerabilities + id: analyze + run: | + # Parse JSON and check for HIGH/CRITICAL vulnerabilities + SEVERITY_THRESHOLD="${{ github.event.inputs.fail_on_severity || 'high' }}" + + if [ ! -f audit-results.json ]; then + echo "No audit results found" + echo "has_critical=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Count vulnerabilities by severity + CRITICAL_COUNT=$(jq '[.dependencies[].vulns[]? | select(.fix_versions != null)] | map(select(.id | startswith("GHSA") or startswith("CVE") or startswith("PYSEC"))) | length' audit-results.json 2>/dev/null || echo "0") + TOTAL_VULNS=$(jq '[.dependencies[].vulns[]?] | length' audit-results.json 2>/dev/null || echo "0") + + echo "## Dependency Audit Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Total vulnerabilities found:** $TOTAL_VULNS" >> $GITHUB_STEP_SUMMARY + echo "- **Severity threshold:** $SEVERITY_THRESHOLD" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$TOTAL_VULNS" -gt 0 ]; then + echo "### Vulnerable Packages" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + jq '.dependencies[] | select(.vulns | length > 0) | {name, version, vulns: [.vulns[].id]}' audit-results.json >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + # Check if we should fail based on severity + # pip-audit doesn't provide severity, so we fail on any vulnerability for high/critical threshold + if [ "$SEVERITY_THRESHOLD" = "high" ] || [ "$SEVERITY_THRESHOLD" = "critical" ]; then + if [ "$TOTAL_VULNS" -gt 0 ]; then + echo "::warning::Vulnerabilities detected. Review audit-results.json for details." + echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT + fi + fi + else + echo "No vulnerabilities found." >> $GITHUB_STEP_SUMMARY + echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT + fi + + - name: Fail on vulnerabilities + if: steps.analyze.outputs.has_vulnerabilities == 'true' + run: | + echo "::error::Vulnerabilities detected that meet or exceed the severity threshold." + echo "Review the Security tab or download the audit artifact for details." + exit 1 diff --git a/.github/workflows/phase-snapshot.yml b/.github/workflows/phase-snapshot.yml new file mode 100644 index 00000000..8af7aec3 --- /dev/null +++ b/.github/workflows/phase-snapshot.yml @@ -0,0 +1,253 @@ +name: Phase Snapshot + +on: + push: + branches: + - 'phase-*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.12" + UV_VERSION: "0.5" + DATABASE_URL: postgresql+asyncpg://forecastlab:forecastlab@localhost:5432/forecastlab_snapshot_test + +jobs: + validate: + name: Full Validation + runs-on: ubuntu-latest + + services: + postgres: + image: pgvector/pgvector:pg16 + env: + POSTGRES_USER: forecastlab + POSTGRES_PASSWORD: forecastlab + POSTGRES_DB: forecastlab_snapshot_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + outputs: + lint_status: ${{ steps.lint.outcome }} + typecheck_status: ${{ steps.typecheck.outcome }} + test_status: ${{ steps.test.outcome }} + migration_status: ${{ steps.migration.outcome }} + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: uv sync --frozen --all-extras --dev + + - name: Lint check + id: lint + run: | + uv run ruff check . + uv run ruff format --check . + + - name: Type check + id: typecheck + run: | + uv run mypy app/ + uv run pyright app/ + + - name: Run migrations + id: migration + run: uv run alembic upgrade head + + - name: Run tests + id: test + env: + APP_ENV: testing + run: uv run pytest -v --tb=short + + create-snapshot: + name: Create Audit Snapshot + needs: validate + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Extract phase number + id: phase + run: | + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + PHASE_NUM=$(echo "$BRANCH_NAME" | sed -n 's/phase-\([0-9]*\).*/\1/p') + + if [ -z "$PHASE_NUM" ]; then + echo "::error::Could not extract phase number from branch: $BRANCH_NAME" + exit 1 + fi + + echo "phase_num=$PHASE_NUM" >> $GITHUB_OUTPUT + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Generate snapshot metadata + id: metadata + run: | + TIMESTAMP=$(date -u +%Y%m%d) + SHORT_SHA=$(git rev-parse --short HEAD) + FULL_SHA=$(git rev-parse HEAD) + TAG_NAME="phase-${{ steps.phase.outputs.phase_num }}-snapshot-${TIMESTAMP}-${SHORT_SHA}" + + echo "timestamp=$TIMESTAMP" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "full_sha=$FULL_SHA" >> $GITHUB_OUTPUT + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + + - name: Collect audit data + id: audit + run: | + # Collect comprehensive audit information + mkdir -p audit-output + + # Git information + cat > audit-output/audit-data.json << EOF + { + "snapshot": { + "tag": "${{ steps.metadata.outputs.tag_name }}", + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "phase": "${{ steps.phase.outputs.phase_num }}", + "branch": "${{ steps.phase.outputs.branch_name }}" + }, + "git": { + "sha": "${{ steps.metadata.outputs.full_sha }}", + "short_sha": "${{ steps.metadata.outputs.short_sha }}", + "author": "$(git log -1 --format='%an')", + "author_email": "$(git log -1 --format='%ae')", + "commit_message": $(git log -1 --format='%s' | jq -Rs .), + "commit_date": "$(git log -1 --format='%aI')" + }, + "validation": { + "lint": "${{ needs.validate.outputs.lint_status }}", + "typecheck": "${{ needs.validate.outputs.typecheck_status }}", + "tests": "${{ needs.validate.outputs.test_status }}", + "migrations": "${{ needs.validate.outputs.migration_status }}" + }, + "environment": { + "python_version": "${{ env.PYTHON_VERSION }}", + "uv_version": "${{ env.UV_VERSION }}", + "runner_os": "${{ runner.os }}" + }, + "workflow": { + "run_id": "${{ github.run_id }}", + "run_number": "${{ github.run_number }}", + "workflow": "${{ github.workflow }}", + "actor": "${{ github.actor }}" + } + } + EOF + + # File statistics + echo "" >> audit-output/audit-data.json + + # Dependency snapshot + uv pip freeze > audit-output/requirements-frozen.txt 2>/dev/null || uv export --format requirements-txt > audit-output/requirements-frozen.txt + + - name: Generate markdown report + run: | + cat > audit-output/SNAPSHOT-REPORT.md << 'EOF' + # Phase ${{ steps.phase.outputs.phase_num }} Snapshot Report + + ## Snapshot Information + + | Field | Value | + |-------|-------| + | **Tag** | `${{ steps.metadata.outputs.tag_name }}` | + | **Branch** | `${{ steps.phase.outputs.branch_name }}` | + | **Commit** | `${{ steps.metadata.outputs.full_sha }}` | + | **Date** | ${{ steps.metadata.outputs.timestamp }} | + | **Triggered by** | @${{ github.actor }} | + + ## Validation Results + + | Check | Status | + |-------|--------| + | Lint | ${{ needs.validate.outputs.lint_status == 'success' && '✅ Passed' || '❌ Failed' }} | + | Type Check | ${{ needs.validate.outputs.typecheck_status == 'success' && '✅ Passed' || '❌ Failed' }} | + | Tests | ${{ needs.validate.outputs.test_status == 'success' && '✅ Passed' || '❌ Failed' }} | + | Migrations | ${{ needs.validate.outputs.migration_status == 'success' && '✅ Passed' || '❌ Failed' }} | + + ## Environment + + - Python: ${{ env.PYTHON_VERSION }} + - uv: ${{ env.UV_VERSION }} + - Runner: ${{ runner.os }} + + ## Files + + - `audit-data.json` - Machine-readable audit data + - `requirements-frozen.txt` - Pinned dependencies at snapshot time + + --- + *Generated by GitHub Actions workflow `${{ github.workflow }}` run #${{ github.run_number }}* + EOF + + - name: Upload audit artifact + uses: actions/upload-artifact@v4 + with: + name: phase-${{ steps.phase.outputs.phase_num }}-snapshot-${{ steps.metadata.outputs.timestamp }} + path: audit-output/ + retention-days: 365 + + - name: Create annotated tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + TAG_MESSAGE="Phase ${{ steps.phase.outputs.phase_num }} snapshot + + Validation Results: + - Lint: ${{ needs.validate.outputs.lint_status }} + - Type Check: ${{ needs.validate.outputs.typecheck_status }} + - Tests: ${{ needs.validate.outputs.test_status }} + - Migrations: ${{ needs.validate.outputs.migration_status }} + + Workflow: ${{ github.workflow }} #${{ github.run_number }} + Actor: ${{ github.actor }}" + + git tag -a "${{ steps.metadata.outputs.tag_name }}" -m "$TAG_MESSAGE" + git push origin "${{ steps.metadata.outputs.tag_name }}" + + - name: Generate summary + run: | + echo "## Phase ${{ steps.phase.outputs.phase_num }} Snapshot Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tag: \`${{ steps.metadata.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Validation | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Lint | ${{ needs.validate.outputs.lint_status }} |" >> $GITHUB_STEP_SUMMARY + echo "| Type Check | ${{ needs.validate.outputs.typecheck_status }} |" >> $GITHUB_STEP_SUMMARY + echo "| Tests | ${{ needs.validate.outputs.test_status }} |" >> $GITHUB_STEP_SUMMARY + echo "| Migrations | ${{ needs.validate.outputs.migration_status }} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/schema-validation.yml b/.github/workflows/schema-validation.yml new file mode 100644 index 00000000..c8fe61a1 --- /dev/null +++ b/.github/workflows/schema-validation.yml @@ -0,0 +1,142 @@ +name: Schema Validation + +on: + push: + paths: + - 'alembic/**' + - 'app/**/models.py' + - 'app/core/database.py' + pull_request: + paths: + - 'alembic/**' + - 'app/**/models.py' + - 'app/core/database.py' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.12" + UV_VERSION: "0.5" + DATABASE_URL: postgresql+asyncpg://forecastlab:forecastlab@localhost:5432/forecastlab_schema_test + +jobs: + schema-validation: + name: Validate Database Schema + runs-on: ubuntu-latest + + services: + postgres: + image: pgvector/pgvector:pg16 + env: + POSTGRES_USER: forecastlab + POSTGRES_PASSWORD: forecastlab + POSTGRES_DB: forecastlab_schema_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + + - name: Set up Python + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: uv sync --frozen --all-extras --dev + + - name: Fresh DB migration test + run: | + echo "::group::Applying all migrations to fresh database" + uv run alembic upgrade head + echo "::endgroup::" + + - name: Check migration chain integrity + run: | + echo "::group::Verifying migration chain" + # Get all revision heads - should be exactly one + HEADS=$(uv run alembic heads 2>&1) + HEAD_COUNT=$(echo "$HEADS" | grep -c "^[a-f0-9]" || true) + + if [ "$HEAD_COUNT" -gt 1 ]; then + echo "::error::Multiple migration heads detected - branched history found" + echo "$HEADS" + exit 1 + fi + + echo "Migration chain integrity: OK (single head)" + echo "$HEADS" + echo "::endgroup::" + + - name: Schema drift detection + run: | + echo "::group::Checking for schema drift" + # alembic check compares models to current DB state + # Returns non-zero if autogenerate would create new migrations + if uv run alembic check 2>&1; then + echo "Schema is in sync with models" + else + echo "::error::Schema drift detected - models don't match migrations" + echo "Run 'alembic revision --autogenerate' to generate missing migrations" + exit 1 + fi + echo "::endgroup::" + + - name: Downgrade/upgrade cycle test + run: | + echo "::group::Testing migration reversibility" + # Get current revision + CURRENT=$(uv run alembic current 2>&1 | grep -oE "^[a-f0-9]+" | head -1) + + if [ -z "$CURRENT" ]; then + echo "No migrations applied, skipping cycle test" + exit 0 + fi + + echo "Current revision: $CURRENT" + + # Downgrade one step + echo "Downgrading one migration..." + uv run alembic downgrade -1 + + # Upgrade back + echo "Upgrading back to head..." + uv run alembic upgrade head + + # Verify we're back at head + FINAL=$(uv run alembic current 2>&1 | grep -oE "^[a-f0-9]+" | head -1) + + if [ "$CURRENT" != "$FINAL" ]; then + echo "::error::Migration cycle failed - revision mismatch after downgrade/upgrade" + echo "Expected: $CURRENT, Got: $FINAL" + exit 1 + fi + + echo "Migration cycle test: PASSED" + echo "::endgroup::" + + - name: Generate schema report + if: always() + run: | + echo "## Schema Validation Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Migration History" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + uv run alembic history --verbose 2>&1 | head -50 >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Current State" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + uv run alembic current --verbose 2>&1 >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..146b6213 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..64b117f0 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "python", + "package-name": "forecastlabai", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "include-component-in-tag": false, + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "pyproject.toml" + ] + } + } +} From 3cdc593a880be850521666d683e084c0742e4690 Mon Sep 17 00:00:00 2001 From: "Gabe@w7dev" Date: Mon, 26 Jan 2026 08:18:38 +0000 Subject: [PATCH 2/3] fix(ci): address sourcery-ai review comments on dependency-check - Replace misleading fail_on_severity input with fail_on_vulnerabilities boolean (pip-audit doesn't provide severity data) - Remove unused CRITICAL_COUNT variable and simplify jq logic - Remove redundant || true from pip-audit commands (continue-on-error already handles failures while preserving exit code visibility) - Add explicit error when audit-results.json is missing Co-Authored-By: Claude Opus 4.5 --- .github/workflows/dependency-check.yml | 51 +++++++++++--------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index 3c552b93..d736c940 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -6,16 +6,11 @@ on: - cron: '0 0 * * 0' workflow_dispatch: inputs: - fail_on_severity: - description: 'Minimum severity to fail on' + fail_on_vulnerabilities: + description: 'Fail workflow if vulnerabilities are found' required: false - default: 'high' - type: choice - options: - - low - - medium - - high - - critical + default: true + type: boolean concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -59,12 +54,12 @@ jobs: uv export --format requirements-txt --all-extras > requirements-audit.txt # Run pip-audit with JSON output for artifact + # Note: continue-on-error allows workflow to proceed; exit code preserved in step outcome uv run pip-audit \ --requirement requirements-audit.txt \ --format json \ --output audit-results.json \ - --desc on \ - || true + --desc on echo "Audit complete. Results saved to audit-results.json" @@ -77,8 +72,7 @@ jobs: --requirement requirements-audit.txt \ --format sarif \ --output audit-results.sarif \ - --desc on \ - || true + --desc on - name: Upload SARIF to GitHub Security if: always() @@ -101,23 +95,22 @@ jobs: - name: Analyze vulnerabilities id: analyze run: | - # Parse JSON and check for HIGH/CRITICAL vulnerabilities - SEVERITY_THRESHOLD="${{ github.event.inputs.fail_on_severity || 'high' }}" + # Determine if we should fail on vulnerabilities (default: true for scheduled runs) + FAIL_ON_VULNS="${{ github.event.inputs.fail_on_vulnerabilities || 'true' }}" if [ ! -f audit-results.json ]; then - echo "No audit results found" - echo "has_critical=false" >> $GITHUB_OUTPUT - exit 0 + echo "::error::Audit results file not found - pip-audit may have failed to run" + echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT + exit 1 fi - # Count vulnerabilities by severity - CRITICAL_COUNT=$(jq '[.dependencies[].vulns[]? | select(.fix_versions != null)] | map(select(.id | startswith("GHSA") or startswith("CVE") or startswith("PYSEC"))) | length' audit-results.json 2>/dev/null || echo "0") + # Count total vulnerabilities TOTAL_VULNS=$(jq '[.dependencies[].vulns[]?] | length' audit-results.json 2>/dev/null || echo "0") echo "## Dependency Audit Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- **Total vulnerabilities found:** $TOTAL_VULNS" >> $GITHUB_STEP_SUMMARY - echo "- **Severity threshold:** $SEVERITY_THRESHOLD" >> $GITHUB_STEP_SUMMARY + echo "- **Fail on vulnerabilities:** $FAIL_ON_VULNS" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "$TOTAL_VULNS" -gt 0 ]; then @@ -126,13 +119,12 @@ jobs: jq '.dependencies[] | select(.vulns | length > 0) | {name, version, vulns: [.vulns[].id]}' audit-results.json >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - # Check if we should fail based on severity - # pip-audit doesn't provide severity, so we fail on any vulnerability for high/critical threshold - if [ "$SEVERITY_THRESHOLD" = "high" ] || [ "$SEVERITY_THRESHOLD" = "critical" ]; then - if [ "$TOTAL_VULNS" -gt 0 ]; then - echo "::warning::Vulnerabilities detected. Review audit-results.json for details." - echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT - fi + echo "::warning::$TOTAL_VULNS vulnerability(ies) detected. Review audit-results.json for details." + + if [ "$FAIL_ON_VULNS" = "true" ]; then + echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT + else + echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT fi else echo "No vulnerabilities found." >> $GITHUB_STEP_SUMMARY @@ -142,6 +134,5 @@ jobs: - name: Fail on vulnerabilities if: steps.analyze.outputs.has_vulnerabilities == 'true' run: | - echo "::error::Vulnerabilities detected that meet or exceed the severity threshold." - echo "Review the Security tab or download the audit artifact for details." + echo "::error::Vulnerabilities detected. Review the Security tab or download the audit artifact for details." exit 1 From 0392e87e8fd2fc39fa7f63ba202d7faf5d0df7f4 Mon Sep 17 00:00:00 2001 From: "Gabe@w7dev" Date: Mon, 26 Jan 2026 08:23:23 +0000 Subject: [PATCH 3/3] docs: add mermaid diagrams for GitHub Actions workflows Extract and save sourcery-ai generated diagrams: - cd-release-sequence.md: actor interactions during release - cd-release-flow.md: release workflow job flow - phase-snapshot-flow.md: phase-* branch snapshot flow - schema-validation-flow.md: migration/drift check flow - dependency-check-flow.md: pip-audit scan flow (updated) - README.md: index with workflow summary Co-Authored-By: Claude Opus 4.5 --- docs/github/diagrams/README.md | 42 +++++++++++++++++++ docs/github/diagrams/cd-release-flow.md | 31 ++++++++++++++ docs/github/diagrams/cd-release-sequence.md | 38 +++++++++++++++++ docs/github/diagrams/dependency-check-flow.md | 31 ++++++++++++++ docs/github/diagrams/phase-snapshot-flow.md | 42 +++++++++++++++++++ .../github/diagrams/schema-validation-flow.md | 25 +++++++++++ 6 files changed, 209 insertions(+) create mode 100644 docs/github/diagrams/README.md create mode 100644 docs/github/diagrams/cd-release-flow.md create mode 100644 docs/github/diagrams/cd-release-sequence.md create mode 100644 docs/github/diagrams/dependency-check-flow.md create mode 100644 docs/github/diagrams/phase-snapshot-flow.md create mode 100644 docs/github/diagrams/schema-validation-flow.md diff --git a/docs/github/diagrams/README.md b/docs/github/diagrams/README.md new file mode 100644 index 00000000..548df6f1 --- /dev/null +++ b/docs/github/diagrams/README.md @@ -0,0 +1,42 @@ +# GitHub Actions Workflow Diagrams + +Mermaid diagrams documenting the CI/CD workflow architecture. + +## Diagrams + +| Diagram | Description | +|---------|-------------| +| [cd-release-sequence.md](cd-release-sequence.md) | Sequence diagram showing actor interactions during release creation | +| [cd-release-flow.md](cd-release-flow.md) | Flow diagram of the CD release workflow jobs | +| [phase-snapshot-flow.md](phase-snapshot-flow.md) | Flow diagram of the phase-* branch snapshot workflow | +| [schema-validation-flow.md](schema-validation-flow.md) | Flow diagram of the schema validation workflow | +| [dependency-check-flow.md](dependency-check-flow.md) | Flow diagram of the dependency security scan workflow | + +## Workflow Summary + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ GitHub Actions Workflows │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Push to main ──────────► cd-release.yml │ +│ └── release-please + package build │ +│ │ +│ Push to phase-* ───────► phase-snapshot.yml │ +│ └── full validation + audit tag │ +│ │ +│ Push/PR to alembic/ ───► schema-validation.yml │ +│ or models.py └── migration + drift checks │ +│ │ +│ Weekly / Manual ───────► dependency-check.yml │ +│ └── pip-audit + SARIF upload │ +│ │ +│ Push/PR to main/dev ───► ci.yml │ +│ └── lint + typecheck + test │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Source + +These diagrams were generated by [sourcery-ai](https://sourcery.ai) during PR review and saved for documentation purposes. diff --git a/docs/github/diagrams/cd-release-flow.md b/docs/github/diagrams/cd-release-flow.md new file mode 100644 index 00000000..a6d17c93 --- /dev/null +++ b/docs/github/diagrams/cd-release-flow.md @@ -0,0 +1,31 @@ +# CD Release - Flow Diagram + +Flow diagram for CD Release workflow. + +```mermaid +flowchart TD + TriggerMain["Push to main"] --> ReleaseJob + + subgraph ReleaseJob["Job: release-please"] + R1["Run googleapis/release-please-action"] --> R2["Read release-please-config.json and .release-please-manifest.json"] + R2 --> R3["Create or update Release PR and Git tag"] + R3 --> R4["Set outputs: release_created, tag_name, version, upload_url"] + end + + ReleaseJob -->|release_created == 'true'| BuildJob + + subgraph BuildJob["Job: build-package"] + B1["Checkout code at released tag"] --> B2["Install uv and Python 3.12"] + B2 --> B3["Install build dependency via uv pip"] + B3 --> B4["Build Python package: python -m build (dist/*)"] + B4 --> B5["Upload artifacts to GitHub Release using gh release upload"] + B5 --> B6["Upload dist/ as Actions artifact"] + B6 --> B7["Generate GitHub summary with tag, version, and artifact listing"] + end +``` + +## Related Files + +- `.github/workflows/cd-release.yml` +- `release-please-config.json` +- `.release-please-manifest.json` diff --git a/docs/github/diagrams/cd-release-sequence.md b/docs/github/diagrams/cd-release-sequence.md new file mode 100644 index 00000000..dcf4d207 --- /dev/null +++ b/docs/github/diagrams/cd-release-sequence.md @@ -0,0 +1,38 @@ +# CD Release - Sequence Diagram + +Sequence diagram for CD release creation and packaging. + +```mermaid +sequenceDiagram + actor Dev + participant GitHub + participant ReleaseWorkflow as Release_please_workflow + participant BuildWorkflow as Build_package_workflow + participant ReleasePlease as release_please_action + participant GHReleases as GitHub_Releases + + Dev->>GitHub: Push commit to main + GitHub->>ReleaseWorkflow: Trigger CD Release workflow + + ReleaseWorkflow->>ReleasePlease: Run release-please-action with config/manifest + ReleasePlease-->>ReleaseWorkflow: Outputs release_created, tag_name, version + + alt Release created + ReleaseWorkflow-->>BuildWorkflow: needs release-please with release_created == true + GitHub->>BuildWorkflow: Start build-package job + + BuildWorkflow->>GitHub: Checkout repository at tag_name + BuildWorkflow->>BuildWorkflow: Install uv, Python, build dependency + BuildWorkflow->>BuildWorkflow: Build distribution artifacts (dist/*) + BuildWorkflow->>GHReleases: Upload dist/* to release tag + BuildWorkflow->>GitHub: Upload dist/ as workflow artifact + else No release created + ReleaseWorkflow-->>Dev: No new release (no changes or already released) + end +``` + +## Related Files + +- `.github/workflows/cd-release.yml` +- `release-please-config.json` +- `.release-please-manifest.json` diff --git a/docs/github/diagrams/dependency-check-flow.md b/docs/github/diagrams/dependency-check-flow.md new file mode 100644 index 00000000..7ba4381e --- /dev/null +++ b/docs/github/diagrams/dependency-check-flow.md @@ -0,0 +1,31 @@ +# Dependency Security Check - Flow Diagram + +Flow diagram for Dependency Security Check workflow. + +```mermaid +flowchart TD + TriggerCron["Weekly schedule (Sun 00:00 UTC)"] --> ScanJob + TriggerManual["workflow_dispatch with fail_on_vulnerabilities"] --> ScanJob + + subgraph ScanJob["Job: vulnerability-scan (Python Vulnerability Scan)"] + D1["Checkout code"] --> D2["Install uv and Python 3.12"] + D2 --> D3["uv sync dependencies (dev + extras)"] + D3 --> D4["Install pip-audit via uv pip"] + + D4 --> D5["Export requirements from uv lock to requirements-audit.txt"] + D5 --> D6["Run pip-audit JSON, write audit-results.json"] + D6 --> D7["Run pip-audit SARIF, write audit-results.sarif"] + + D7 --> D8["Upload SARIF to GitHub Security tab"] + D6 --> D9["Upload JSON audit artifact and requirements-audit.txt"] + + D9 --> D10["Analyze vulnerabilities using jq, write summary and has_vulnerabilities output"] + D10 --> D11{"fail_on_vulnerabilities?"} + D11 -->|true + vulns found| D12["Fail job"] + D11 -->|false or no vulns| D13["Pass job"] + end +``` + +## Related Files + +- `.github/workflows/dependency-check.yml` diff --git a/docs/github/diagrams/phase-snapshot-flow.md b/docs/github/diagrams/phase-snapshot-flow.md new file mode 100644 index 00000000..4f20f618 --- /dev/null +++ b/docs/github/diagrams/phase-snapshot-flow.md @@ -0,0 +1,42 @@ +# Phase Snapshot - Flow Diagram + +Flow diagram for Phase Snapshot workflow jobs. + +```mermaid +flowchart TD + Trigger["Push to phase-* branch"] --> ValidateJob + + subgraph ValidateJob["Job: validate (Full Validation)"] + V1["Checkout code"] --> V2["Start Postgres pgvector service"] + V2 --> V3["Install uv and Python 3.12"] + V3 --> V4["uv sync dependencies (dev + extras)"] + V4 --> V5["Lint: ruff check + format --check"] + V5 --> V6["Type check: mypy app/ and pyright app/"] + V6 --> V7["Run migrations: alembic upgrade head"] + V7 --> V8["Run tests: pytest -v"] + + V5 -->|lint_status| ValidateOutputs + V6 -->|typecheck_status| ValidateOutputs + V7 -->|migration_status| ValidateOutputs + V8 -->|test_status| ValidateOutputs + end + + ValidateOutputs["Expose job outputs: lint/typecheck/test/migration statuses"] --> SnapshotJob + + subgraph SnapshotJob["Job: create-snapshot (Audit Snapshot)"] + S1["Checkout code with full history"] --> S2["Install uv and Python 3.12"] + S2 --> S3["Extract phase number from branch name"] + S3 --> S4["Generate snapshot metadata (timestamp, SHAs, tag name)"] + S4 --> S5["Collect audit data: audit-data.json and requirements-frozen.txt"] + S5 --> S6["Generate SNAPSHOT-REPORT.md"] + S6 --> S7["Upload audit artifact (1 year retention)"] + S7 --> S8["Create annotated git tag and push"] + S8 --> S9["Write GitHub Actions summary"] + end + + ValidateJob -->|needs validate| SnapshotJob +``` + +## Related Files + +- `.github/workflows/phase-snapshot.yml` diff --git a/docs/github/diagrams/schema-validation-flow.md b/docs/github/diagrams/schema-validation-flow.md new file mode 100644 index 00000000..01258c03 --- /dev/null +++ b/docs/github/diagrams/schema-validation-flow.md @@ -0,0 +1,25 @@ +# Schema Validation - Flow Diagram + +Flow diagram for Schema Validation workflow. + +```mermaid +flowchart TD + TriggerPush["Push affecting alembic/models/database"] --> SchemaJob + TriggerPR["PR affecting alembic/models/database"] --> SchemaJob + + subgraph SchemaJob["Job: schema-validation (Validate Database Schema)"] + S1["Checkout code"] --> S2["Start Postgres pgvector schema DB"] + S2 --> S3["Install uv and Python 3.12"] + S3 --> S4["uv sync dependencies (dev + extras)"] + + S4 --> S5["Fresh DB migration test: alembic upgrade head"] + S5 --> S6["Check migration chain: alembic heads single head enforcement"] + S6 --> S7["Schema drift detection: alembic check"] + S7 --> S8["Downgrade/upgrade cycle: downgrade -1 then upgrade head and compare revisions"] + S8 --> S9["Generate schema report in GitHub summary (history + current)"] + end +``` + +## Related Files + +- `.github/workflows/schema-validation.yml`