diff --git a/.gitea/workflows/browser-canary.yml b/.gitea/workflows/browser-canary.yml new file mode 100644 index 0000000..e8802b4 --- /dev/null +++ b/.gitea/workflows/browser-canary.yml @@ -0,0 +1,44 @@ +name: MobilityOps browser canary + +on: + schedule: + - cron: "37 4 * * *" + workflow_dispatch: + +concurrency: + group: mobilityops-browser-canary + cancel-in-progress: true + +permissions: + contents: read + +jobs: + chromium: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontend/package-lock.json + - name: Install locked Chromium runtime + working-directory: frontend + run: | + npm ci --no-audit --no-fund + npx playwright install --with-deps chromium + - name: Run non-destructive production canary + working-directory: frontend + env: + MOBILITYOPS_PUBLIC_URL: https://fleetops.itworx.tech + run: npx playwright test --config=playwright.live.config.ts --project=chromium + - name: Upload failure evidence + if: failure() + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3; Gitea-compatible artifact protocol + with: + name: browser-canary-failure + path: | + frontend/playwright-live-report + frontend/test-results + if-no-files-found: ignore diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index bb9acef..cbc8f5f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -4,18 +4,22 @@ on: push: branches: [master] pull_request: - schedule: - - cron: "17 3 * * 1" concurrency: group: mobilityops-ci-${{ gitea.repository }}-${{ gitea.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: - backend: + acceptance: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 - name: Secret scan uses: trufflesecurity/trufflehog@b9dd330365132cd2d01dd5dc8a857a056a2544e1 # v3.79.0 with: @@ -23,79 +27,28 @@ jobs: extra_args: --only-verified - name: Backend tests in isolated PostgreSQL stack run: sh scripts/run-isolated-tests.sh - - name: Backend static checks + - name: Backend static and contract checks run: | docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --build --rm api ruff check app tests docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --rm api mypy app - - name: Contract drift gate - run: | docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --rm \ -v "$PWD:/repo:ro" api python /repo/scripts/check-contracts.py python scripts/check-source-budgets.py - - name: Build production API image for vulnerability scan - run: | - docker build --target runtime --build-arg VCS_REF="$GITHUB_SHA" \ - --tag mobilityops-api-ci --file backend/Dockerfile . - - name: Production API image vulnerability scan (HIGH/CRITICAL) - uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0 - with: - scan-type: image - image-ref: mobilityops-api-ci - format: table - severity: HIGH,CRITICAL - exit-code: "1" - ignore-unfixed: true - - name: Build production web image for vulnerability scan - run: | - docker build --build-arg VCS_REF="$GITHUB_SHA" \ - --tag mobilityops-web-ci frontend - - name: Production web image vulnerability scan (HIGH/CRITICAL) - uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0 - with: - scan-type: image - image-ref: mobilityops-web-ci - format: table - severity: HIGH,CRITICAL - exit-code: "1" - ignore-unfixed: true - - name: Remove CI stack - if: always() - run: docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml down -v --remove-orphans - - frontend: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 cache: npm cache-dependency-path: frontend/package-lock.json - - name: Install locked dependencies + - name: Install frontend dependencies once working-directory: frontend run: npm ci --no-audit --no-fund - - name: Lint (tsc + ESLint with react-hooks and jsx-a11y) + - name: Frontend lint, build, budget and dependency audit working-directory: frontend - run: npm run lint - - name: Typecheck and production build - working-directory: frontend - run: npm run build && npm run budget - - name: Dependency audit - working-directory: frontend - run: npm audit --audit-level=high - - e2e: - # The five-minute Playwright demo is part of the definition of done - # (docs/14-testing-and-acceptance.md); run it against the real Compose stack. - runs-on: ubuntu-latest - needs: [backend, frontend] - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: frontend/package-lock.json + run: | + npm run lint + npm run build + npm run budget + npm audit --audit-level=high - name: Start the demo stack run: | cp .env.example .env @@ -106,34 +59,32 @@ jobs: done curl -fsS http://localhost:1228/health/ready docker compose -p mobilityops-e2e exec -T api python -m app.cli seed --reset - - name: Install Playwright + - name: Install acceptance browsers working-directory: frontend + run: npx playwright install --with-deps chromium firefox + - name: Run browser acceptance and live smoke suites + working-directory: frontend + env: + MOBILITYOPS_PUBLIC_URL: http://localhost:1228 run: | - npm ci --no-audit --no-fund - npx playwright install --with-deps chromium firefox - - name: Run browser acceptance suite - working-directory: frontend - env: - MOBILITYOPS_PUBLIC_URL: http://localhost:1228 - run: npx playwright test - - name: Run non-destructive Chromium and Firefox smoke suite - working-directory: frontend - env: - MOBILITYOPS_PUBLIC_URL: http://localhost:1228 - run: npx playwright test --config=playwright.live.config.ts + npx playwright test + npx playwright test --config=playwright.live.config.ts - name: Run concurrent persisted-read smoke run: python scripts/run-readonly-load-smoke.py --base-url http://localhost:1228 - name: Upload Playwright report if: failure() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3; Gitea-compatible artifact protocol with: name: playwright-report path: | frontend/playwright-report frontend/playwright-live-report + if-no-files-found: ignore - name: Stack logs on failure if: failure() run: docker compose -p mobilityops-e2e logs --tail=200 api web - - name: Remove e2e stack + - name: Remove CI stacks if: always() - run: docker compose -p mobilityops-e2e down -v --remove-orphans + run: | + docker compose -p mobilityops-e2e down -v --remove-orphans + docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml down -v --remove-orphans diff --git a/.gitea/workflows/live-canary.yml b/.gitea/workflows/live-canary.yml index 0f30769..fc56c6e 100644 --- a/.gitea/workflows/live-canary.yml +++ b/.gitea/workflows/live-canary.yml @@ -1,35 +1,27 @@ -name: MobilityOps live canary +name: MobilityOps live probe on: schedule: - cron: "7 * * * *" workflow_dispatch: +concurrency: + group: mobilityops-live-probe + cancel-in-progress: true + +permissions: + contents: read + jobs: - public-demo: + public-probe: runs-on: ubuntu-latest + timeout-minutes: 3 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: frontend/package-lock.json - name: Verify HTTPS readiness and certificate horizon run: | curl --fail --silent --show-error --retry 3 https://fleetops.itworx.tech/health/ready openssl s_client -servername fleetops.itworx.tech -connect fleetops.itworx.tech:443 /dev/null \ | openssl x509 -checkend 1209600 -noout - - name: Install locked Playwright runtime - working-directory: frontend - run: | - npm ci --no-audit --no-fund - npx playwright install --with-deps chromium firefox - - name: Run non-destructive cross-browser production canary - working-directory: frontend - env: - MOBILITYOPS_PUBLIC_URL: https://fleetops.itworx.tech - run: npx playwright test --config=playwright.live.config.ts - name: Report successful external heartbeat env: HEARTBEAT_URL: ${{ secrets.LIVE_CANARY_HEARTBEAT_URL }} @@ -37,11 +29,3 @@ jobs: if [ -n "$HEARTBEAT_URL" ]; then curl --fail --silent --show-error --retry 3 "$HEARTBEAT_URL" fi - - name: Upload failure evidence - if: failure() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: live-canary-failure - path: | - frontend/playwright-live-report - frontend/test-results diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 789f5ac..e6e037c 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -23,27 +23,15 @@ jobs: image --scanners vuln --severity HIGH,CRITICAL \ --ignore-unfixed --exit-code 1 "$image" done - - name: Generate API CycloneDX SBOM - uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0 - with: - scan-type: image - image-ref: mobilityops-api-release - format: cyclonedx - output: mobilityops-api-sbom.cdx.json - - name: Generate web CycloneDX SBOM - uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0 - with: - scan-type: image - image-ref: mobilityops-web-release - format: cyclonedx - output: mobilityops-web-sbom.cdx.json - - name: Generate backup-tools CycloneDX SBOM - uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.30.0 - with: - scan-type: image - image-ref: mobilityops-backup-tools-release - format: cyclonedx - output: mobilityops-backup-tools-sbom.cdx.json + - name: Generate CycloneDX SBOMs with the pinned scanner image + run: | + for component in api web backup-tools; do + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD:/work" -w /work \ + aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 \ + image --format cyclonedx --output "mobilityops-${component}-sbom.cdx.json" \ + "mobilityops-${component}-release" + done - name: Record immutable image metadata run: | docker image inspect mobilityops-api-release > mobilityops-api-image.json @@ -52,7 +40,7 @@ jobs: python scripts/generate-release-provenance.py sha256sum mobilityops-*-sbom.cdx.json mobilityops-*-image.json release-provenance.json > SHA256SUMS - name: Upload release evidence - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3; Gitea-compatible artifact protocol with: name: mobilityops-${{ github.ref_name }}-evidence path: | diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml new file mode 100644 index 0000000..0ce39f2 --- /dev/null +++ b/.gitea/workflows/security.yml @@ -0,0 +1,42 @@ +name: MobilityOps security + +on: + schedule: + - cron: "17 3 * * 1" + workflow_dispatch: + +concurrency: + group: mobilityops-security + cancel-in-progress: true + +permissions: + contents: read + +jobs: + images: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + - name: Build production images once + run: | + docker build --target runtime --build-arg VCS_REF="$GITHUB_SHA" \ + --tag mobilityops-api-ci --file backend/Dockerfile . + docker build --build-arg VCS_REF="$GITHUB_SHA" \ + --tag mobilityops-web-ci frontend + - name: Scan production images for fixed HIGH and CRITICAL vulnerabilities + run: | + for image in mobilityops-api-ci mobilityops-web-ci; do + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + docker.io/aquasec/trivy@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e \ + image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed --no-progress "$image" + done + - name: Scan repository secrets and misconfiguration + uses: docker://docker.io/aquasec/trivy@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e + with: + args: fs --scanners misconfig,secret --exit-code 1 --no-progress . + - name: Remove temporary image tags + if: always() + run: docker image rm mobilityops-api-ci mobilityops-web-ci || true