Skip to content

chore(deps-dev): bump start-server-and-test from 2.1.5 to 3.0.0 in /apps/frontend #313

chore(deps-dev): bump start-server-and-test from 2.1.5 to 3.0.0 in /apps/frontend

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

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