name: CI on: push: branches: [main, master] paths-ignore: - 'docs/**' - 'README.md' - 'README_EN.md' - 'CONTRIBUTING.md' - 'SECURITY.md' - 'CODE_OF_CONDUCT.md' pull_request: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: NODE_VERSION: 25 # Non-desktop CI jobs do not need the Electron runtime binary. ELECTRON_SKIP_BINARY_DOWNLOAD: '1' jobs: changes: name: Detect Docs Changes if: github.event_name == 'pull_request' runs-on: ubuntu-latest outputs: docs: ${{ steps.filter.outputs.docs }} steps: - name: Checkout uses: actions/checkout@v6 - name: Detect docs-related changes id: filter uses: dorny/paths-filter@v4 with: filters: | docs: - 'docs/**' - 'README.md' - 'README_EN.md' - 'CONTRIBUTING.md' - 'SECURITY.md' - 'CODE_OF_CONDUCT.md' - '.github/workflows/docs-pages.yml' test-core: name: Test Core runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Run tests env: DB_PARITY_SKIP_LIVE_SCHEMA: 'true' run: npm test build-web: name: Build Web runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Build web run: npm run build:web build-server: name: Build Server runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Build server run: npm run build:server build-desktop: name: Build Desktop runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies env: ELECTRON_SKIP_BINARY_DOWNLOAD: '' run: npm ci --prefer-offline --no-audit --no-fund - name: Build desktop run: npm run build:desktop typecheck: name: Typecheck runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Run typecheck run: npm run typecheck repo-drift: name: Repo Drift Check runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Run repo drift check run: npm run repo:drift-check schema-sqlite: name: Schema Check (SQLite) runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Run sqlite schema parity run: npm run test:schema:parity - name: Run sqlite schema upgrade run: npm run test:schema:upgrade schema-mysql: name: Schema Check (MySQL) runs-on: ubuntu-latest timeout-minutes: 20 services: mysql: image: mysql:8.4 env: MYSQL_DATABASE: metapi MYSQL_ROOT_PASSWORD: root ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping -h 127.0.0.1 -proot" --health-interval=10s --health-timeout=5s --health-retries=10 env: DB_PARITY_MYSQL_URL: mysql://root:root@127.0.0.1:3306/metapi DB_PARITY_SQLITE: 'false' steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Run mysql schema parity run: npm run test:schema:parity - name: Run mysql schema upgrade run: npm run test:schema:upgrade - name: Run mysql runtime schema bootstrap run: npm run test:schema:runtime schema-postgres: name: Schema Check (Postgres) runs-on: ubuntu-latest timeout-minutes: 20 services: postgres: image: postgres:16 env: POSTGRES_DB: metapi POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres ports: - 5432:5432 options: >- --health-cmd="pg_isready -U postgres -d metapi" --health-interval=10s --health-timeout=5s --health-retries=10 env: DB_PARITY_POSTGRES_URL: postgres://postgres:postgres@127.0.0.1:5432/metapi DB_PARITY_SQLITE: 'false' steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Run postgres schema parity run: npm run test:schema:parity - name: Run postgres schema upgrade run: npm run test:schema:upgrade - name: Run postgres runtime schema bootstrap run: npm run test:schema:runtime docs-build: name: Build Docs if: github.event_name == 'pull_request' && needs.changes.outputs.docs == 'true' needs: changes runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Build docs run: npm run docs:build audit: name: Audit Production Dependencies runs-on: ubuntu-latest timeout-minutes: 10 # Keep PR audit informational so build/test/schema remain the merge gate. steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - name: Audit production dependencies run: | set +e npm audit --omit=dev --audit-level=high status=$? if [ "$status" -ne 0 ] && [ "${{ github.event_name }}" = "pull_request" ]; then echo "::warning::npm audit reported production dependency vulnerabilities; PR audit stays informational." exit 0 fi exit "$status" publish-docker-arch: name: Publish Docker Image (${{ matrix.arch }}) if: github.event_name == 'push' runs-on: ${{ matrix.runner }} timeout-minutes: 45 env: DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi strategy: fail-fast: false matrix: include: - arch: amd64 platform: linux/amd64 runner: ubuntu-latest - arch: arm64 platform: linux/arm64 runner: ubuntu-24.04-arm - arch: armv7 platform: linux/arm/v7 runner: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Ensure Docker Hub secrets are configured run: | if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then echo "Missing Docker Hub secrets: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN" exit 1 fi - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Set up QEMU if: matrix.arch == 'armv7' uses: docker/setup-qemu-action@v4 - name: Log in to Docker Hub uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v6 with: images: ${{ env.DOCKERHUB_IMAGE }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch type=sha,format=short - name: Add architecture suffix to tags id: arch-tags shell: bash run: | set -euo pipefail rm -f tags.txt while IFS= read -r tag; do [ -n "$tag" ] || continue echo "${tag}-${{ matrix.arch }}" >> tags.txt done <<< "${{ steps.meta.outputs.tags }}" { echo 'tags<> "$GITHUB_OUTPUT" - name: Build and push ${{ matrix.platform }} image uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile platforms: ${{ matrix.platform }} push: true tags: ${{ steps.arch-tags.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=ci-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=ci-${{ matrix.arch }} publish-docker: name: Publish Docker Manifest if: github.event_name == 'push' runs-on: ubuntu-latest needs: publish-docker-arch timeout-minutes: 15 env: DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/metapi steps: - name: Checkout uses: actions/checkout@v6 - name: Ensure Docker Hub secrets are configured run: | if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then echo "Missing Docker Hub secrets: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN" exit 1 fi - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to Docker Hub uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v6 with: images: ${{ env.DOCKERHUB_IMAGE }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch type=sha,format=short - name: Create multi-arch manifests shell: bash run: | set -euo pipefail while IFS= read -r tag; do [ -n "$tag" ] || continue docker buildx imagetools create \ --tag "$tag" \ "${tag}-amd64" \ "${tag}-arm64" \ "${tag}-armv7" done <<< "${{ steps.meta.outputs.tags }}"