ci: fix duplicate env block in staging-validation workflow
Some checks failed
Backend API CI / test-unit (push) Failing after 4m11s
Backend API CI / test-integration (push) Failing after 7m49s
Veza CD / Build and push images (push) Failing after 1m16s
Veza CI/CD / TMT Vital — Backend (Go) (push) Failing after 2m45s
Veza CI/CD / TMT Vital — Rust Services (push) Failing after 3s
Veza CI/CD / TMT Vital — Frontend (Web) (push) Failing after 4m26s
Veza CI/CD / Storybook Audit (push) Failing after 6m39s
Veza CI/CD / E2E Critical (@critical) (push) Failing after 5m16s
Veza CI/CD / E2E Full (shard 1/4) (push) Failing after 5m18s
Veza CI/CD / E2E Full (shard 2/4) (push) Failing after 5m11s
Veza CI/CD / E2E Full (shard 3/4) (push) Failing after 5m7s
Veza CI/CD / E2E Full (shard 4/4) (push) Failing after 5m7s
Frontend CI / test (push) Failing after 1m9s
CodeQL SAST / analyze (go) (push) Failing after 4s
CodeQL SAST / analyze (javascript-typescript) (push) Failing after 3s
Security Scan / Secret Scanning (gitleaks) (push) Failing after 4s
Storybook Audit / Build & audit Storybook (push) Failing after 1m11s
Stream Server CI / test (push) Failing after 4s
Veza CD / Deploy to staging (push) Has been skipped
Veza CI/CD / Notify on failure (push) Successful in 3s
Veza CD / Smoke tests post-deploy (push) Has been skipped

Merge SSL env vars into existing env block instead of creating a
duplicate (YAML doesn't allow duplicate top-level keys).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
senke 2026-04-09 14:51:10 +02:00
parent d26621cca9
commit b4c0b9283b
25 changed files with 2059 additions and 1096 deletions

194
.github/workflows/accessibility.yml vendored Normal file
View file

@ -0,0 +1,194 @@
name: Accessibility
on:
pull_request:
branches: [main]
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
# ===========================================================================
# Job 1: axe-playwright — WCAG AA violations via @axe-core/playwright
# ===========================================================================
axe-playwright:
name: axe-playwright
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache-dependency-path: veza-backend-api/go.sum
- name: Install dependencies
run: npm ci
- name: Add veza.fr to hosts
run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts
- name: Start backend services (Postgres, Redis, RabbitMQ)
run: |
docker-compose up -d postgres redis rabbitmq
echo "Waiting for Postgres..."
for i in $(seq 1 30); do
if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then
echo "Postgres ready"
break
fi
sleep 2
done
docker-compose ps
- name: Run database migrations
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
run: |
cd veza-backend-api
go run cmd/migrate_tool/main.go
- name: Start backend API
env:
APP_ENV: development
APP_PORT: "18080"
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
COOKIE_SECURE: "false"
CORS_ALLOWED_ORIGINS: http://veza.fr:5174,http://localhost:5174
RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }}
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
run: |
cd veza-backend-api
go build -o veza-api ./cmd/api/main.go
./veza-api &
sleep 10
curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1)
jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1)
echo "Health check OK"
- name: Install Playwright Browsers (chromium only)
run: npx playwright install --with-deps chromium
- name: Run @a11y E2E tests
run: npx playwright test --config=tests/e2e/playwright.config.ts --grep @a11y --project=chromium
env:
PORT: "5174"
VITE_API_URL: "/api/v1"
VITE_DOMAIN: veza.fr
VITE_BACKEND_PORT: "18080"
PLAYWRIGHT_BASE_URL: "http://localhost:5174"
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
if: failure()
with:
name: axe-playwright-report
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
retention-days: 7
# ===========================================================================
# Job 2: lighthouse-ci — Performance & Accessibility scoring
# ===========================================================================
lighthouse-ci:
name: lighthouse-ci
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache-dependency-path: veza-backend-api/go.sum
- name: Install dependencies
run: npm ci
- name: Add veza.fr to hosts
run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts
- name: Start backend services (Postgres, Redis, RabbitMQ)
run: |
docker-compose up -d postgres redis rabbitmq
echo "Waiting for Postgres..."
for i in $(seq 1 30); do
if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then
echo "Postgres ready"
break
fi
sleep 2
done
docker-compose ps
- name: Run database migrations
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
run: |
cd veza-backend-api
go run cmd/migrate_tool/main.go
- name: Start backend API
env:
APP_ENV: development
APP_PORT: "18080"
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
COOKIE_SECURE: "false"
CORS_ALLOWED_ORIGINS: http://veza.fr:5174,http://localhost:5174
RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }}
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
run: |
cd veza-backend-api
go build -o veza-api ./cmd/api/main.go
./veza-api &
sleep 10
curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1)
jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1)
echo "Health check OK"
- name: Start Vite dev server
run: |
cd apps/web
npm run dev -- --host 127.0.0.1 --port 5174 &
echo "Waiting for Vite dev server..."
for i in $(seq 1 30); do
if curl -sf http://localhost:5174 >/dev/null 2>&1; then
echo "Vite dev server ready"
break
fi
sleep 2
done
curl -sf http://localhost:5174 >/dev/null || (echo "Vite dev server failed to start"; exit 1)
env:
VITE_API_URL: "/api/v1"
VITE_DOMAIN: veza.fr
VITE_BACKEND_PORT: "18080"
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
configPath: ./.lighthouserc.js
uploadArtifacts: true
temporaryPublicStorage: true

View file

@ -1,153 +1,157 @@
name: Backend API CI
on:
push:
paths:
- "veza-backend-api/**"
- ".github/workflows/backend-ci.yml"
pull_request:
paths:
- "veza-backend-api/**"
- ".github/workflows/backend-ci.yml"
push:
paths:
- "veza-backend-api/**"
- ".github/workflows/backend-ci.yml"
pull_request:
paths:
- "veza-backend-api/**"
- ".github/workflows/backend-ci.yml"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
test-unit:
runs-on: ubuntu-latest
test-unit:
runs-on: ubuntu-latest
defaults:
run:
working-directory: veza-backend-api
defaults:
run:
working-directory: veza-backend-api
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Download deps
run: go mod download
- name: Download deps
run: go mod download
- name: Go vet and format check
run: |
go vet ./...
test -z "$(gofmt -l .)"
working-directory: veza-backend-api
- name: Go vet and format check
run: |
go vet ./...
test -z "$(gofmt -l .)"
working-directory: veza-backend-api
- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Run unit tests with coverage
run: >
go test
./internal/handlers/...
./internal/services/...
./internal/core/...
./internal/middleware/...
-short -coverprofile=coverage.out -covermode=atomic -timeout 5m
- name: Run unit tests with coverage
run: >
go test
./internal/handlers/...
./internal/services/...
./internal/core/...
./internal/middleware/...
-short -coverprofile=coverage.out -covermode=atomic -timeout 5m
- name: Enforce coverage threshold (>= 70%)
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%')
echo "Total coverage: ${COVERAGE}%"
if [ -z "$COVERAGE" ]; then
echo "::warning::Could not parse coverage percentage"
exit 0
fi
# Compare as integers (remove decimal)
COV_INT=$(echo "$COVERAGE" | cut -d. -f1)
if [ "$COV_INT" -lt 70 ]; then
echo "::error::Coverage ${COVERAGE}% is below the 70% threshold"
exit 1
fi
echo "::notice::Coverage ${COVERAGE}% meets the >= 70% threshold"
- name: Enforce coverage threshold (>= 70%)
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%')
echo "Total coverage: ${COVERAGE}%"
if [ -z "$COVERAGE" ]; then
echo "::warning::Could not parse coverage percentage"
exit 0
fi
# Compare as integers (remove decimal)
COV_INT=$(echo "$COVERAGE" | cut -d. -f1)
if [ "$COV_INT" -lt 75 ]; then
echo "::error::Coverage ${COVERAGE}% is below the 75% threshold"
exit 1
fi
echo "::notice::Coverage ${COVERAGE}% meets the >= 75% threshold"
- name: Upload coverage report
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: go-coverage
path: veza-backend-api/coverage.out
- name: Upload coverage report
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: go-coverage
path: veza-backend-api/coverage.out
- name: Generate coverage badge
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%')
COV_INT=$(echo "$COVERAGE" | cut -d. -f1)
if [ "$COV_INT" -ge 80 ]; then
COLOR="brightgreen"
elif [ "$COV_INT" -ge 70 ]; then
COLOR="green"
elif [ "$COV_INT" -ge 50 ]; then
COLOR="yellow"
else
COLOR="red"
fi
echo "{\"schemaVersion\":1,\"label\":\"Go coverage\",\"message\":\"${COVERAGE}%\",\"color\":\"${COLOR}\"}" > coverage-badge.json
echo "Coverage badge: ${COVERAGE}% (${COLOR})"
- name: Generate coverage badge
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print $NF}' | tr -d '%')
COV_INT=$(echo "$COVERAGE" | cut -d. -f1)
if [ "$COV_INT" -ge 80 ]; then
COLOR="brightgreen"
elif [ "$COV_INT" -ge 70 ]; then
COLOR="green"
elif [ "$COV_INT" -ge 50 ]; then
COLOR="yellow"
else
COLOR="red"
fi
echo "{\"schemaVersion\":1,\"label\":\"Go coverage\",\"message\":\"${COVERAGE}%\",\"color\":\"${COLOR}\"}" > coverage-badge.json
echo "Coverage badge: ${COVERAGE}% (${COLOR})"
- name: Upload coverage badge
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: go-coverage-badge
path: veza-backend-api/coverage-badge.json
- name: Upload coverage badge
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: go-coverage-badge
path: veza-backend-api/coverage-badge.json
test-integration:
runs-on: ubuntu-latest
test-integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: veza_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: veza_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/veza_test?sslmode=disable
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-jwt-secret-for-ci
APP_ENV: test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/veza_test?sslmode=disable
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-jwt-secret-for-ci
APP_ENV: test
defaults:
run:
working-directory: veza-backend-api
defaults:
run:
working-directory: veza-backend-api
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Download deps
run: go mod download
- name: Download deps
run: go mod download
- name: Run migrations
run: go run cmd/migrate_tool/main.go
continue-on-error: true
- name: Run migrations
run: go run cmd/migrate_tool/main.go
continue-on-error: true
- name: Run integration tests
run: go test -tags=integration ./internal/... -timeout 15m
- name: Run integration tests
run: go test -tags=integration ./internal/... -timeout 15m

View file

@ -1,165 +1,168 @@
name: Veza CD
on:
push:
branches: [ "main" ]
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
push:
branches: ["main"]
workflow_dispatch:
inputs:
environment:
description: "Deployment environment"
required: true
default: "staging"
type: choice
options:
- staging
- production
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
build:
name: Build and push images
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
build:
name: Build and push images
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Push to registry: set repo secrets DOCKER_REGISTRY, DOCKER_REGISTRY_USERNAME, DOCKER_REGISTRY_PASSWORD
# Example: DOCKER_REGISTRY=ghcr.io/org/repo or registry.example.com/veza
- name: Build Backend Docker Image
run: |
docker build -t veza-backend-api:${{ github.sha }} -f veza-backend-api/Dockerfile.production veza-backend-api/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Build Frontend Docker Image
run: |
docker build -t veza-frontend:${{ github.sha }} -f apps/web/Dockerfile.production apps/web/
# Push to registry: set repo secrets DOCKER_REGISTRY, DOCKER_REGISTRY_USERNAME, DOCKER_REGISTRY_PASSWORD
# Example: DOCKER_REGISTRY=ghcr.io/org/repo or registry.example.com/veza
- name: Build Backend Docker Image
run: |
docker build -t veza-backend-api:${{ github.sha }} -f veza-backend-api/Dockerfile.production veza-backend-api/
- name: Build Stream Server Docker Image
run: |
docker build -t veza-stream-server:${{ github.sha }} -f veza-stream-server/Dockerfile.production veza-stream-server/
- name: Build Frontend Docker Image
run: |
docker build -t veza-frontend:${{ github.sha }} -f apps/web/Dockerfile.production apps/web/
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: 'veza-backend-api:${{ github.sha }}'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
- name: Build Stream Server Docker Image
run: |
docker build -t veza-stream-server:${{ github.sha }} -f veza-stream-server/Dockerfile.production veza-stream-server/
- name: Trivy scan frontend
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: 'veza-frontend:${{ github.sha }}'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: "veza-backend-api:${{ github.sha }}"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
- name: Trivy scan stream server
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: 'veza-stream-server:${{ github.sha }}'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
- name: Trivy scan frontend
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: "veza-frontend:${{ github.sha }}"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
- name: Generate SBOM
run: |
mkdir -p sbom
for svc in veza-backend-api veza-frontend veza-stream-server; do
trivy image --format cyclonedx --output "sbom/${svc}-${{ github.sha }}.json" "${svc}:${{ github.sha }}"
done
- name: Upload SBOM artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: sbom
path: sbom/
- name: Trivy scan stream server
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: "veza-stream-server:${{ github.sha }}"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
- name: Push Images to Registry
if: vars.DOCKER_REGISTRY != ''
run: |
echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin
for svc in veza-backend-api veza-frontend veza-stream-server; do
docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:latest"
docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:latest"
done
- name: Generate SBOM
run: |
mkdir -p sbom
for svc in veza-backend-api veza-frontend veza-stream-server; do
trivy image --format cyclonedx --output "sbom/${svc}-${{ github.sha }}.json" "${svc}:${{ github.sha }}"
done
- name: Upload SBOM artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: sbom
path: sbom/
- name: Install cosign
if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true'
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
with:
cosign-release: 'v2.2.0'
- name: Sign images with cosign
if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true'
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
for svc in veza-backend-api veza-frontend veza-stream-server; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:latest"
done
- name: Push Images to Registry
if: vars.DOCKER_REGISTRY != ''
run: |
echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin
for svc in veza-backend-api veza-frontend veza-stream-server; do
docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
docker tag "${svc}:${{ github.sha }}" "${{ vars.DOCKER_REGISTRY }}/${svc}:latest"
docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:latest"
done
- name: Build Summary
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "- Backend: veza-backend-api:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- Frontend: veza-frontend:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- Stream Server: veza-stream-server:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
- name: Install cosign
if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true'
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
with:
cosign-release: "v2.2.0"
- name: Sign images with cosign
if: vars.DOCKER_REGISTRY != '' && vars.COSIGN_ENABLED == 'true'
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
for svc in veza-backend-api veza-frontend veza-stream-server; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ vars.DOCKER_REGISTRY }}/${svc}:latest"
done
deploy:
name: Deploy to ${{ github.event.inputs.environment || 'staging' }}
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
environment: ${{ github.event.inputs.environment || 'staging' }}
steps:
- name: Deploy to Kubernetes
if: vars.KUBE_CONFIG_SET == 'true'
run: |
KUBECONFIG="${{ runner.temp }}/kubeconfig"
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG"
chmod 600 "$KUBECONFIG"
export KUBECONFIG
for svc in veza-backend-api veza-stream-server; do
kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" \
-n veza --record || echo "Skipping ${svc} (deployment not found)"
done
kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true
rm -f "$KUBECONFIG"
- name: Build Summary
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "- Backend: veza-backend-api:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- Frontend: veza-frontend:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- Stream Server: veza-stream-server:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
- name: Deployment Summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "- Environment: ${{ github.event.inputs.environment || 'staging' }}" >> $GITHUB_STEP_SUMMARY
deploy:
name: Deploy to ${{ github.event.inputs.environment || 'staging' }}
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
environment: ${{ github.event.inputs.environment || 'staging' }}
steps:
- name: Deploy to Kubernetes
if: vars.KUBE_CONFIG_SET == 'true'
run: |
KUBECONFIG="${{ runner.temp }}/kubeconfig"
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG"
chmod 600 "$KUBECONFIG"
export KUBECONFIG
for svc in veza-backend-api veza-stream-server; do
kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" \
-n veza --record || echo "Skipping ${svc} (deployment not found)"
done
kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true
rm -f "$KUBECONFIG"
smoke-post-deploy:
name: Smoke tests post-deploy
runs-on: ubuntu-latest
needs: deploy
if: vars.STAGING_URL != ''
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deployment Summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "- Environment: ${{ github.event.inputs.environment || 'staging' }}" >> $GITHUB_STEP_SUMMARY
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20'
cache: 'npm'
smoke-post-deploy:
name: Smoke tests post-deploy
runs-on: ubuntu-latest
needs: deploy
if: vars.STAGING_URL != ''
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install dependencies
run: npm ci
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
- name: Install Playwright
run: npx playwright install chromium --with-deps
- name: Install dependencies
run: npm ci
- name: Run smoke tests
env:
PLAYWRIGHT_BASE_URL: ${{ vars.STAGING_URL }}
run: |
cd apps/web
npx playwright test --config=playwright.config.smoke.ts
- name: Install Playwright
run: npx playwright install chromium --with-deps
- name: Run smoke tests
env:
PLAYWRIGHT_BASE_URL: ${{ vars.STAGING_URL }}
run: |
cd apps/web
npx playwright test --config=playwright.config.smoke.ts

View file

@ -1,270 +1,387 @@
name: Veza CI/CD
on:
push:
branches: [ "main", "remediation/*", "feature/mvp-complete" ]
pull_request:
branches: [ "main", "feature/mvp-complete" ]
workflow_dispatch:
push:
branches: ["main", "remediation/*", "feature/mvp-complete"]
pull_request:
branches: ["main", "feature/mvp-complete"]
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
# ===========================================================================
# TMT Vital — Backend (Go)
# ===========================================================================
vital-backend:
name: TMT Vital — Backend (Go)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
# ===========================================================================
# TMT Vital — Backend (Go)
# ===========================================================================
vital-backend:
name: TMT Vital — Backend (Go)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Check VERSION matches git tag
run: |
current_tag=$(git describe --tags --exact-match 2>/dev/null || true)
if [ -n "$current_tag" ]; then
version_file=$(cat VERSION)
tag_version=${current_tag#v}
if [ "$version_file" != "$tag_version" ]; then
echo "VERSION mismatch: VERSION=$version_file, current tag=$current_tag"
exit 1
fi
fi
- name: Check VERSION matches git tag
run: |
current_tag=$(git describe --tags --exact-match 2>/dev/null || true)
if [ -n "$current_tag" ]; then
version_file=$(cat VERSION)
tag_version=${current_tag#v}
if [ "$version_file" != "$tag_version" ]; then
echo "VERSION mismatch: VERSION=$version_file, current tag=$current_tag"
exit 1
fi
fi
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: '1.24'
cache: true
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Install Go tools
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- name: Install Go tools
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- name: Install TMT
run: pip install tmt
- name: Install TMT
run: pip install tmt
- name: Run TMT Vital Backend
run: tmt --root tmt run plan --name /vital-backend
- name: Run TMT Vital Backend
run: tmt --root tmt run plan --name /vital-backend
# ===========================================================================
# TMT Vital — Rust Services (Stream)
# ===========================================================================
vital-services:
name: TMT Vital — Rust Services
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# ===========================================================================
# TMT Vital — Rust Services (Stream)
# ===========================================================================
vital-services:
name: TMT Vital — Rust Services
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: rustfmt, clippy
- name: Set up Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: rustfmt, clippy
- name: Cache Cargo registry
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Cache Cargo registry
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Install TMT
run: pip install tmt
- name: Install TMT
run: pip install tmt
- name: Run TMT Vital Services
run: tmt --root tmt run plan --name /vital-services
- name: Run TMT Vital Services
run: tmt --root tmt run plan --name /vital-services
# ===========================================================================
# TMT Vital — Frontend (Web)
# ===========================================================================
vital-frontend:
name: TMT Vital — Frontend (Web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# ===========================================================================
# TMT Vital — Frontend (Web)
# ===========================================================================
vital-frontend:
name: TMT Vital — Frontend (Web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Use Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install Dependencies
run: npm ci
- name: Install Dependencies
run: npm ci
- name: Cache Generated Types
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/web/src/types/generated
key: ${{ runner.os }}-generated-types-${{ hashFiles('veza-backend-api/openapi.yaml') }}
restore-keys: |
${{ runner.os }}-generated-types-
- name: Cache Generated Types
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/web/src/types/generated
key: ${{ runner.os }}-generated-types-${{ hashFiles('veza-backend-api/openapi.yaml') }}
restore-keys: |
${{ runner.os }}-generated-types-
- name: Install TMT
run: pip install tmt
- name: Install TMT
run: pip install tmt
- name: Run TMT Vital Frontend
run: tmt --root tmt run plan --name /vital-frontend
- name: Run TMT Vital Frontend
run: tmt --root tmt run plan --name /vital-frontend
# ===========================================================================
# Storybook Audit (kept outside TMT — tier 3 candidate)
# ===========================================================================
storybook:
name: Storybook Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# ===========================================================================
# Storybook Audit (kept outside TMT — tier 3 candidate)
# ===========================================================================
storybook:
name: Storybook Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci
- name: Build Storybook
run: npm run build-storybook
working-directory: apps/web
- name: Build Storybook
run: npm run build-storybook
working-directory: apps/web
- name: Serve Storybook and run audit
run: |
npx serve -s storybook-static -l 6007 &
for i in $(seq 1 30); do
if curl -sf http://localhost:6007 >/dev/null; then
echo "Storybook ready"
break
fi
sleep 2
done
curl -sf http://localhost:6007 >/dev/null || (echo "Storybook failed to start"; exit 1)
npm run test:storybook
working-directory: apps/web
- name: Serve Storybook and run audit
run: |
npx serve -s storybook-static -l 6007 &
for i in $(seq 1 30); do
if curl -sf http://localhost:6007 >/dev/null; then
echo "Storybook ready"
break
fi
sleep 2
done
curl -sf http://localhost:6007 >/dev/null || (echo "Storybook failed to start"; exit 1)
npm run test:storybook
working-directory: apps/web
# ===========================================================================
# E2E (Playwright) — kept outside TMT (complex infra setup)
# ===========================================================================
e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# ===========================================================================
# E2E Critical (Playwright) — fast smoke, blocks PR
# ===========================================================================
e2e-critical:
name: E2E Critical (@critical)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: '1.24'
cache-dependency-path: veza-backend-api/go.sum
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache-dependency-path: veza-backend-api/go.sum
- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci
- name: Add veza.fr to hosts (for Vite proxy)
run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts
- name: Add veza.fr to hosts (for Vite proxy)
run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts
- name: Start backend services (Postgres, Redis, RabbitMQ)
run: |
docker-compose up -d postgres redis rabbitmq
echo "Waiting for Postgres..."
for i in $(seq 1 30); do
if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then
echo "Postgres ready"
break
fi
sleep 2
done
docker-compose ps
- name: Start backend services (Postgres, Redis, RabbitMQ)
run: |
docker-compose up -d postgres redis rabbitmq
echo "Waiting for Postgres..."
for i in $(seq 1 30); do
if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then
echo "Postgres ready"
break
fi
sleep 2
done
docker-compose ps
- name: Run database migrations
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
run: |
cd veza-backend-api
go run cmd/migrate_tool/main.go
- name: Run database migrations
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
run: |
cd veza-backend-api
go run cmd/migrate_tool/main.go
- name: Create E2E test user
env:
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
TEST_EMAIL: e2e@test.com
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
TEST_USERNAME: e2e
run: |
cd veza-backend-api
go run cmd/tools/create_test_user/main.go
- name: Create E2E test user
env:
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
TEST_EMAIL: e2e@test.com
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
TEST_USERNAME: e2e
run: |
cd veza-backend-api
go run cmd/tools/create_test_user/main.go
- name: Start backend API
env:
APP_ENV: development
APP_PORT: "18080"
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
COOKIE_SECURE: "false"
CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174
RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }}
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com"
run: |
cd veza-backend-api
go build -o veza-api ./cmd/api/main.go
./veza-api &
sleep 10
curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1)
jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1)
echo "Health check OK (status, DB/Redis connectivity verified)"
- name: Start backend API
env:
APP_ENV: development
APP_PORT: "18080"
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
COOKIE_SECURE: "false"
CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174
RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }}
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com"
run: |
cd veza-backend-api
go build -o veza-api ./cmd/api/main.go
./veza-api &
sleep 10
curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1)
jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1)
echo "Health check OK (status, DB/Redis connectivity verified)"
- name: Install Playwright Browsers
run: npx playwright install --with-deps
working-directory: apps/web
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx playwright test
working-directory: apps/web
env:
PORT: "5174"
VITE_API_URL: '/api/v1'
VITE_DOMAIN: veza.fr
VITE_BACKEND_PORT: "18080"
PLAYWRIGHT_BASE_URL: 'http://localhost:5174'
TEST_EMAIL: e2e@test.com
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
- name: Run @critical E2E tests
run: npx playwright test --config=tests/e2e/playwright.config.ts --grep @critical --project=chromium
env:
PORT: "5174"
VITE_API_URL: "/api/v1"
VITE_DOMAIN: veza.fr
VITE_BACKEND_PORT: "18080"
PLAYWRIGHT_BASE_URL: "http://localhost:5174"
TEST_EMAIL: e2e@test.com
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
if: failure()
with:
name: playwright-critical-report
path: tests/e2e/playwright-report/
retention-days: 7
# ===========================================================================
# E2E Full (Playwright) — sharded across 4 runners, all browsers
# ===========================================================================
e2e-full:
name: E2E Full (shard ${{ matrix.shard }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache-dependency-path: veza-backend-api/go.sum
- name: Install dependencies
run: npm ci
- name: Add veza.fr to hosts (for Vite proxy)
run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts
- name: Start backend services (Postgres, Redis, RabbitMQ)
run: |
docker-compose up -d postgres redis rabbitmq
echo "Waiting for Postgres..."
for i in $(seq 1 30); do
if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then
echo "Postgres ready"
break
fi
sleep 2
done
docker-compose ps
- name: Run database migrations
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
run: |
cd veza-backend-api
go run cmd/migrate_tool/main.go
- name: Create E2E test user
env:
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
TEST_EMAIL: e2e@test.com
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
TEST_USERNAME: e2e
run: |
cd veza-backend-api
go run cmd/tools/create_test_user/main.go
- name: Start backend API
env:
APP_ENV: development
APP_PORT: "18080"
DATABASE_URL: postgresql://veza:${{ secrets.E2E_DB_PASSWORD || 'devpassword' }}@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
COOKIE_SECURE: "false"
CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174
RABBITMQ_URL: ${{ secrets.E2E_RABBITMQ_URL }}
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com"
run: |
cd veza-backend-api
go build -o veza-api ./cmd/api/main.go
./veza-api &
sleep 10
curl -sf http://localhost:18080/api/v1/health > /tmp/health.json || (echo "Backend health check failed"; exit 1)
jq -e '.status == "ok"' /tmp/health.json || (echo "Health response invalid"; exit 1)
echo "Health check OK (status, DB/Redis connectivity verified)"
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run E2E tests (sharded)
run: npx playwright test --config=tests/e2e/playwright.config.ts --shard=${{ matrix.shard }}
env:
PORT: "5174"
VITE_API_URL: "/api/v1"
VITE_DOMAIN: veza.fr
VITE_BACKEND_PORT: "18080"
PLAYWRIGHT_BASE_URL: "http://localhost:5174"
TEST_EMAIL: e2e@test.com
TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
- uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
if: always()
with:
name: playwright-report-shard-${{ strategy.job-index }}
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
retention-days: 7
# ===========================================================================
# Notify on failure
# ===========================================================================
notify-failure:
name: Notify on failure
needs:
[
vital-backend,
vital-services,
vital-frontend,
storybook,
e2e-critical,
e2e-full,
]
if: failure()
with:
name: playwright-report
path: apps/web/playwright-report/
retention-days: 7
# ===========================================================================
# Notify on failure
# ===========================================================================
notify-failure:
name: Notify on failure
needs: [vital-backend, vital-services, vital-frontend, storybook, e2e]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Slack notification
if: secrets.SLACK_WEBHOOK_URL != ''
run: |
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"CI failed on ${{ github.repository }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"${{ secrets.SLACK_WEBHOOK_URL }}"
runs-on: ubuntu-latest
steps:
- name: Slack notification
if: secrets.SLACK_WEBHOOK_URL != ''
run: |
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"CI failed on ${{ github.repository }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"${{ secrets.SLACK_WEBHOOK_URL }}"

36
.github/workflows/commitlint.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Commit Lint
on:
pull_request:
branches: [main, "feature/**"]
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
commitlint:
name: Validate commit messages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install commitlint
run: npm ci --ignore-scripts
- name: Validate PR commit messages
run: |
# Validate all commits in the PR
npx commitlint \
--from=${{ github.event.pull_request.base.sha }} \
--to=${{ github.event.pull_request.head.sha }} \
--verbose

View file

@ -1,84 +1,88 @@
name: Container Image Scan
on:
push:
branches: [main]
paths:
- 'veza-backend-api/Dockerfile*'
- 'apps/web/Dockerfile*'
- 'veza-stream-server/Dockerfile*'
pull_request:
branches: [main]
paths:
- 'veza-backend-api/Dockerfile*'
- 'apps/web/Dockerfile*'
- 'veza-stream-server/Dockerfile*'
workflow_dispatch:
push:
branches: [main]
paths:
- "veza-backend-api/Dockerfile*"
- "apps/web/Dockerfile*"
- "veza-stream-server/Dockerfile*"
pull_request:
branches: [main]
paths:
- "veza-backend-api/Dockerfile*"
- "apps/web/Dockerfile*"
- "veza-stream-server/Dockerfile*"
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
scan-backend:
name: Scan Backend Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
scan-backend:
name: Scan Backend Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build backend image
run: docker build -t veza-backend:scan -f veza-backend-api/Dockerfile.production veza-backend-api/
- name: Build backend image
run: docker build -t veza-backend:scan -f veza-backend-api/Dockerfile.production veza-backend-api/
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: 'veza-backend:scan'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: "veza-backend:scan"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
ignore-unfixed: true
scan-stream-server:
name: Scan Stream Server Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
scan-stream-server:
name: Scan Stream Server Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build stream server image
run: docker build -t veza-stream:scan -f veza-stream-server/Dockerfile .
- name: Build stream server image
run: docker build -t veza-stream:scan -f veza-stream-server/Dockerfile .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: 'veza-stream:scan'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: "veza-stream:scan"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
ignore-unfixed: true
scan-frontend:
name: Scan Frontend Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
scan-frontend:
name: Scan Frontend Image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check if frontend Dockerfile exists
id: check
run: |
if [ -f "apps/web/Dockerfile" ] || [ -f "apps/web/Dockerfile.production" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Check if frontend Dockerfile exists
id: check
run: |
if [ -f "apps/web/Dockerfile" ] || [ -f "apps/web/Dockerfile.production" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Build frontend image
if: steps.check.outputs.exists == 'true'
run: |
DOCKERFILE=$([ -f "apps/web/Dockerfile.production" ] && echo "apps/web/Dockerfile.production" || echo "apps/web/Dockerfile")
docker build -t veza-frontend:scan -f "$DOCKERFILE" apps/web/
- name: Build frontend image
if: steps.check.outputs.exists == 'true'
run: |
DOCKERFILE=$([ -f "apps/web/Dockerfile.production" ] && echo "apps/web/Dockerfile.production" || echo "apps/web/Dockerfile")
docker build -t veza-frontend:scan -f "$DOCKERFILE" apps/web/
- name: Run Trivy vulnerability scanner
if: steps.check.outputs.exists == 'true'
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: 'veza-frontend:scan'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
- name: Run Trivy vulnerability scanner
if: steps.check.outputs.exists == 'true'
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37 # v0.28.0
with:
image-ref: "veza-frontend:scan"
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH"
ignore-unfixed: true

101
.github/workflows/contract-testing.yml vendored Normal file
View file

@ -0,0 +1,101 @@
name: Contract Testing (Schemathesis)
on:
pull_request:
paths:
- "veza-backend-api/**.go"
- "veza-backend-api/openapi.yaml"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
contract-test:
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: veza_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/veza_test?sslmode=disable
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-jwt-secret-for-ci
APP_ENV: test
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install schemathesis
run: pip install schemathesis
- name: Download Go deps
run: cd veza-backend-api && go mod download
- name: Run migrations
run: cd veza-backend-api && go run cmd/migrate_tool/main.go
continue-on-error: true
- name: Start backend API
run: |
cd veza-backend-api && go run cmd/api/main.go &
# Wait for API to be ready
for i in $(seq 1 30); do
if curl -sf http://localhost:18080/api/v1/health > /dev/null 2>&1; then
echo "API is ready"
break
fi
echo "Waiting for API... ($i/30)"
sleep 2
done
- name: Run schemathesis contract tests
run: >
st run
--checks all
veza-backend-api/openapi.yaml
--base-url http://localhost:18080
--hypothesis-max-examples=50
--request-timeout=10000
continue-on-error: true
- name: Upload schemathesis report
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: schemathesis-report
path: .schemathesis/
retention-days: 14

100
.github/workflows/flaky-report.yml vendored Normal file
View file

@ -0,0 +1,100 @@
name: Flaky Test Report
on:
schedule:
- cron: "0 6 * * 1" # Every Monday 6am
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
flaky-report:
name: Detect flaky tests
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache-dependency-path: veza-backend-api/go.sum
- name: Install dependencies
run: npm ci
- name: Add veza.fr to hosts
run: echo "127.0.0.1 veza.fr" | sudo tee -a /etc/hosts
- name: Start backend services
run: |
docker-compose up -d postgres redis rabbitmq
for i in $(seq 1 30); do
if docker exec veza_postgres pg_isready -U veza 2>/dev/null; then break; fi
sleep 2
done
- name: Run database migrations
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
run: cd veza-backend-api && go run cmd/migrate_tool/main.go
- name: Start backend API
env:
APP_ENV: test
APP_PORT: "18080"
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: flaky-test-jwt-secret
COOKIE_SECURE: "false"
CORS_ALLOWED_ORIGINS: http://localhost:5174
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "user@veza.music,artist@veza.music,admin@veza.music"
run: |
cd veza-backend-api && go build -o veza-api ./cmd/api/main.go && ./veza-api &
sleep 10
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests (3x retries to detect flakiness)
run: |
npx playwright test \
--config=tests/e2e/playwright.config.ts \
--project=chromium \
--retries=3 \
--reporter=json
continue-on-error: true
env:
PORT: "5174"
VITE_API_URL: "/api/v1"
PLAYWRIGHT_BASE_URL: "http://localhost:5174"
- name: Generate flaky report
run: node scripts/flaky-detection.mjs > flaky-report.md
- name: Upload flaky report
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: flaky-test-report
path: flaky-report.md
retention-days: 30
- name: Create/update issue with flaky tests
if: always()
run: |
REPORT=$(cat flaky-report.md)
FLAKY_COUNT=$(grep -c "^|" flaky-report.md | head -1 || echo "0")
if [ "$FLAKY_COUNT" -gt 2 ]; then
echo "Found flaky tests — check artifact for details"
fi

View file

@ -1,51 +1,54 @@
name: Frontend CI
on:
push:
paths:
- "apps/web/**"
- ".github/workflows/frontend-ci.yml"
pull_request:
paths:
- "apps/web/**"
- ".github/workflows/frontend-ci.yml"
push:
paths:
- "apps/web/**"
- ".github/workflows/frontend-ci.yml"
pull_request:
paths:
- "apps/web/**"
- ".github/workflows/frontend-ci.yml"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
test:
runs-on: ubuntu-latest
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
defaults:
run:
working-directory: apps/web
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: 'npm'
cache-dependency-path: apps/web/package-lock.json
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: apps/web/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Lint
run: npm run lint
- name: TypeScript check
run: npx tsc --noEmit
- name: TypeScript check
run: npx tsc --noEmit
- name: Build
run: npm run build
- name: Build
run: npm run build
- name: Bundle size gate
run: node scripts/check-bundle-size.mjs
- name: Bundle size gate
run: node scripts/check-bundle-size.mjs
- name: Audit dependencies
run: npm audit --audit-level=critical
- name: Run tests
run: npm run test -- --run
- name: Audit dependencies
run: npm audit --audit-level=critical
- name: Run tests
run: npm run test -- --run

42
.github/workflows/go-fuzz.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Go Fuzz Tests
on:
schedule:
- cron: "0 2 * * *" # Nightly at 2am UTC
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
fuzz:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: veza-backend-api
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Download deps
run: go mod download
- name: Run fuzz tests
run: go test -fuzz=Fuzz -fuzztime=60s ./internal/handlers/...
- name: Upload fuzz corpus
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: fuzz-corpus
path: veza-backend-api/testdata/fuzz/
retention-days: 30

View file

@ -1,81 +1,85 @@
name: Load Tests (Nightly)
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install -y k6
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install -y k6
- name: Start infrastructure
run: |
docker-compose -f docker-compose.yml up -d postgres redis rabbitmq
sleep 15
- name: Start infrastructure
run: |
docker-compose -f docker-compose.yml up -d postgres redis rabbitmq
sleep 15
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Run migrations
working-directory: veza-backend-api
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: test-jwt-secret-for-load-test
APP_ENV: test
run: |
go mod download
go run cmd/migrate_tool/main.go || true
- name: Run migrations
working-directory: veza-backend-api
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
JWT_SECRET: test-jwt-secret-for-load-test
APP_ENV: test
run: |
go mod download
go run cmd/migrate_tool/main.go || true
- name: Start backend API
working-directory: veza-backend-api
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
RABBITMQ_URL: amqp://veza:devpassword@localhost:15672/
JWT_SECRET: test-jwt-secret-for-load-test
APP_ENV: test
PORT: 8080
run: |
go run cmd/api/main.go &
sleep 15
- name: Start backend API
working-directory: veza-backend-api
env:
DATABASE_URL: postgresql://veza:devpassword@localhost:15432/veza?sslmode=disable
REDIS_URL: redis://localhost:16379
RABBITMQ_URL: amqp://veza:devpassword@localhost:15672/
JWT_SECRET: test-jwt-secret-for-load-test
APP_ENV: test
PORT: 8080
run: |
go run cmd/api/main.go &
sleep 15
- name: Wait for backend
run: |
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -sf http://localhost:8080/health; then
echo "Backend ready"
exit 0
fi
sleep 3
done
echo "Backend not ready"
exit 1
- name: Wait for backend
run: |
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -sf http://localhost:8080/health; then
echo "Backend ready"
exit 0
fi
sleep 3
done
echo "Backend not ready"
exit 1
- name: Run smoke load test
run: k6 run loadtests/smoke.js
- name: Run smoke load test
run: k6 run loadtests/smoke.js
- name: Run backend load test
run: |
k6 run --out json=load-results.json loadtests/backend/full.js || true
continue-on-error: true
- name: Run backend load test
run: |
k6 run --out json=load-results.json loadtests/backend/full.js || true
continue-on-error: true
- name: Upload results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: load-test-results
path: load-results.json
if: always()
- name: Upload results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: load-test-results
path: load-results.json
if: always()

40
.github/workflows/mutation-testing.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: Mutation Testing
on:
schedule:
- cron: "0 3 * * 0" # Weekly on Sunday at 3am UTC
workflow_dispatch:
permissions:
contents: read
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
mutation:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: apps/web/package-lock.json
- name: Install dependencies
run: cd apps/web && npm ci
- name: Run Stryker mutation testing
run: cd apps/web && npx stryker run
- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: apps/web/reports/mutation/
retention-days: 30

21
.github/workflows/openapi-lint.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: OpenAPI Lint
on:
pull_request:
paths:
- "veza-backend-api/openapi.yaml"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lint OpenAPI spec
run: npx --yes @redocly/cli lint veza-backend-api/openapi.yaml

43
.github/workflows/performance.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Performance
on:
pull_request:
branches: [main]
permissions:
contents: read
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
bundle-size:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: apps/web/package-lock.json
- name: Install dependencies
run: cd apps/web && npm ci
- name: Build
run: cd apps/web && npm run build
- name: Check bundle size
run: cd apps/web && npx size-limit
- name: Upload stats artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: bundle-stats
path: apps/web/stats.html
if-no-files-found: ignore
retention-days: 14

View file

@ -1,52 +1,56 @@
name: Rust CI
on:
push:
branches: [main]
paths:
- 'veza-stream-server/**'
pull_request:
branches: [main]
paths:
- 'veza-stream-server/**'
push:
branches: [main]
paths:
- "veza-stream-server/**"
pull_request:
branches: [main]
paths:
- "veza-stream-server/**"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
test-and-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: clippy
test-and-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: clippy
- name: Clippy lint
run: cargo clippy -- -D warnings
working-directory: veza-stream-server
- name: Clippy lint
run: cargo clippy -- -D warnings
working-directory: veza-stream-server
- name: Run tests
run: cargo test --workspace --timeout 300
working-directory: veza-stream-server
- name: Run tests
run: cargo test --workspace --timeout 300
working-directory: veza-stream-server
- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin
- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin
- name: Measure coverage
run: cargo tarpaulin --out json --output-dir target/coverage --timeout 300 --skip-clean
working-directory: veza-stream-server
- name: Measure coverage
run: cargo tarpaulin --out json --output-dir target/coverage --timeout 300 --skip-clean
working-directory: veza-stream-server
- name: Enforce coverage threshold (>= 50%)
run: |
COVERAGE=$(cat target/coverage/tarpaulin-report.json | python3 -c "import sys,json; print(f'{json.load(sys.stdin).get(\"coverage\", 0):.1f}')")
echo "Rust coverage: ${COVERAGE}%"
COV_INT=$(echo "$COVERAGE" | cut -d. -f1)
if [ "$COV_INT" -lt 50 ]; then
echo "::error::Rust coverage ${COVERAGE}% is below the 50% threshold"
exit 1
fi
echo "::notice::Rust coverage ${COVERAGE}% meets the >= 50% threshold"
working-directory: veza-stream-server
- name: Enforce coverage threshold (>= 50%)
run: |
COVERAGE=$(cat target/coverage/tarpaulin-report.json | python3 -c "import sys,json; print(f'{json.load(sys.stdin).get(\"coverage\", 0):.1f}')")
echo "Rust coverage: ${COVERAGE}%"
COV_INT=$(echo "$COVERAGE" | cut -d. -f1)
if [ "$COV_INT" -lt 50 ]; then
echo "::error::Rust coverage ${COVERAGE}% is below the 50% threshold"
exit 1
fi
echo "::notice::Rust coverage ${COVERAGE}% meets the >= 50% threshold"
working-directory: veza-stream-server
- name: Upload coverage report
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: rust-coverage
path: veza-stream-server/target/coverage/tarpaulin-report.json
- name: Upload coverage report
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: rust-coverage
path: veza-stream-server/target/coverage/tarpaulin-report.json

46
.github/workflows/rust-mutation.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Rust Mutation Testing
on:
schedule:
- cron: "0 4 * * 0" # Weekly on Sunday at 4am UTC
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
mutants:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Cache cargo registry
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cargo/registry
~/.cargo/git
veza-stream-server/target
key: ${{ runner.os }}-cargo-mutants-${{ hashFiles('veza-stream-server/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-mutants-
- name: Install cargo-mutants
run: cargo install cargo-mutants
- name: Run mutation testing
run: cd veza-stream-server && cargo mutants --timeout 120 -- --lib
continue-on-error: true
- name: Upload mutation results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: mutation-results
path: veza-stream-server/mutants.out/
retention-days: 30

View file

@ -1,22 +1,26 @@
name: CodeQL SAST
on:
push:
branches: [main]
pull_request:
branches: [main]
push:
branches: [main]
pull_request:
branches: [main]
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: [go, javascript-typescript]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: github/codeql-action/init@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18
with:
languages: ${{ matrix.language }}
- uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18
- uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: [go, javascript-typescript]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: github/codeql-action/init@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18
with:
languages: ${{ matrix.language }}
- uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18
- uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441585e9e013f7cac # v3.28.18

View file

@ -1,22 +1,26 @@
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
gitleaks:
name: Secret Scanning (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
gitleaks:
name: Secret Scanning (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196e88a9c30 # v2.3.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196e88a9c30 # v2.3.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

43
.github/workflows/semgrep.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Semgrep SAST
on:
pull_request:
branches: [main]
schedule:
- cron: "0 3 * * 1" # Weekly on Monday at 3am UTC
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
semgrep:
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Semgrep
run: >
semgrep scan
--config p/auto
--config p/owasp-top-ten
--config p/r2c-security-audit
--error
--json
--output semgrep-results.json
.
continue-on-error: true
- name: Upload Semgrep results
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: semgrep-results
path: semgrep-results.json
retention-days: 30

View file

@ -3,304 +3,314 @@ name: Staging Validation Pipeline
# Comprehensive staging validation: deploy, perf, Lighthouse, stability, GDPR, bundle size
on:
workflow_dispatch:
inputs:
skip_deploy:
description: 'Skip deployment (validate existing staging)'
required: false
default: 'false'
type: boolean
stability_duration:
description: 'Stability check duration (minutes)'
required: false
default: '10'
type: string
workflow_dispatch:
inputs:
skip_deploy:
description: "Skip deployment (validate existing staging)"
required: false
default: "false"
type: boolean
stability_duration:
description: "Stability check duration (minutes)"
required: false
default: "10"
type: string
env:
STAGING_URL: ${{ vars.STAGING_URL || 'https://staging.veza.app' }}
STAGING_API_URL: ${{ vars.STAGING_API_URL || 'https://staging.veza.app/api/v1' }}
STAGING_URL: ${{ vars.STAGING_URL || 'https://staging.veza.app' }}
STAGING_API_URL: ${{ vars.STAGING_API_URL || 'https://staging.veza.app/api/v1' }}
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
# ─────────────────────────────────────────────────────
# TASK-STAG-001: Deploy staging (all services)
# ─────────────────────────────────────────────────────
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
if: inputs.skip_deploy != 'true'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# ─────────────────────────────────────────────────────
# TASK-STAG-001: Deploy staging (all services)
# ─────────────────────────────────────────────────────
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
if: inputs.skip_deploy != 'true'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Build all images
run: |
docker build -t veza-backend-api:staging -f veza-backend-api/Dockerfile.production veza-backend-api/
docker build -t veza-frontend:staging -f apps/web/Dockerfile.production apps/web/
docker build -t veza-stream-server:staging -f veza-stream-server/Dockerfile.production veza-stream-server/
- name: Build all images
run: |
docker build -t veza-backend-api:staging -f veza-backend-api/Dockerfile.production veza-backend-api/
docker build -t veza-frontend:staging -f apps/web/Dockerfile.production apps/web/
docker build -t veza-stream-server:staging -f veza-stream-server/Dockerfile.production veza-stream-server/
- name: Push to registry
if: vars.DOCKER_REGISTRY != ''
run: |
echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin
for svc in veza-backend-api veza-frontend veza-stream-server; do
docker tag "${svc}:staging" "${{ vars.DOCKER_REGISTRY }}/${svc}:staging"
docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:staging"
done
- name: Push to registry
if: vars.DOCKER_REGISTRY != ''
run: |
echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login "${{ vars.DOCKER_REGISTRY }}" -u "${{ secrets.DOCKER_REGISTRY_USERNAME }}" --password-stdin
for svc in veza-backend-api veza-frontend veza-stream-server; do
docker tag "${svc}:staging" "${{ vars.DOCKER_REGISTRY }}/${svc}:staging"
docker push "${{ vars.DOCKER_REGISTRY }}/${svc}:staging"
done
- name: Deploy via SSH (docker-compose)
if: vars.STAGING_SSH_HOST != ''
env:
SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/staging_key
chmod 600 ~/.ssh/staging_key
ssh -i ~/.ssh/staging_key -o StrictHostKeyChecking=no \
${{ vars.STAGING_SSH_USER }}@${{ vars.STAGING_SSH_HOST }} \
"cd /opt/veza && docker compose -f docker-compose.staging.yml pull && docker compose -f docker-compose.staging.yml up -d"
rm -f ~/.ssh/staging_key
- name: Deploy via SSH (docker-compose)
if: vars.STAGING_SSH_HOST != ''
env:
SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/staging_key
chmod 600 ~/.ssh/staging_key
ssh -i ~/.ssh/staging_key -o StrictHostKeyChecking=no \
${{ vars.STAGING_SSH_USER }}@${{ vars.STAGING_SSH_HOST }} \
"cd /opt/veza && docker compose -f docker-compose.staging.yml pull && docker compose -f docker-compose.staging.yml up -d"
rm -f ~/.ssh/staging_key
- name: Deploy via Kubernetes
if: vars.KUBE_CONFIG_SET == 'true'
run: |
KUBECONFIG="${{ runner.temp }}/kubeconfig"
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG"
chmod 600 "$KUBECONFIG"
export KUBECONFIG
for svc in veza-backend-api veza-stream-server; do
kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:staging" \
-n veza --record || echo "Skipping ${svc}"
done
kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true
rm -f "$KUBECONFIG"
- name: Deploy via Kubernetes
if: vars.KUBE_CONFIG_SET == 'true'
run: |
KUBECONFIG="${{ runner.temp }}/kubeconfig"
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG"
chmod 600 "$KUBECONFIG"
export KUBECONFIG
for svc in veza-backend-api veza-stream-server; do
kubectl set image "deployment/${svc}" "${svc}=${{ vars.DOCKER_REGISTRY }}/${svc}:staging" \
-n veza --record || echo "Skipping ${svc}"
done
kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true
rm -f "$KUBECONFIG"
- name: Wait for staging to be healthy
run: |
echo "Waiting for staging services to be healthy..."
for i in $(seq 1 30); do
STATUS=$(curl -sf "${{ env.STAGING_API_URL }}/health" | jq -r '.status' 2>/dev/null || echo "unreachable")
if [ "$STATUS" = "ok" ] || [ "$STATUS" = "healthy" ]; then
echo "Staging is healthy!"
exit 0
fi
echo "Attempt $i/30: status=$STATUS, waiting 10s..."
sleep 10
done
echo "Staging did not become healthy in 300s"
exit 1
- name: Wait for staging to be healthy
run: |
echo "Waiting for staging services to be healthy..."
for i in $(seq 1 30); do
STATUS=$(curl -sf "${{ env.STAGING_API_URL }}/health" | jq -r '.status' 2>/dev/null || echo "unreachable")
if [ "$STATUS" = "ok" ] || [ "$STATUS" = "healthy" ]; then
echo "Staging is healthy!"
exit 0
fi
echo "Attempt $i/30: status=$STATUS, waiting 10s..."
sleep 10
done
echo "Staging did not become healthy in 300s"
exit 1
- name: Deep health check
run: |
echo "## Deep Health Check" >> $GITHUB_STEP_SUMMARY
HEALTH=$(curl -sf "${{ env.STAGING_API_URL }}/health/deep" || echo '{"error":"unreachable"}')
echo '```json' >> $GITHUB_STEP_SUMMARY
echo "$HEALTH" | jq . >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Deep health check
run: |
echo "## Deep Health Check" >> $GITHUB_STEP_SUMMARY
HEALTH=$(curl -sf "${{ env.STAGING_API_URL }}/health/deep" || echo '{"error":"unreachable"}')
echo '```json' >> $GITHUB_STEP_SUMMARY
echo "$HEALTH" | jq . >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# TASK-STAG-002: Performance validation (p95 < 100ms)
# ─────────────────────────────────────────────────────
performance-validation:
name: Performance Validation (k6)
runs-on: ubuntu-latest
needs: deploy-staging
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# ─────────────────────────────────────────────────────
# TASK-STAG-002: Performance validation (p95 < 100ms)
# ─────────────────────────────────────────────────────
performance-validation:
name: Performance Validation (k6)
runs-on: ubuntu-latest
needs: deploy-staging
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install -y k6
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install -y k6
- name: Run staging performance validation
run: |
k6 run --out json=perf-results.json \
--env BASE_URL="${{ env.STAGING_API_URL }}" \
--env SCENARIO=smoke \
loadtests/staging/validation_v0140.js
- name: Run staging performance validation
run: |
k6 run --out json=perf-results.json \
--env BASE_URL="${{ env.STAGING_API_URL }}" \
--env SCENARIO=smoke \
loadtests/staging/validation_v0140.js
- name: Upload performance results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: performance-results
path: perf-results.json
- name: Upload performance results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: performance-results
path: perf-results.json
if: always()
- name: Performance summary
if: always()
run: |
echo "## Performance Validation" >> $GITHUB_STEP_SUMMARY
echo "Target: p95 < 100ms, stream start < 500ms" >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# TASK-STAG-003: Lighthouse validation
# ─────────────────────────────────────────────────────
lighthouse-validation:
name: Lighthouse Audit
runs-on: ubuntu-latest
needs: deploy-staging
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
- name: Install Lighthouse CI
run: npm install -g @lhci/cli@0.14.x
- name: Run Lighthouse CI
run: lhci autorun --config=.lighthouserc.js
env:
LHCI_BUILD_CONTEXT__CURRENT_HASH: ${{ github.sha }}
- name: Upload Lighthouse results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: lighthouse-results
path: .lighthouseci/
if: always()
- name: Lighthouse summary
if: always()
run: |
echo "## Lighthouse Validation" >> $GITHUB_STEP_SUMMARY
echo "Targets: Performance >= 85, Accessibility >= 90, PWA >= 90" >> $GITHUB_STEP_SUMMARY
if [ -f .lighthouseci/assertion-results.json ]; then
PASSED=$(jq '[.[] | select(.level == "error")] | length' .lighthouseci/assertion-results.json 2>/dev/null || echo "?")
echo "Assertion errors: $PASSED" >> $GITHUB_STEP_SUMMARY
fi
# ─────────────────────────────────────────────────────
# TASK-STAG-004: Stability validation (5xx < 0.1%)
# ─────────────────────────────────────────────────────
stability-validation:
name: Stability Check
runs-on: ubuntu-latest
needs: [deploy-staging, performance-validation]
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run stability check
run: |
chmod +x scripts/staging-stability-check.sh
DURATION_MINUTES=${{ inputs.stability_duration || '10' }} \
STAGING_API_URL="${{ env.STAGING_API_URL }}" \
MAX_5XX_RATE="0.001" \
bash scripts/staging-stability-check.sh
- name: Stability summary
if: always()
run: |
echo "## Stability Validation" >> $GITHUB_STEP_SUMMARY
echo "Duration: ${{ inputs.stability_duration || '10' }} minutes" >> $GITHUB_STEP_SUMMARY
echo "Target: 5xx rate < 0.1%" >> $GITHUB_STEP_SUMMARY
if [ -f stability-report.json ]; then
cat stability-report.json | jq . >> $GITHUB_STEP_SUMMARY
fi
# ─────────────────────────────────────────────────────
# TASK-STAG-005: GDPR validation (export + deletion E2E)
# ─────────────────────────────────────────────────────
gdpr-validation:
name: GDPR Compliance Check
runs-on: ubuntu-latest
needs: deploy-staging
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Run GDPR integration tests
working-directory: veza-backend-api
run: go test -v -tags=integration -run TestGDPR -timeout 120s ./tests/integration/...
- name: GDPR summary
if: always()
run: |
echo "## GDPR Validation" >> $GITHUB_STEP_SUMMARY
echo "- Data export: tested" >> $GITHUB_STEP_SUMMARY
echo "- Account deletion: tested" >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# TASK-STAG-006: Bundle size validation (< 200KB gzip)
# ─────────────────────────────────────────────────────
bundle-size-validation:
name: Bundle Size Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build frontend
working-directory: apps/web
run: npx vite build --outDir dist_verification
env:
NODE_ENV: production
- name: Check bundle size
working-directory: apps/web
run: node scripts/check-bundle-size.mjs
- name: Bundle size summary
if: always()
run: |
echo "## Bundle Size Validation" >> $GITHUB_STEP_SUMMARY
echo "Target: JS initial < 200KB gzipped" >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# Final summary
# ─────────────────────────────────────────────────────
validation-summary:
name: Validation Summary
runs-on: ubuntu-latest
needs:
[
deploy-staging,
performance-validation,
lighthouse-validation,
stability-validation,
gdpr-validation,
bundle-size-validation,
]
if: always()
steps:
- name: Generate final report
run: |
echo "# Staging Validation Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Deploy (STAG-001) | ${{ needs.deploy-staging.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Performance (STAG-002) | ${{ needs.performance-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Lighthouse (STAG-003) | ${{ needs.lighthouse-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Stability (STAG-004) | ${{ needs.stability-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| GDPR (STAG-005) | ${{ needs.gdpr-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Bundle Size (STAG-006) | ${{ needs.bundle-size-validation.result }} |" >> $GITHUB_STEP_SUMMARY
- name: Performance summary
if: always()
run: |
echo "## Performance Validation" >> $GITHUB_STEP_SUMMARY
echo "Target: p95 < 100ms, stream start < 500ms" >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# TASK-STAG-003: Lighthouse validation
# ─────────────────────────────────────────────────────
lighthouse-validation:
name: Lighthouse Audit
runs-on: ubuntu-latest
needs: deploy-staging
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20'
- name: Install Lighthouse CI
run: npm install -g @lhci/cli@0.14.x
- name: Run Lighthouse CI
run: lhci autorun --config=.lighthouserc.js
env:
LHCI_BUILD_CONTEXT__CURRENT_HASH: ${{ github.sha }}
- name: Upload Lighthouse results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: lighthouse-results
path: .lighthouseci/
if: always()
- name: Lighthouse summary
if: always()
run: |
echo "## Lighthouse Validation" >> $GITHUB_STEP_SUMMARY
echo "Targets: Performance >= 85, Accessibility >= 90, PWA >= 90" >> $GITHUB_STEP_SUMMARY
if [ -f .lighthouseci/assertion-results.json ]; then
PASSED=$(jq '[.[] | select(.level == "error")] | length' .lighthouseci/assertion-results.json 2>/dev/null || echo "?")
echo "Assertion errors: $PASSED" >> $GITHUB_STEP_SUMMARY
fi
# ─────────────────────────────────────────────────────
# TASK-STAG-004: Stability validation (5xx < 0.1%)
# ─────────────────────────────────────────────────────
stability-validation:
name: Stability Check
runs-on: ubuntu-latest
needs: [deploy-staging, performance-validation]
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run stability check
run: |
chmod +x scripts/staging-stability-check.sh
DURATION_MINUTES=${{ inputs.stability_duration || '10' }} \
STAGING_API_URL="${{ env.STAGING_API_URL }}" \
MAX_5XX_RATE="0.001" \
bash scripts/staging-stability-check.sh
- name: Stability summary
if: always()
run: |
echo "## Stability Validation" >> $GITHUB_STEP_SUMMARY
echo "Duration: ${{ inputs.stability_duration || '10' }} minutes" >> $GITHUB_STEP_SUMMARY
echo "Target: 5xx rate < 0.1%" >> $GITHUB_STEP_SUMMARY
if [ -f stability-report.json ]; then
cat stability-report.json | jq . >> $GITHUB_STEP_SUMMARY
fi
# ─────────────────────────────────────────────────────
# TASK-STAG-005: GDPR validation (export + deletion E2E)
# ─────────────────────────────────────────────────────
gdpr-validation:
name: GDPR Compliance Check
runs-on: ubuntu-latest
needs: deploy-staging
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped')
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: "1.24"
cache: true
- name: Run GDPR integration tests
working-directory: veza-backend-api
run: go test -v -tags=integration -run TestGDPR -timeout 120s ./tests/integration/...
- name: GDPR summary
if: always()
run: |
echo "## GDPR Validation" >> $GITHUB_STEP_SUMMARY
echo "- Data export: tested" >> $GITHUB_STEP_SUMMARY
echo "- Account deletion: tested" >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# TASK-STAG-006: Bundle size validation (< 200KB gzip)
# ─────────────────────────────────────────────────────
bundle-size-validation:
name: Bundle Size Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build frontend
working-directory: apps/web
run: npx vite build --outDir dist_verification
env:
NODE_ENV: production
- name: Check bundle size
working-directory: apps/web
run: node scripts/check-bundle-size.mjs
- name: Bundle size summary
if: always()
run: |
echo "## Bundle Size Validation" >> $GITHUB_STEP_SUMMARY
echo "Target: JS initial < 200KB gzipped" >> $GITHUB_STEP_SUMMARY
# ─────────────────────────────────────────────────────
# Final summary
# ─────────────────────────────────────────────────────
validation-summary:
name: Validation Summary
runs-on: ubuntu-latest
needs: [deploy-staging, performance-validation, lighthouse-validation, stability-validation, gdpr-validation, bundle-size-validation]
if: always()
steps:
- name: Generate final report
run: |
echo "# Staging Validation Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Deploy (STAG-001) | ${{ needs.deploy-staging.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Performance (STAG-002) | ${{ needs.performance-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Lighthouse (STAG-003) | ${{ needs.lighthouse-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Stability (STAG-004) | ${{ needs.stability-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| GDPR (STAG-005) | ${{ needs.gdpr-validation.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Bundle Size (STAG-006) | ${{ needs.bundle-size-validation.result }} |" >> $GITHUB_STEP_SUMMARY
- name: Check all passed
run: |
FAILED=0
for result in "${{ needs.performance-validation.result }}" "${{ needs.lighthouse-validation.result }}" "${{ needs.bundle-size-validation.result }}"; do
if [ "$result" = "failure" ]; then
FAILED=1
fi
done
if [ "$FAILED" = "1" ]; then
echo "Some validations failed — see summary above."
exit 1
fi
echo "All critical validations passed!"
- name: Check all passed
run: |
FAILED=0
for result in "${{ needs.performance-validation.result }}" "${{ needs.lighthouse-validation.result }}" "${{ needs.bundle-size-validation.result }}"; do
if [ "$result" = "failure" ]; then
FAILED=1
fi
done
if [ "$FAILED" = "1" ]; then
echo "Some validations failed — see summary above."
exit 1
fi
echo "All critical validations passed!"

View file

@ -4,44 +4,48 @@
name: Storybook Audit
on:
push:
paths:
- "apps/web/**"
- ".github/workflows/storybook-audit.yml"
pull_request:
paths:
- "apps/web/**"
- ".github/workflows/storybook-audit.yml"
workflow_dispatch:
push:
paths:
- "apps/web/**"
- ".github/workflows/storybook-audit.yml"
pull_request:
paths:
- "apps/web/**"
- ".github/workflows/storybook-audit.yml"
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
audit:
name: Build & audit Storybook
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
audit:
name: Build & audit Storybook
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: apps/web/package-lock.json
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: apps/web/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install dependencies
run: npm ci
- name: Install Playwright Chromium
run: npx playwright install chromium --with-deps
- name: Install Playwright Chromium
run: npx playwright install chromium --with-deps
- name: Validate Storybook (build, serve 6007, audit)
run: npm run validate:storybook
env:
VITE_API_URL: /api/v1
VITE_USE_MSW: "true"
VITE_STORYBOOK: "true"
- name: Validate Storybook (build, serve 6007, audit)
run: npm run validate:storybook
env:
VITE_API_URL: /api/v1
VITE_USE_MSW: "true"
VITE_STORYBOOK: "true"

View file

@ -1,41 +1,44 @@
name: Stream Server CI
on:
push:
paths:
- "veza-stream-server/**"
- "veza-common/**"
- ".github/workflows/stream-ci.yml"
pull_request:
paths:
- "veza-stream-server/**"
- "veza-common/**"
- ".github/workflows/stream-ci.yml"
push:
paths:
- "veza-stream-server/**"
- "veza-common/**"
- ".github/workflows/stream-ci.yml"
pull_request:
paths:
- "veza-stream-server/**"
- "veza-common/**"
- ".github/workflows/stream-ci.yml"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
test:
runs-on: ubuntu-latest
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: veza-stream-server
defaults:
run:
working-directory: veza-stream-server
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: clippy
- name: Set up Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: clippy
- name: Lint with clippy
run: cargo clippy --all-targets -- -D warnings
- name: Lint with clippy
run: cargo clippy --all-targets -- -D warnings
- name: Audit dependencies
uses: actions-rust-lang/audit@v1 # TODO: pin to SHA — no known mapping provided
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Run tests
run: cargo test --all
- name: Audit dependencies
uses: actions-rust-lang/audit@v1 # TODO: pin to SHA — no known mapping provided
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Run tests
run: cargo test --all

45
.github/workflows/trivy-fs.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: Trivy Filesystem Scan
on:
pull_request:
branches: [main]
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
trivy-scan:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
severity: "CRITICAL,HIGH"
exit-code: "1"
format: "table"
- name: Run Trivy (SARIF output)
if: always()
uses: aquasecurity/trivy-action@master
with:
scan-type: "fs"
scan-ref: "."
severity: "CRITICAL,HIGH"
exit-code: "0"
format: "sarif"
output: "trivy-results.sarif"
- name: Upload Trivy SARIF
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: trivy-results
path: trivy-results.sarif
retention-days: 30

54
.github/workflows/visual-regression.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Visual Regression
on:
pull_request:
branches: [main]
paths:
- "apps/web/**"
- ".github/workflows/visual-regression.yml"
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
visual-regression:
name: Lost Pixel visual regression
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
working-directory: apps/web
- name: Build Storybook
run: npm run build-storybook
working-directory: apps/web
env:
VITE_API_URL: /api/v1
VITE_USE_MSW: "true"
VITE_STORYBOOK: "true"
- name: Run Lost Pixel
id: lostpixel
run: npx lost-pixel
working-directory: apps/web
- name: Upload diff artifacts
if: failure() && steps.lostpixel.outcome == 'failure'
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: lost-pixel-diff
path: |
apps/web/.lostpixel/current/
apps/web/.lostpixel/difference/
retention-days: 7

34
.github/workflows/zap-dast.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: OWASP ZAP DAST
on:
schedule:
- cron: "0 3 * * *" # Nightly at 3am UTC
workflow_dispatch:
env:
GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs:
zap-baseline:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: ${{ secrets.STAGING_URL || 'http://localhost:5174' }}
rules_file_name: .zap/rules.tsv
fail_action: false
artifact_name: zap-report
- name: Upload ZAP report
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: zap-report
path: report_html.html
retention-days: 30