name: CI on: push: pull_request: types: [opened, reopened, synchronize, edited] workflow_dispatch: permissions: contents: read concurrency: group: ci-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} jobs: policy: name: policy runs-on: ubuntu-latest container: node:24.21.0 outputs: slug: ${{ steps.policy.outputs.slug }} tag: ${{ steps.policy.outputs.tag }} steps: - uses: https://gitea.com/actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Policy regression tests run: node scripts/ci-policy.test.mjs && node scripts/ci-image.test.mjs - name: Validate event and commit id: policy run: node scripts/ci-policy.mjs validate: name: validate runs-on: ubuntu-latest container: node:24.21.0 steps: - uses: https://gitea.com/actions/checkout@v4 with: persist-credentials: false - run: npm install --global bun@1.4.2 - run: bun install --frozen-lockfile - run: bun run lint - run: bun run format:check - run: bunx prettier --check 'scripts/ci-*.mjs' '.gitea/workflows/*.yml' - run: bunx tsc --noEmit -p tsconfig.app.json - run: bunx tsc --noEmit -p tsconfig.spec.json test: name: test runs-on: ubuntu-latest container: node:24.21.0 env: NODE_OPTIONS: --max-old-space-size=2048 steps: - uses: https://gitea.com/actions/checkout@v4 with: persist-credentials: false - run: npm install --global bun@1.4.2 - run: bun install --frozen-lockfile - run: bun scripts/generate-langs.js - name: Functional coverage run: bunx vitest run --coverage --maxWorkers=1 --exclude='src/app/benchmark/**' - name: Coverage report if: ${{ always() }} uses: https://gitea.com/actions/upload-artifact@v3 with: name: coverage path: coverage/ retention-days: 7 if-no-files-found: error benchmark: name: benchmark runs-on: ubuntu-latest container: node:24.21.0 # shortcut: advisory while #78 is open; restore the gate after the protocol passes. continue-on-error: true env: NODE_OPTIONS: --max-old-space-size=2048 steps: - uses: https://gitea.com/actions/checkout@v4 with: persist-credentials: false - run: npm install --global bun@1.4.2 - run: bun install --frozen-lockfile - run: bun scripts/generate-langs.js - name: Unchanged benchmark protocol run: timeout 300s bunx vitest run src/app/benchmark/leave-planner-benchmark.spec.ts --maxWorkers=1 build: name: build runs-on: ubuntu-latest container: node:24.21.0 steps: - uses: https://gitea.com/actions/checkout@v4 with: persist-credentials: false - run: npm install --global bun@1.4.2 - run: bun install --frozen-lockfile - run: bun run build -- --configuration production - uses: https://gitea.com/actions/upload-artifact@v3 with: name: dist path: dist/ retention-days: 7 if-no-files-found: error security: name: security runs-on: ubuntu-latest container: node:24.21.0 steps: - uses: https://gitea.com/actions/checkout@v4 with: persist-credentials: false - name: Install scanners run: | apt-get update apt-get install --yes --no-install-recommends python3-venv python3 -m venv /tmp/semgrep /tmp/semgrep/bin/pip install semgrep==1.180.0 case "$(uname -m)" in x86_64) trivy_arch=64bit ;; aarch64) trivy_arch=ARM64 ;; *) echo 'Unsupported Trivy architecture' >&2; exit 1 ;; esac archive="trivy_0.75.0_Linux-${trivy_arch}.tar.gz" curl --fail --location --silent --show-error "https://github.com/aquasecurity/trivy/releases/download/v0.75.0/${archive}" --output "/tmp/${archive}" curl --fail --location --silent --show-error https://github.com/aquasecurity/trivy/releases/download/v0.75.0/trivy_0.75.0_checksums.txt --output /tmp/trivy-checksums.txt (cd /tmp && sha256sum --check --ignore-missing trivy-checksums.txt) tar -xzf "/tmp/${archive}" -C /usr/local/bin trivy - name: JavaScript and TypeScript SAST run: /tmp/semgrep/bin/semgrep scan --config p/javascript --config p/typescript --metrics off --sarif --output semgrep-results.sarif src scripts - name: Dependency and secret scan if: ${{ !cancelled() }} run: trivy fs --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --format sarif --output trivy-results.sarif --skip-dirs node_modules,dist,coverage . - name: Security reports if: ${{ always() }} uses: https://gitea.com/actions/upload-artifact@v3 with: name: security path: '*-results.sarif' retention-days: 7 if-no-files-found: error images: name: images needs: [policy, validate, test, build, security] if: >- success() && (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) runs-on: ubuntu-latest container: docker:29.8.2 concurrency: group: image-${{ needs.policy.outputs.tag != '' && 'stable' || needs.policy.outputs.slug }} cancel-in-progress: false services: docker: image: docker:29.8.2-dind env: DOCKER_TLS_CERTDIR: '' options: >- --privileged --health-cmd "docker info" --health-interval 5s --health-timeout 5s --health-retries 12 env: DOCKER_HOST: tcp://docker:2375 DOCKER_TLS_CERTDIR: '' defaults: run: shell: sh steps: - name: Install checkout and policy runtimes run: apk add --no-cache nodejs git - uses: https://gitea.com/actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Build branch images or promote a release env: REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: node scripts/ci-image.mjs required: name: required if: ${{ always() }} needs: [policy, validate, test, build, security, images] runs-on: ubuntu-latest container: node:24.21.0 env: CHECK_RESULTS: ${{ needs.policy.result }} ${{ needs.validate.result }} ${{ needs.test.result }} ${{ needs.build.result }} ${{ needs.security.result }} IMAGE_RESULT: ${{ needs.images.result }} IMAGE_EXPECTED: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} steps: - name: Require successful validation and expected publication run: | test "$CHECK_RESULTS" = 'success success success success success' if [ "$IMAGE_EXPECTED" = 'true' ]; then test "$IMAGE_RESULT" = 'success' else test "$IMAGE_RESULT" = 'skipped' fi