CI/CD Pipeline #3422
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: ["**"] | |
| tags: ["v*"] | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| # Run quality gates daily at 2 AM UTC | |
| - cron: "0 2 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| run_quality_gates: | |
| description: "Run comprehensive quality gates" | |
| required: false | |
| default: false | |
| type: boolean | |
| # Group runs by workflow + branch/PR and cancel in-progress runs on new pushes | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| NODE_VERSION: "20" | |
| PNPM_VERSION: "10.18.3" | |
| NODE_OPTIONS: "--max-old-space-size=8192" | |
| # Disable Nx daemon in CI to prevent hanging issues | |
| NX_DAEMON: false | |
| # Skip Husky hooks in CI to prevent build loops | |
| HUSKY: 0 | |
| CI: true | |
| # E2E test parallelism - number of shards for Playwright test distribution | |
| # Reduced from 20 to 12 due to systematic failures with 20 shards (resource contention) | |
| E2E_SHARDS: 12 | |
| # Option B: Disable database cache if Option A (caching full .nx dir) doesn't work | |
| # Uncomment these three lines to fall back to legacy file-based caching: | |
| # NX_DISABLE_DB: true | |
| # NX_DB_CACHE: false | |
| # NX_REJECT_UNKNOWN_LOCAL_CACHE: false | |
| jobs: | |
| # Syncpack - validates and auto-fixes dependency versions | |
| syncpack: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Node.js with pnpm cache | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| # Cache full .nx dir (includes workspace-data DB) for cross-run cache sharing | |
| path: .nx | |
| # More precise cache key - only hash files that actually affect task execution | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('nx.json', '**/project.json', 'package.json') }} | |
| # Fallback to broader keys for better cache hits | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3-${{ hashFiles('nx.json', '**/project.json') }} | |
| ${{ runner.os }}-nx-v3- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Check dependency versions (PRs) | |
| if: github.event_name == 'pull_request' | |
| run: pnpm nx syncpack:lint bibgraph | |
| - name: Fix dependency versions (main branch) | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| run: | | |
| pnpm syncpack fix-mismatches | |
| pnpm syncpack format | |
| - name: Check for changes | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| id: changes | |
| run: | | |
| if git diff --quiet; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Update lockfile and commit fixes | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.changes.outputs.changed == 'true' | |
| run: | | |
| pnpm install --no-frozen-lockfile | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add -A | |
| git commit -m "chore(deps): auto-fix syncpack mismatches [skip ci]" | |
| git push | |
| # Determine next version using semantic-release dry-run | |
| # This runs BEFORE validate so we can build with the correct version | |
| determine-version: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| outputs: | |
| next-version: ${{ steps.version.outputs.next-version }} | |
| should-release: ${{ steps.version.outputs.should-release }} | |
| updated-package-json: ${{ steps.version.outputs.updated-package-json }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Determine and update next version | |
| id: version | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Run semantic-release in dry-run mode to get the next version | |
| OUTPUT=$(npx semantic-release --dry-run 2>&1) || true | |
| # Extract version from output (looks for "The next release version is X.X.X") | |
| VERSION=$(echo "$OUTPUT" | grep -oP "The next release version is \K[0-9]+\.[0-9]+\.[0-9]+" || echo "") | |
| if [ -n "$VERSION" ]; then | |
| # Write outputs (key=value pairs only) | |
| { | |
| echo "next-version=$VERSION" | |
| echo "should-release=true" | |
| echo "updated-package-json=true" | |
| } >> "$GITHUB_OUTPUT" | |
| # Log informational messages to stdout (not to GITHUB_OUTPUT) | |
| echo "Next version will be: $VERSION" | |
| # Update package.json with next version for build | |
| jq --arg version "$VERSION" '.version = $version' package.json > package.json.tmp | |
| mv package.json.tmp package.json | |
| echo "Updated package.json with version: $VERSION" | |
| # Commit and push the version update | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add package.json | |
| git commit -m "chore(ci): bump version to $VERSION for build [skip ci]" | |
| git push origin main | |
| else | |
| # No new release needed, use current version from package.json | |
| VERSION=$(jq -r .version package.json) | |
| # Write outputs (key=value pairs only) | |
| { | |
| echo "next-version=$VERSION" | |
| echo "should-release=false" | |
| echo "updated-package-json=false" | |
| } >> "$GITHUB_OUTPUT" | |
| # Log informational message to stdout | |
| echo "No new release needed, using current version: $VERSION" | |
| fi | |
| # Main validation job - build, typecheck, lint, and all tests in one job | |
| # Consolidating reduces setup overhead and lets Nx fully optimize the task graph | |
| validate: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| needs: [determine-version] | |
| # Run even when determine-version is skipped (for PRs/non-main branches) | |
| if: always() && !cancelled() | |
| outputs: | |
| should-deploy: ${{ steps.deploy.outputs.should-deploy }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Node.js with pnpm cache | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| # Cache full .nx dir (includes workspace-data DB) for cross-run cache sharing | |
| path: .nx | |
| # More precise cache key - only hash files that actually affect task execution | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('nx.json', '**/project.json', 'package.json') }} | |
| # Fallback to broader keys for better cache hits | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3-${{ hashFiles('nx.json', '**/project.json') }} | |
| ${{ runner.os }}-nx-v3- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build and typecheck all projects | |
| timeout-minutes: 25 | |
| env: | |
| GITHUB_PAGES: true | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| # Pass version from determine-version job to vite build | |
| NEXT_RELEASE_VERSION: ${{ needs.determine-version.outputs.next-version }} | |
| run: pnpm nx run-many -t build typecheck --parallel=3 | |
| - name: Lint all projects | |
| timeout-minutes: 20 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| run: pnpm nx run-many -t lint --parallel=3 | |
| - name: Run tests | |
| timeout-minutes: 6 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=3072 | |
| NX_PARALLEL: 1 | |
| run: | | |
| # CI ULTRA-OPTIMIZED: Run only true unit tests with coverage, exclude all component/integration tests | |
| # Run packages individually for precise control | |
| pnpm nx test:coverage client --skip-nx-cache & | |
| pnpm nx test:coverage utils --skip-nx-cache & | |
| pnpm nx test:coverage ui --skip-nx-cache & | |
| pnpm nx test:coverage cli --skip-nx-cache & | |
| # For web app, run only unit tests with coverage (not component/integration) | |
| pnpm nx test:coverage web --skip-nx-cache --testNamePattern="\.unit\.test\." & | |
| wait | |
| - name: Merge coverage reports | |
| run: | | |
| echo "=== Merging coverage reports from all packages ===" | |
| mkdir -p coverage | |
| # Copy all coverage directories to root | |
| for dir in packages/*/coverage apps/*/coverage; do | |
| if [ -d "$dir" ]; then | |
| echo "Found coverage: $dir" | |
| # Copy contents, not the directory itself | |
| cp -r "$dir"/* coverage/ 2>/dev/null || true | |
| fi | |
| done | |
| # List what we have | |
| if [ -d "coverage" ] && [ "$(ls -A coverage 2>/dev/null)" ]; then | |
| echo "✅ Coverage merged successfully" | |
| ls -la coverage/ | |
| else | |
| echo "⚠️ No coverage files found to merge" | |
| fi | |
| - name: Verify build artifacts before upload | |
| run: | | |
| echo "=== Verifying build artifacts before upload ===" | |
| if [ ! -d "dist" ]; then | |
| echo "❌ dist directory not found" | |
| exit 1 | |
| fi | |
| echo "✅ dist directory found" | |
| find dist -maxdepth 2 -type f -o -type d | head -20 | |
| if [ ! -d "dist/apps" ]; then | |
| echo "❌ dist/apps directory not found" | |
| exit 1 | |
| fi | |
| echo "✅ dist/apps directory found" | |
| if [ ! -d "dist/apps/web" ]; then | |
| echo "❌ dist/apps/web directory not found" | |
| exit 1 | |
| fi | |
| echo "✅ dist/apps/web directory found" | |
| echo "=== Build artifacts verified successfully ===" | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: build-artifacts | |
| path: dist/ | |
| retention-days: 1 | |
| - name: Upload coverage | |
| uses: actions/upload-artifact@v5 | |
| if: always() | |
| with: | |
| name: coverage-all | |
| path: coverage/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| - name: Determine deployment | |
| id: deploy | |
| run: | | |
| if { [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; } && [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| echo "should-deploy=true" >> "${GITHUB_OUTPUT}" | |
| else | |
| echo "should-deploy=false" >> "${GITHUB_OUTPUT}" | |
| fi | |
| # Security audit - Check for vulnerabilities (runs in parallel with validate) | |
| security-audit: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Node.js with pnpm cache | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: .nx | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('.github/workflows/ci.yml', 'nx.json', '**/project.json', 'pnpm-lock.yaml', 'tsconfig*.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Run security audit | |
| run: pnpm audit --audit-level moderate | |
| # Setup E2E sharding matrix dynamically from E2E_SHARDS env var | |
| setup-e2e: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| outputs: | |
| shards: ${{ steps.set-matrix.outputs.shards }} | |
| steps: | |
| - name: Generate shard matrix | |
| id: set-matrix | |
| run: | | |
| shards="[$(seq -s, 1 "$E2E_SHARDS")]" | |
| echo "shards=$shards" >> "$GITHUB_OUTPUT" | |
| echo "Generated shard matrix: $shards" | |
| # E2E tests - DISABLED - Full application tests (run after validate) | |
| # Uses matrix sharding to run tests in parallel across E2E_SHARDS runners | |
| # Commented out to disable - currently failing due to preview server issues | |
| # e2e: | |
| # runs-on: ubuntu-latest | |
| # timeout-minutes: 45 | |
| # needs: [validate, setup-e2e] | |
| # if: always() && needs.validate.result == 'success' | |
| # strategy: | |
| # fail-fast: true | |
| # matrix: | |
| # shard: ${{ fromJson(needs.setup-e2e.outputs.shards) }} | |
| # steps: | |
| # - name: Checkout | |
| # uses: actions/checkout@v6 | |
| # - name: Setup Node.js | |
| # uses: actions/setup-node@v6 | |
| # with: | |
| # node-version: ${{ env.NODE_VERSION }} | |
| # - name: Setup pnpm | |
| # uses: pnpm/action-setup@v3 | |
| # with: | |
| # version: ${{ env.PNPM_VERSION }} | |
| # - name: Install dependencies | |
| # run: pnpm install --frozen-lockfile | |
| # - name: Download build artifacts | |
| # uses: actions/download-artifact@v6 | |
| # with: | |
| # name: build-artifacts | |
| # path: . | |
| # - name: Validate build artifacts | |
| # run: | | |
| # if [ ! -d "apps/web/dist" ]; then | |
| # echo "Error: Build artifacts missing - apps/web/dist not found" | |
| # exit 1 | |
| # fi | |
| # echo "Build artifacts validated successfully" | |
| # - name: Cache Playwright browsers | |
| # uses: actions/cache@v4 | |
| # with: | |
| # path: ~/.cache/ms-playwright | |
| # key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} | |
| # restore-keys: | | |
| # playwright-${{ runner.os }}- | |
| # - name: Install Playwright browsers | |
| # run: pnpm exec playwright install --with-deps | |
| # - name: Start preview server for E2E tests | |
| # run: | | |
| # cd apps/web | |
| # # Use Python's built-in HTTP server to serve pre-built artifacts | |
| # # This is much faster than pnpm preview which rebuilds everything | |
| # python3 -m http.server 4173 --directory dist & | |
| # echo "Static file server starting on port 4173..." | |
| # # Wait for server to be ready (should be very quick with static server) | |
| # timeout 10 bash -c 'until curl -s http://localhost:4173 > /dev/null; do sleep 1; done' | |
| # echo "✅ Static file server is ready on http://localhost:4173" | |
| # - name: Run E2E tests (shard ${{ matrix.shard }}/${{ env.E2E_SHARDS }}) | |
| # run: | | |
| # cd apps/web | |
| # # Run Playwright tests with JSON output to temporary file to avoid corruption | |
| # # Use list reporter for console output, JSON reporter for clean results | |
| # pnpm exec playwright test \ | |
| # --shard=${{ matrix.shard }}/${{ env.E2E_SHARDS }} \ | |
| # --reporter=list,json \ | |
| # 2>test-results/console-output.log \ | |
| # || { | |
| # TEST_EXIT_CODE=$? | |
| # echo "Playwright tests completed with exit code: $TEST_EXIT_CODE" | |
| # # Copy the JSON results to expected location if it exists | |
| # if [ -f "test-results/results.json" ]; then | |
| # echo "✅ JSON results file created successfully" | |
| # # Validate JSON integrity | |
| # if jq empty "test-results/results.json" 2>/dev/null; then | |
| # echo "✅ JSON results file is valid" | |
| # else | |
| # echo "⚠️ JSON results file is corrupted, showing first 200 characters:" | |
| # head -200 "test-results/results.json" | cat -v | |
| # fi | |
| # else | |
| # echo "⚠️ No JSON results file found" | |
| # fi | |
| # # Analyze the actual test results to determine if this is a real failure | |
| # echo "🔍 Analyzing test results to distinguish between flaky passes and actual failures..." | |
| # # Check for various possible result file locations | |
| # RESULTS_FILE="" | |
| # if [ -f "test-results/results.json" ]; then | |
| # RESULTS_FILE="test-results/results.json" | |
| # elif [ -f "blob-report/results.json" ]; then | |
| # RESULTS_FILE="blob-report/results.json" | |
| # else | |
| # # Look for any JSON files that might contain results | |
| # RESULTS_FILE=$(find test-results -name "*.json" -type f -exec file {} \; | grep "JSON" | cut -d: -f1 | head -1) | |
| # fi | |
| # if [ -n "$RESULTS_FILE" ] && [ -f "$RESULTS_FILE" ]; then | |
| # echo "📄 Found results file: $RESULTS_FILE" | |
| # # Try to parse the JSON results safely | |
| # if jq empty "$RESULTS_FILE" 2>/dev/null; then | |
| # echo "✅ Results file is valid JSON, analyzing..." | |
| # # Get test summary from the results | |
| # TOTAL_SPECS=$(jq -r '[.projects[]?.suites[]?.specs[]?] | length' "$RESULTS_FILE" 2>/dev/null || echo "0") | |
| # TOTAL_TESTS=$(jq -r '[.projects[]?.suites[]?.specs[]?.tests[]?] | length' "$RESULTS_FILE" 2>/dev/null || echo "0") | |
| # echo "📊 Test Statistics:" | |
| # echo " Total test specs: $TOTAL_SPECS" | |
| # echo " Total individual tests: $TOTAL_TESTS" | |
| # if [ "$TOTAL_TESTS" -gt 0 ]; then | |
| # # Count different test outcomes | |
| # PASSED_TESTS=$(jq -r '[.projects[]?.suites[]?.specs[]?.tests[]? | select(.results[-1].status == "passed")] | length' "$RESULTS_FILE" 2>/dev/null || echo "0") | |
| # FAILED_TESTS=$(jq -r '[.projects[]?.suites[]?.specs[]?.tests[]? | select(.results[-1].status == "unexpected")] | length' "$RESULTS_FILE" 2>/dev/null || echo "0") | |
| # SKIPPED_TESTS=$(jq -r '[.projects[]?.suites[]?.specs[]?.tests[]? | select(.results[-1].status == "skipped")] | length' "$RESULTS_FILE" 2>/dev/null || echo "0") | |
| # FLAKY_TESTS=$(jq -r '[.projects[]?.suites[]?.specs[]?.tests[]? | select(.results | length > 1)] | length' "$RESULTS_FILE" 2>/dev/null || echo "0") | |
| # echo " Passed: $PASSED_TESTS" | |
| # echo " Failed: $FAILED_TESTS" | |
| # echo " Skipped: $SKIPPED_TESTS" | |
| # echo " Flaky: $FLAKY_TESTS" | |
| # # Alternative: Check if JSON structure is what we expect | |
| # # If not, fall back to console output analysis | |
| # if [ "$TOTAL_TESTS" -gt 0 ]; then | |
| # echo "✅ Found test data in JSON, analyzing..." | |
| # # The key logic: if there are no failed tests, treat as success | |
| # if [ "$FAILED_TESTS" -eq 0 ]; then | |
| # echo "✅ All tests passed (some may have been flaky but eventually succeeded)." | |
| # echo "🎉 Treating the test run as successful." | |
| # exit 0 | |
| # else | |
| # echo "❌ Found $FAILED_TESTS failed test(s). This represents actual test failures." | |
| # echo "🔍 Failed tests:" | |
| # jq -r '.projects[]?.suites[]?.specs[]?.tests[]? | select(.results[-1].status == "unexpected") | " - \(.title // \"Unnamed test\")"' "$RESULTS_FILE" 2>/dev/null || echo " (Could not extract test names)" | |
| # echo "💾 Check the uploaded HTML report for detailed failure information." | |
| # exit 1 | |
| # fi | |
| # else | |
| # echo "⚠️ JSON structure different than expected. Falling back to console output analysis..." | |
| # # Look for pass/fail counts in console output if captured | |
| # echo "🔍 Playwright exit code: $TEST_EXIT_CODE" | |
| # echo "📊 Treating as failure due to parsing issues." | |
| # exit 1 | |
| # fi | |
| # else | |
| # echo "⚠️ No tests were found in the JSON results. This might indicate:" | |
| # echo " - Test discovery issues" | |
| # echo " - Configuration problems" | |
| # echo " - JSON reporter failure due to static caching (known issue)" | |
| # echo " - Empty test shard (acceptable when manual tests are excluded)" | |
| # echo "🔍 Playwright exit code: $TEST_EXIT_CODE" | |
| # # Check console output for test results when JSON fails (static caching issue) | |
| # # This handles the case where tests run successfully but JSON reporter fails | |
| # echo "🔍 Analyzing console output to detect actual test execution..." | |
| # # Look for test execution indicators in the captured console output log | |
| # # Use a more permissive approach to detect if tests actually ran and passed | |
| # if [ -f "test-results/console-output.log" ] && grep -q -E "(passed|flaky).*[0-9]+.*test" "test-results/console-output.log"; then | |
| # echo "✅ Detected test execution in console output despite JSON reporting failure." | |
| # echo "🎯 This appears to be a JSON reporter issue, not actual test failures." | |
| # echo "🎉 Treating the test run as successful (tests passed but JSON reporter failed)." | |
| # exit 0 | |
| # # Check if this is likely an empty shard due to manual test exclusion | |
| # elif [ "$TEST_EXIT_CODE" -eq 0 ]; then | |
| # echo "✅ Playwright exited successfully (code 0) with no tests." | |
| # echo "🎯 This appears to be an empty shard due to manual test exclusion in CI." | |
| # echo "🎉 Treating the test run as successful." | |
| # exit 0 | |
| # else | |
| # echo "❓ Tests may have run but failed to report results properly." | |
| # echo "📊 This could be due to static caching interference with JSON reporting." | |
| # echo "💾 Check the uploaded HTML report for actual test results." | |
| # exit 1 | |
| # fi | |
| # fi | |
| # else | |
| # echo "⚠️ Results file exists but is not valid JSON." | |
| # echo "🔍 File content preview:" | |
| # head -20 "$RESULTS_FILE" 2>/dev/null || echo "Could not read file" | |
| # exit $TEST_EXIT_CODE | |
| # fi | |
| # else | |
| # echo "⚠️ No test results file found. This indicates a test execution failure." | |
| # echo "🔍 Searching for test-related files..." | |
| # find test-results -type f -name "*.json" -o -name "*.txt" -o -name "*.log" 2>/dev/null | head -10 | sed 's/^/ /' || echo " (No files found)" | |
| # echo "🔍 Playwright exit code: $TEST_EXIT_CODE" | |
| # echo "💾 This likely indicates a test infrastructure problem rather than test failures." | |
| # exit $TEST_EXIT_CODE | |
| # fi | |
| # } | |
| # - name: Upload E2E artifacts | |
| # uses: actions/upload-artifact@v5 | |
| # if: failure() | |
| # with: | |
| # name: playwright-report-shard-${{ matrix.shard }} | |
| # path: apps/web/test-results/ | |
| # retention-days: 7 | |
| # Aggregate coverage and generate reports | |
| coverage: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [validate] | |
| if: always() && needs.validate.result == 'success' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download coverage artifacts | |
| id: download-coverage | |
| uses: actions/download-artifact@v6 | |
| continue-on-error: true | |
| with: | |
| name: coverage-all | |
| path: coverage/ | |
| - name: Check coverage availability | |
| id: coverage-check | |
| run: | | |
| if [ -d "coverage" ] && [ "$(ls -A coverage 2>/dev/null)" ]; then | |
| echo "has-coverage=true" >> "${GITHUB_OUTPUT}" | |
| echo "Coverage artifacts found" | |
| else | |
| echo "has-coverage=false" >> "${GITHUB_OUTPUT}" | |
| echo "No coverage artifacts found - skipping coverage report" | |
| fi | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: .nx | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('.github/workflows/ci.yml', 'nx.json', '**/project.json', 'pnpm-lock.yaml', 'tsconfig*.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate workspace coverage report | |
| if: steps.coverage-check.outputs.has-coverage == 'true' | |
| run: pnpm coverage:report | |
| - name: Upload coverage to Codecov | |
| if: steps.coverage-check.outputs.has-coverage == 'true' | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| file: ./coverage/workspace-coverage.json | |
| flags: unittests | |
| name: codecov-umbrella | |
| - name: Upload final coverage reports | |
| if: steps.coverage-check.outputs.has-coverage == 'true' | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: final-coverage-reports | |
| path: coverage/ | |
| retention-days: 30 | |
| - name: Coverage skipped summary | |
| if: steps.coverage-check.outputs.has-coverage != 'true' | |
| run: | | |
| { | |
| echo "## Coverage Report Skipped" | |
| echo "No coverage artifacts were uploaded from the validate job." | |
| echo "To generate coverage, run tests with the \`--coverage\` flag." | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| # Accessibility tests (pa11y via Nx) | |
| test-accessibility: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [validate] | |
| if: always() && needs.validate.result == 'success' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'a11y-check')) | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: .nx | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('.github/workflows/ci.yml', 'nx.json', '**/project.json', 'pnpm-lock.yaml', 'tsconfig*.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3- | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: build-artifacts | |
| path: . | |
| - name: Install pa11y-ci | |
| run: npm install -g pa11y-ci | |
| - name: Run accessibility tests | |
| run: pnpm nx test:accessibility web | |
| continue-on-error: true | |
| - name: Upload accessibility report | |
| uses: actions/upload-artifact@v5 | |
| if: always() | |
| with: | |
| name: accessibility-report | |
| path: pa11y-ci-report/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # Performance tests (Lighthouse via Nx) | |
| test-performance: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [validate] | |
| if: always() && needs.validate.result == 'success' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'perf-check')) | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: .nx | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('.github/workflows/ci.yml', 'nx.json', '**/project.json', 'pnpm-lock.yaml', 'tsconfig*.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3- | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: build-artifacts | |
| path: . | |
| - name: Install Lighthouse CI | |
| run: npm install -g @lhci/cli@0.13.x | |
| - name: Run performance tests | |
| run: pnpm nx test:performance web | |
| continue-on-error: true | |
| env: | |
| LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} | |
| - name: Upload Lighthouse report | |
| uses: actions/upload-artifact@v5 | |
| if: always() | |
| with: | |
| name: lighthouse-report | |
| path: .lighthouseci/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # Route-based Smoke Tests - Fast validation that all routes from routeTree.gen.ts load successfully | |
| smoke-tests: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [validate] | |
| if: always() && needs.validate.result == 'success' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Install dependencies | |
| timeout-minutes: 5 | |
| run: pnpm install --frozen-lockfile | |
| - name: Setup Nx cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: .nx | |
| key: ${{ runner.os }}-nx-v3-${{ hashFiles('.github/workflows/ci.yml', 'nx.json', '**/project.json', 'pnpm-lock.yaml', 'tsconfig*.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nx-v3- | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: build-artifacts | |
| path: build-artifacts-temp | |
| - name: Restore build artifacts for smoke tests | |
| run: | | |
| echo "=== Restoring build artifacts for smoke tests ===" | |
| # Check if artifacts are stored directly (without dist/ prefix) or under dist/ | |
| if [ -d "build-artifacts-temp/dist" ]; then | |
| echo "✅ Found dist directory in temp artifacts (with dist/ prefix)" | |
| mv build-artifacts-temp/dist ./dist | |
| echo "✅ Moved dist directory to expected location" | |
| elif [ -d "build-artifacts-temp/apps" ]; then | |
| echo "✅ Found apps directory in temp artifacts (without dist/ prefix)" | |
| mv build-artifacts-temp ./dist | |
| echo "✅ Moved temp directory contents to dist/" | |
| else | |
| echo "❌ No valid build artifacts found in temp directory" | |
| echo "=== Contents of temp directory ===" | |
| find build-artifacts-temp -type f -o -type d | head -20 | |
| exit 1 | |
| fi | |
| # Verify the restored structure | |
| if [ -d "dist/apps/web" ]; then | |
| echo "✅ dist/apps/web directory restored successfully" | |
| find dist/apps/web -maxdepth 1 -type f | head -5 | |
| else | |
| echo "❌ dist/apps/web directory not found after restore" | |
| echo "=== Contents of dist directory ===" | |
| find dist -type f -o -type d | head -20 | |
| exit 1 | |
| fi | |
| # Clean up temp directory | |
| rm -rf build-artifacts-temp | |
| echo "✅ Temporary artifacts directory cleaned up" | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Start preview server for smoke tests | |
| run: | | |
| # Use built-in Node.js HTTP module to serve pre-built artifacts from dist/apps/web | |
| # This is much faster than pnpm preview which rebuilds everything and doesn't require extra packages | |
| node -e " | |
| const http = require('http'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const url = require('url'); | |
| const server = http.createServer((req, res) => { | |
| const parsedUrl = url.parse(req.url, true); | |
| // Handle the root path and any path that doesn't start with /assets/ | |
| let requestPath = parsedUrl.pathname; | |
| if (requestPath === '/') { | |
| requestPath = '/index.html'; | |
| } | |
| let filePath = path.join(process.env.GITHUB_WORKSPACE || '.', 'dist/apps/web', requestPath); | |
| const extname = path.extname(filePath); | |
| const contentType = { | |
| '.html': 'text/html', | |
| '.js': 'text/javascript', | |
| '.css': 'text/css', | |
| '.json': 'application/json', | |
| '.png': 'image/png', | |
| '.jpg': 'image/jpeg', | |
| '.ico': 'image/x-icon' | |
| }[extname] || 'text/plain'; | |
| fs.readFile(filePath, (err, content) => { | |
| if (err) { | |
| console.log(\`File not found: \${filePath} for request: \${parsedUrl.pathname}\`); | |
| res.writeHead(404); | |
| res.end(\`File not found: \${parsedUrl.pathname}\`); | |
| } else { | |
| console.log(\`Serving file: \${filePath} for request: \${parsedUrl.pathname}\`); | |
| res.writeHead(200, { 'Content-Type': contentType }); | |
| res.end(content); | |
| } | |
| }); | |
| }); | |
| server.listen(4173, () => { | |
| console.log('Static server ready on port 4173'); | |
| console.log('Serving files from:', path.join(process.env.GITHUB_WORKSPACE || '.', 'dist/apps/web')); | |
| }); | |
| " & | |
| echo "Static file server starting on port 4173..." | |
| # Wait for server to be ready (should be very quick with static server) | |
| timeout 10 bash -c 'until curl -s http://localhost:4173 > /dev/null; do sleep 1; done' | |
| echo "✅ Static file server is ready on http://localhost:4173" | |
| - name: Run route-based smoke tests | |
| timeout-minutes: 8 | |
| run: | | |
| cd apps/web | |
| export NODE_ENV=production RUNNING_E2E=true BASE_URL=http://localhost:4173 | |
| pnpm exec playwright test src/test/e2e/page-smoke.e2e.test.ts --reporter=list | |
| - name: Upload smoke test artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: smoke-test-results | |
| path: | | |
| apps/web/test-results/smoke-test-results.json | |
| apps/web/test-results/smoke-test-report | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # Deployment | |
| deploy: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [validate, smoke-tests, security-audit] | |
| if: always() && needs.validate.outputs.should-deploy == 'true' && github.ref == 'refs/heads/main' && needs.validate.result == 'success' && needs.security-audit.result == 'success' | |
| environment: | |
| name: production | |
| url: https://mearman.github.io/BibGraph/ | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Setup Pages | |
| uses: actions/configure-pages@v5 | |
| - name: Download build artifacts to temp directory | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: build-artifacts | |
| path: build-artifacts-temp | |
| - name: Install dependencies | |
| timeout-minutes: 5 | |
| run: pnpm install --frozen-lockfile | |
| - name: Restore build artifacts | |
| run: | | |
| echo "=== Restoring build artifacts from temp directory ===" | |
| # Check if artifacts are stored directly (without dist/ prefix) or under dist/ | |
| if [ -d "build-artifacts-temp/dist" ]; then | |
| echo "✅ Found dist directory in temp artifacts (with dist/ prefix)" | |
| mv build-artifacts-temp/dist ./dist | |
| echo "✅ Moved dist directory to expected location" | |
| elif [ -d "build-artifacts-temp/apps" ]; then | |
| echo "✅ Found apps directory in temp artifacts (without dist/ prefix)" | |
| mv build-artifacts-temp ./dist | |
| echo "✅ Moved temp directory contents to dist/" | |
| else | |
| echo "❌ No valid build artifacts found in temp directory" | |
| echo "=== Contents of temp directory ===" | |
| find build-artifacts-temp -type f -o -type d | head -20 | |
| exit 1 | |
| fi | |
| # Verify the restored structure | |
| if [ -d "dist/apps/web" ]; then | |
| echo "✅ dist/apps/web directory restored successfully" | |
| find dist/apps/web -maxdepth 1 -type f | head -5 | |
| else | |
| echo "❌ dist/apps/web directory not found after restore" | |
| echo "=== Contents of dist directory ===" | |
| find dist -type f -o -type d | head -20 | |
| exit 1 | |
| fi | |
| # Clean up temp directory | |
| rm -rf build-artifacts-temp | |
| echo "✅ Temporary artifacts directory cleaned up" | |
| - name: Install PostHog CLI | |
| run: npm install -g @posthog/cli | |
| - name: Inject source map metadata | |
| run: posthog-cli --host https://eu.posthog.com sourcemap inject --version ${{ github.sha }} --directory ./dist/apps/web | |
| env: | |
| POSTHOG_CLI_TOKEN: ${{ secrets.POSTHOG_CLI_TOKEN }} | |
| POSTHOG_CLI_ENV_ID: ${{ secrets.POSTHOG_CLI_ENV_ID }} | |
| - name: Upload source maps to PostHog | |
| run: posthog-cli --host https://eu.posthog.com sourcemap upload --directory ./dist/apps/web --delete-after | |
| env: | |
| POSTHOG_CLI_TOKEN: ${{ secrets.POSTHOG_CLI_TOKEN }} | |
| POSTHOG_CLI_ENV_ID: ${{ secrets.POSTHOG_CLI_ENV_ID }} | |
| - name: Upload build artifacts | |
| uses: actions/upload-pages-artifact@v4 | |
| with: | |
| path: dist/apps/web | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v4 | |
| - name: Run smoke tests | |
| run: | | |
| sleep 30 | |
| curl -f ${{ steps.deployment.outputs.page_url }} || exit 1 | |
| # Post-deployment E2E tests against live GitHub Pages | |
| post-deploy-e2e: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: deploy | |
| if: false # Disabled - full E2E suite takes 20-25 minutes and times out | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps | |
| - name: Wait for GitHub Pages propagation | |
| run: | | |
| echo "Waiting for GitHub Pages to be fully propagated..." | |
| sleep 60 | |
| - name: Run comprehensive E2E tests against live site | |
| env: | |
| E2E_BASE_URL: "https://mearman.github.io/BibGraph/" | |
| E2E_FULL_SUITE: "true" | |
| CI: "true" | |
| run: | | |
| echo "Running E2E tests against live GitHub Pages site..." | |
| pnpm nx test:e2e web | |
| - name: Upload post-deployment E2E artifacts | |
| uses: actions/upload-artifact@v5 | |
| if: failure() | |
| with: | |
| name: post-deploy-e2e-report | |
| path: test-results/playwright-report/ | |
| retention-days: 7 | |
| - name: Create issue on failure | |
| if: failure() | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: '🚨 Post-Deployment E2E Test Failure', | |
| body: `Post-deployment E2E tests failed against the live GitHub Pages site. | |
| **Deployment URL**: https://mearman.github.io/BibGraph/ | |
| **Workflow Run**: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId} | |
| **Commit**: ${context.sha} | |
| Please investigate the deployment and rollback if necessary. | |
| Steps taken: | |
| - ✅ Build and test completed | |
| - ✅ Deployment to GitHub Pages completed | |
| - ❌ Post-deployment E2E tests failed | |
| The rollback job should trigger automatically to restore the previous working version.`, | |
| labels: ['bug', 'ci/cd', 'urgent'] | |
| }) | |
| # Release automation | |
| release: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [validate, smoke-tests, security-audit, deploy] | |
| if: always() && needs.validate.result == 'success' && needs.security-audit.result == 'success' && needs.deploy.result == 'success' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v3 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Configure Git | |
| run: | | |
| git config --local user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| - name: Run semantic-release | |
| run: | | |
| npx semantic-release || { | |
| echo "Semantic-release failed or no changes to release" | |
| echo "This is normal if no conventional commits warrant a release" | |
| exit 0 | |
| } | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Automatic rollback on post-deployment E2E failure | |
| rollback: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: post-deploy-e2e | |
| if: failure() && needs.post-deploy-e2e.result == 'failure' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: write | |
| pages: write | |
| id-token: write | |
| issues: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Get previous successful deployment | |
| id: previous-deployment | |
| run: | | |
| echo "Finding previous successful GitHub Pages deployment..." | |
| # Get the list of Pages deployments | |
| deployments=$(curl -s \ | |
| -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| https://api.github.com/repos/${{ github.repository }}/pages/deployments) | |
| # Find the most recent successful deployment (excluding current failed one) | |
| previous_deployment_id=$(echo "$deployments" | jq '[.[] | select(.status == "succeeded" and .updated_at < "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'")] | sort_by(.updated_at) | reverse | .[0].id') | |
| if [ "$previous_deployment_id" = "null" ] || [ -z "$previous_deployment_id" ]; then | |
| echo "No previous successful deployment found" | |
| echo "deployment_id=" >> "${GITHUB_OUTPUT}" | |
| exit 1 | |
| fi | |
| echo "deployment_id=$previous_deployment_id" >> "${GITHUB_OUTPUT}" | |
| echo "Previous successful deployment ID: $previous_deployment_id" | |
| - name: Rollback to previous deployment | |
| if: steps.previous-deployment.outputs.deployment_id != '' | |
| run: | | |
| echo "Rolling back to deployment ID: ${{ steps.previous-deployment.outputs.deployment_id }}" | |
| # Cancel current deployment if still active | |
| current_deployments=$(curl -s \ | |
| -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| https://api.github.com/repos/${{ github.repository }}/pages/deployments) | |
| current_id=$(echo "$current_deployments" | jq '.[0].id') | |
| if [ "$current_id" != "null" ] && [ -n "$current_id" ]; then | |
| curl -X POST \ | |
| -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| "https://api.github.com/repos/${{ github.repository }}/pages/deployments/$current_id/cancel" | |
| echo "Cancelled current deployment: $current_id" | |
| fi | |
| # Promote previous successful deployment | |
| curl -X POST \ | |
| -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| -H "Accept: application/vnd.github.v3+json" \ | |
| "https://api.github.com/repos/${{ github.repository }}/pages/deployments/${{ steps.previous-deployment.outputs.deployment_id }}/promote" | |
| echo "Rollback initiated successfully" | |
| - name: Create rollback issue | |
| if: steps.previous-deployment.outputs.deployment_id != '' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: '🔄 Automatic Rollback Completed', | |
| body: `The GitHub Pages deployment has been automatically rolled back due to post-deployment E2E test failures. | |
| **Previous Deployment ID**: ${{ steps.previous-deployment.outputs.deployment_id }} | |
| **Live Site**: https://mearman.github.io/BibGraph/ | |
| **Workflow Run**: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId} | |
| **Failed Commit**: ${context.sha} | |
| **What happened:** | |
| - ✅ Build and test completed successfully | |
| - ✅ Deployment to GitHub Pages completed | |
| - ❌ Post-deployment E2E tests failed | |
| - 🔄 Automatic rollback to previous successful deployment completed | |
| **Next steps:** | |
| 1. Investigate the E2E test failures in the workflow run | |
| 2. Review the deployment artifacts and test reports | |
| 3. Fix the issues and retry deployment | |
| 4. Monitor the next deployment's post-deployment tests | |
| The site should now be restored to the previous working version.`, | |
| labels: ['ci/cd', 'rollback', 'resolved'] | |
| }) | |
| - name: Wait for rollback propagation | |
| if: steps.previous-deployment.outputs.deployment_id != '' | |
| run: | | |
| echo "Waiting for rollback to propagate..." | |
| sleep 30 | |
| # Verify rollback is working | |
| max_attempts=10 | |
| attempt=1 | |
| while [ $attempt -le $max_attempts ]; do | |
| echo "Checking rollback status (attempt $attempt/$max_attempts)..." | |
| if curl -f -s "https://mearman.github.io/BibGraph/" > /dev/null; then | |
| echo "✅ Rollback verified - site is accessible" | |
| break | |
| fi | |
| if [ $attempt -eq $max_attempts ]; then | |
| echo "⚠️ Rollback verification failed after $max_attempts attempts" | |
| exit 1 | |
| fi | |
| sleep 30 | |
| attempt=$((attempt + 1)) | |
| done | |
| # Results and notifications | |
| results: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [validate, security-audit, coverage, test-accessibility, test-performance, deploy, post-deploy-e2e, release, rollback] | |
| if: always() | |
| steps: | |
| - name: Download coverage reports | |
| if: needs.coverage.result == 'success' | |
| uses: actions/download-artifact@v6 | |
| continue-on-error: true | |
| with: | |
| name: final-coverage-reports | |
| path: coverage/ | |
| - name: Generate results summary | |
| run: | | |
| { | |
| echo "# 📊 CI/CD Pipeline Results" | |
| echo "" | |
| echo "| Job | Status | Details |" | |
| echo "|-----|--------|---------|" | |
| echo "| ✅ Validate | ${{ needs.validate.result }} | Build + typecheck + lint + all tests |" | |
| echo "| 🔒 Security Audit | ${{ needs.security-audit.result }} | Vulnerability scanning |" | |
| echo "| 🚫 E2E Tests | DISABLED | Full E2E suite (disabled due to CI issues) |" | |
| echo "| 📈 Coverage | ${{ needs.coverage.result }} | Workspace coverage report |" | |
| echo "| ♿ Accessibility | ${{ needs.test-accessibility.result }} | pa11y WCAG 2.1 AA |" | |
| echo "| ⚡ Performance | ${{ needs.test-performance.result }} | Lighthouse CI |" | |
| echo "| 🚀 Deploy | ${{ needs.deploy.result }} | ${{ needs.validate.outputs.should-deploy == 'true' && 'GitHub Pages (after all tests pass)' || 'Skipped' }} |" | |
| echo "| 🎯 Post-Deploy E2E | ${{ needs.post-deploy-e2e.result }} | ${{ needs.post-deploy-e2e.result == 'success' && 'Live site verification passed' || (needs.post-deploy-e2e.result == 'skipped' && 'Not run' || 'Live site verification failed') }} |" | |
| echo "| 🔄 Rollback | ${{ needs.rollback.result }} | ${{ needs.rollback.result == 'success' && 'Automatic rollback completed' || (needs.rollback.result == 'skipped' && 'Not needed' || 'Rollback failed') }} |" | |
| echo "| 📦 Release | ${{ needs.release.result }} | ${{ needs.release.result == 'success' && 'Release created successfully' || (needs.release.result == 'skipped' && 'Not created (failed post-deploy E2E)' || 'Release failed') }} |" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| - name: Coverage summary | |
| if: needs.coverage.result == 'success' | |
| run: | | |
| if [ -f coverage/workspace-coverage.json ]; then | |
| OVERALL=$(jq -r '.overall' coverage/workspace-coverage.json) | |
| LINES=$(jq -r '.totals.lines.percentage' coverage/workspace-coverage.json) | |
| FUNCTIONS=$(jq -r '.totals.functions.percentage' coverage/workspace-coverage.json) | |
| { | |
| echo "## 📈 Coverage Summary" | |
| echo "" | |
| echo "- **Overall Coverage**: ${OVERALL}%" | |
| echo "- **Lines Coverage**: ${LINES}%" | |
| echo "- **Functions Coverage**: ${FUNCTIONS}%" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| fi | |
| - name: Notification on failure | |
| if: failure() | |
| run: | | |
| echo "❌ Pipeline failed! Check the individual job logs for details." >> "${GITHUB_STEP_SUMMARY}" | |
| exit 1 | |
| - name: Success notification | |
| if: success() | |
| run: | | |
| echo "🎉 All checks passed! Ready for merge/deployment." >> "${GITHUB_STEP_SUMMARY}" |