chore(deps-dev): bump start-server-and-test from 2.1.5 to 3.0.0 in /apps/frontend #313
Workflow file for this run
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: | |
| - main | |
| - develop | |
| tags: | |
| - 'v*' | |
| pull_request: | |
| branches: | |
| - main | |
| - develop | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| ignore_scan_errors: | |
| description: 'Ignore container scan errors (registry access issues)' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| # ============================================================================ | |
| # SECURITY SCANNING (runs first, fast fail) | |
| # ============================================================================ | |
| security: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install security tools | |
| run: | | |
| pip install bandit safety | |
| - name: Run bandit (Python security) | |
| run: | | |
| bandit -r apps/backend/src -f json -o bandit-report.json || true | |
| bandit -r apps/backend/src --severity-level medium | |
| - name: Run safety (Python vulnerabilities) | |
| run: | | |
| safety check --file apps/backend/requirements.txt --json || true | |
| safety check --file apps/backend/requirements.txt | |
| continue-on-error: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Run npm audit (Frontend vulnerabilities) | |
| working-directory: apps/frontend | |
| run: | | |
| npm audit --audit-level=moderate || true | |
| npm audit --production --audit-level=high | |
| continue-on-error: true | |
| # ============================================================================ | |
| # CODE FORMATTING (runs parallel with security) | |
| # ============================================================================ | |
| format: | |
| name: Check Formatting | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install black | |
| run: pip install black==26.1.0 | |
| - name: Check Python formatting | |
| run: | | |
| black --check apps/backend/ | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci --prefer-offline | |
| - name: Check TypeScript/React formatting | |
| working-directory: apps/frontend | |
| run: npx prettier --check "src/**/*.{js,jsx,ts,tsx,json,css}" | |
| # ============================================================================ | |
| # LINTING (runs after security/format) | |
| # ============================================================================ | |
| lint: | |
| name: Lint Code | |
| runs-on: ubuntu-latest | |
| needs: [security, format] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install linting tools | |
| run: | | |
| pip install ruff mypy | |
| - name: Run ruff (Python linter) | |
| run: | | |
| ruff check apps/backend/ --select=E,F,W --ignore=E501 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci --prefer-offline | |
| - name: Run ESLint (TypeScript/React linter) | |
| working-directory: apps/frontend | |
| run: npm run lint | |
| # ============================================================================ | |
| # BACKEND TESTS (runs parallel with lint) | |
| # ============================================================================ | |
| backend-tests: | |
| name: Backend Tests | |
| runs-on: ubuntu-latest | |
| needs: [security, format] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Cache Python dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('apps/backend/requirements-dev.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - name: Install dependencies | |
| run: | | |
| pip install -e apps/backend | |
| pip install -r apps/backend/requirements-dev.txt | |
| - name: Run tests with coverage | |
| run: | | |
| cd apps/backend | |
| pytest --cov=opencloudtouch --cov-report=xml --cov-report=json --cov-report=term-missing --cov-fail-under=80 | |
| env: | |
| OCT_MOCK_MODE: "true" | |
| OCT_HAS_DEVICES: "false" | |
| CI: "true" | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: ./.out/coverage/backend/coverage.xml | |
| flags: backend | |
| name: backend-coverage | |
| fail_ci_if_error: true | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload coverage JSON (for summary) | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: backend-coverage | |
| path: .out/coverage/backend/coverage.json | |
| retention-days: 1 | |
| # ============================================================================ | |
| # FRONTEND TESTS (runs parallel with backend-tests) | |
| # ============================================================================ | |
| frontend-tests: | |
| name: Frontend Tests | |
| runs-on: ubuntu-latest | |
| needs: [security, format] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Install dependencies | |
| run: | | |
| npm ci | |
| npm install @rollup/rollup-linux-x64-gnu --save-optional | |
| - name: Build frontend | |
| working-directory: apps/frontend | |
| run: npm run build | |
| - name: Run unit tests with coverage | |
| working-directory: apps/frontend | |
| run: npm run test:coverage | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: ./.out/coverage/frontend/lcov.info | |
| flags: frontend | |
| name: frontend-coverage | |
| fail_ci_if_error: true | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload coverage JSON (for summary) | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: frontend-coverage | |
| path: .out/coverage/frontend/coverage-summary.json | |
| retention-days: 1 | |
| - name: Upload frontend build (for Docker) | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: frontend-dist | |
| path: .out/dist/ | |
| retention-days: 1 | |
| # ============================================================================ | |
| # E2E TESTS (runs parallel with unit tests after security/format) | |
| # ============================================================================ | |
| e2e-tests: | |
| name: E2E Tests | |
| runs-on: ubuntu-latest | |
| needs: [security, format] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Cache Python dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('apps/backend/requirements.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - name: Install backend dependencies | |
| run: pip install -e apps/backend | |
| - name: Install frontend dependencies | |
| run: | | |
| npm ci | |
| npm install @rollup/rollup-linux-x64-gnu --save-optional | |
| - name: Build frontend | |
| working-directory: apps/frontend | |
| run: npm run build | |
| - name: Start backend server | |
| run: | | |
| cd apps/backend | |
| OCT_MOCK_MODE=true OCT_LOG_LEVEL=WARNING OCT_ALLOW_DANGEROUS_OPERATIONS=true python -m uvicorn opencloudtouch.main:app --host 0.0.0.0 --port 7778 & | |
| sleep 10 | |
| curl --retry 10 --retry-delay 2 --retry-connrefused http://localhost:7778/health | |
| - name: Start frontend preview server | |
| run: | | |
| cd apps/frontend | |
| npm run preview -- --port 4173 --strictPort & | |
| sleep 5 | |
| curl --retry 10 --retry-delay 2 --retry-connrefused http://localhost:4173 | |
| - name: Run Cypress E2E tests | |
| working-directory: apps/frontend | |
| run: npm run test:e2e | |
| continue-on-error: true # E2E tests may fail due to environment differences | |
| env: | |
| CYPRESS_BASE_URL: http://localhost:4173 | |
| CYPRESS_API_URL: http://localhost:7778/api | |
| - name: Upload Cypress screenshots on failure | |
| uses: actions/upload-artifact@v7 | |
| if: failure() | |
| with: | |
| name: cypress-screenshots | |
| path: apps/frontend/tests/e2e/screenshots | |
| retention-days: 7 | |
| - name: Upload Cypress videos on failure | |
| uses: actions/upload-artifact@v7 | |
| if: failure() | |
| with: | |
| name: cypress-videos | |
| path: apps/frontend/tests/e2e/videos | |
| retention-days: 7 | |
| # ============================================================================ | |
| # DOCKER SECURITY SCAN (gate before multi-arch build - BUILD-04) | |
| # ============================================================================ | |
| docker-security: | |
| name: Docker Security Scan | |
| runs-on: ubuntu-latest | |
| needs: [e2e-tests, frontend-tests] | |
| if: (github.event_name == 'push' || github.event_name == 'release') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Download pre-built frontend | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: frontend-dist | |
| path: .out/dist | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build test image for scanning | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: deployment/Dockerfile | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: opencloudtouch:test | |
| cache-from: type=gha,scope=trivy-scan | |
| cache-to: type=gha,mode=max,scope=trivy-scan | |
| - name: Run Trivy vulnerability scanner (blocking) | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| image-ref: 'opencloudtouch:test' | |
| format: 'table' | |
| severity: 'HIGH,CRITICAL' | |
| exit-code: '1' # Fail build on HIGH/CRITICAL vulnerabilities | |
| ignore-unfixed: true | |
| - name: Run Trivy with JSON output for artifact | |
| uses: aquasecurity/trivy-action@master | |
| if: always() | |
| with: | |
| image-ref: 'opencloudtouch:test' | |
| format: 'json' | |
| output: 'trivy-results.json' | |
| severity: 'HIGH,CRITICAL,MEDIUM,LOW' | |
| - name: Upload Trivy scan results | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: trivy-scan-results | |
| path: trivy-results.json | |
| retention-days: 30 | |
| # ============================================================================ | |
| # DOCKER BUILD (parallel multi-arch, AFTER security scan passes) | |
| # ============================================================================ | |
| build: | |
| name: Build Docker Image (${{ matrix.platform }}) | |
| runs-on: ubuntu-latest | |
| needs: [e2e-tests, docker-security, frontend-tests] # Only build if tests AND security scan pass | |
| if: (github.event_name == 'push' || github.event_name == 'release') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) | |
| strategy: | |
| matrix: | |
| platform: [linux/amd64, linux/arm64, linux/arm/v7] | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Download pre-built frontend | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: frontend-dist | |
| path: .out/dist | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Extract metadata (tags, labels) | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| type=sha,prefix=sha- | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: deployment/Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| push: true | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=build-${{ matrix.platform }} | |
| cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,annotation-index.org.opencontainers.image.description=OpenCloudTouch for ${{ matrix.platform }} | |
| - name: Export digest | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-${{ strategy.job-index }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # ============================================================================ | |
| # DOCKER PUSH (merge digests and push multi-arch manifest) | |
| # ============================================================================ | |
| push: | |
| name: Push Docker Image | |
| runs-on: ubuntu-latest | |
| needs: [build, e2e-tests] | |
| if: (github.event_name == 'push' || github.event_name == 'release') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: digests-* | |
| path: /tmp/digests | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Extract metadata (tags, labels) | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| type=sha,prefix=sha- | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect image | |
| run: | | |
| docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | |
| # ============================================================================ | |
| # CONTAINER SECURITY SCAN (runs after push) | |
| # ============================================================================ | |
| container-scan: | |
| name: Scan Container for Vulnerabilities | |
| runs-on: ubuntu-latest | |
| needs: [push] | |
| if: (github.event_name == 'push' || github.event_name == 'release') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) | |
| permissions: | |
| contents: read | |
| security-events: write # Required for uploading SARIF | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Extract metadata (for tag) | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=semver,pattern={{version}} | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| continue-on-error: ${{ inputs.ignore_scan_errors == true }} | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| - name: Upload Trivy results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() && hashFiles('trivy-results.sarif') != '' | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| - name: Run Trivy vulnerability scanner (table output) | |
| uses: aquasecurity/trivy-action@master | |
| continue-on-error: ${{ inputs.ignore_scan_errors == true }} | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | |
| format: 'table' | |
| severity: 'CRITICAL,HIGH,MEDIUM' | |
| # ============================================================================ | |
| # BUILD SUMMARY (runs always at the end) | |
| # ============================================================================ | |
| summary: | |
| name: Build Summary | |
| runs-on: ubuntu-latest | |
| needs: [security, format, lint, backend-tests, frontend-tests, e2e-tests, container-scan] | |
| if: always() | |
| steps: | |
| - name: Download backend coverage | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| name: backend-coverage | |
| path: ./coverage | |
| - name: Download frontend coverage | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| name: frontend-coverage | |
| path: ./coverage | |
| - name: Parse coverage and generate summary | |
| run: | | |
| echo "# π CI/CD Pipeline Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Job status overview | |
| echo "## π Job Status" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Security Scan | ${{ needs.security.result == 'success' && 'β Passed' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Format Check | ${{ needs.format.result == 'success' && 'β Passed' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lint | ${{ needs.lint.result == 'success' && 'β Passed' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Backend Tests | ${{ needs.backend-tests.result == 'success' && 'β Passed' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Frontend Tests | ${{ needs.frontend-tests.result == 'success' && 'β Passed' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| E2E Tests | ${{ needs.e2e-tests.result == 'success' && 'β Passed' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Container Scan | ${{ needs.container-scan.result == 'success' && 'β Passed' || needs.container-scan.result == 'skipped' && 'βοΈ Skipped' || 'β Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Coverage | |
| echo "## π Test Coverage" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Backend coverage | |
| if [ -f ./coverage/coverage.json ]; then | |
| BACKEND_COV=$(python3 -c "import json; data=json.load(open('./coverage/coverage.json')); print(f\"{data['totals']['percent_covered']:.1f}%\")" 2>/dev/null || echo "N/A") | |
| echo "**Backend:** $BACKEND_COV" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "**Backend:** Coverage data not available" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Frontend coverage | |
| if [ -f ./coverage/coverage-summary.json ]; then | |
| FRONTEND_COV=$(python3 -c "import json; data=json.load(open('./coverage/coverage-summary.json')); total=data.get('total', {}); lines=total.get('lines', {}); print(f\"{lines.get('pct', 0):.1f}%\")" 2>/dev/null || echo "N/A") | |
| echo "**Frontend:** $FRONTEND_COV" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "**Frontend:** Coverage data not available" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "_π‘ Tip: Click on job names above to see detailed results_" >> $GITHUB_STEP_SUMMARY | |
| - name: Check overall status | |
| run: | | |
| if [[ "${{ needs.security.result }}" != "success" ]] || \ | |
| [[ "${{ needs.format.result }}" != "success" ]] || \ | |
| [[ "${{ needs.lint.result }}" != "success" ]] || \ | |
| [[ "${{ needs.backend-tests.result }}" != "success" ]] || \ | |
| [[ "${{ needs.frontend-tests.result }}" != "success" ]] || \ | |
| [[ "${{ needs.e2e-tests.result }}" != "success" ]]; then | |
| echo "β Some jobs failed - see summary above" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| else | |
| echo "β All jobs passed successfully!" >> $GITHUB_STEP_SUMMARY | |
| fi |