diff --git a/.github/workflows/ci-in-a-box.yml b/.github/workflows/ci-in-a-box.yml new file mode 100644 index 00000000..e899a2b9 --- /dev/null +++ b/.github/workflows/ci-in-a-box.yml @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# +# SPDX-License-Identifier: Apache-2.0 + +# Called Box here to have the name that shows up in github GUI not be the full Ci in a box +name: Box + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "20 20 * * *" + merge_group: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +env: + CI_BOX_COMPOSE_FILE: ci-in-a-box/compose.yaml + DRAUPNIR_CONTEXT: .. + DRAUPNIR_DOCKERFILE: ci-in-a-box/Dockerfile-Draupnir-Develop + DRAUPNIR_CONFIG_SOURCE: ../config + REVERSE_PROXY_CONFIG_PATH: ./scripts/nginx/nginx.conf + REVERSE_PROXY_NEWS_PATH: ../apps/draupnir/test/draupnir_news.json + SYNAPSE_HTTP_ANTISPAM_SOURCE: ../synapse-http-antispam/synapse_http_antispam.py + +jobs: + build: + name: Build & Lint / Node ${{ matrix.node }} / ${{ matrix.variant }} / ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + continue-on-error: ${{ matrix.node == '26' }} + strategy: + fail-fast: false + matrix: + node: ["24", "26"] + variant: [slim, alpine] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + env: + CI_BOX_PROJECT: ci-in-a-box-node${{ matrix.node }}-${{ matrix.variant }}-${{ matrix.arch }} + CI_BOX_VARIANT_FILE: ci-in-a-box/compose.node${{ matrix.node }}-${{ matrix.variant }}.yaml + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: Validate Compose configuration + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' config --quiet + - name: Build and lint + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile build-lint up --build draupnir-build-lint + - name: Collect diagnostics + if: failure() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' ps && docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' logs --no-color + - name: Cleanup + if: always() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' down --volumes --remove-orphans + + unit: + name: Unit tests / Node ${{ matrix.node }} / ${{ matrix.variant }} / ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + continue-on-error: ${{ matrix.node == '26' }} + strategy: + fail-fast: false + matrix: + node: ["24", "26"] + variant: [slim, alpine] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + env: + CI_BOX_PROJECT: ci-in-a-box-node${{ matrix.node }}-${{ matrix.variant }}-${{ matrix.arch }} + CI_BOX_VARIANT_FILE: ci-in-a-box/compose.node${{ matrix.node }}-${{ matrix.variant }}.yaml + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: Validate Compose configuration + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' config --quiet + - name: Run unit tests + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile unit up --build draupnir-unit + - name: Collect diagnostics + if: failure() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' ps && docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' logs --no-color + - name: Cleanup + if: always() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' down --volumes --remove-orphans + + integration: + name: Ci Box Integration tests / Node ${{ matrix.node }} / ${{ matrix.variant }} / ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + continue-on-error: ${{ matrix.node == '26' }} + strategy: + fail-fast: false + matrix: + node: ["24", "26"] + variant: [slim, alpine] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + env: + CI_BOX_PROJECT: ci-in-a-box-node${{ matrix.node }}-${{ matrix.variant }}-${{ matrix.arch }} + CI_BOX_VARIANT_FILE: ci-in-a-box/compose.node${{ matrix.node }}-${{ matrix.variant }}.yaml + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: Check out external antispam module + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: maunium/synapse-http-antispam + path: synapse-http-antispam + ref: main + - name: Validate Compose configuration + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' config --quiet + - name: Start support stack + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" up -d + - name: Run integration tests + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile integration up --build draupnir-integration + - name: Collect diagnostics + if: failure() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' ps && docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' logs --no-color + - name: Cleanup + if: always() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' down --volumes --remove-orphans + + appservice-integration: + name: Application Service Integration tests / Node ${{ matrix.node }} / ${{ matrix.variant }} / ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + continue-on-error: ${{ matrix.node == '26' }} + strategy: + fail-fast: false + matrix: + node: ["24", "26"] + variant: [slim, alpine] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + env: + CI_BOX_PROJECT: ci-in-a-box-node${{ matrix.node }}-${{ matrix.variant }}-${{ matrix.arch }} + CI_BOX_VARIANT_FILE: ci-in-a-box/compose.node${{ matrix.node }}-${{ matrix.variant }}.yaml + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: Check out external antispam module + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: maunium/synapse-http-antispam + path: synapse-http-antispam + # Main is intentional so we know if compatibility breaks with the latest version of the antispam module. + # This is for the same reason why Ci is rerun periodically. + ref: main + - name: Validate Compose configuration + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' config --quiet + - name: Start support stack + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" up -d + - name: Run appservice integration tests + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile appservice-integration up --build draupnir-appservice-integration + - name: Collect diagnostics + if: failure() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' ps && docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' logs --no-color + - name: Cleanup + if: always() + run: docker compose -p "$CI_BOX_PROJECT" -f "$CI_BOX_COMPOSE_FILE" -f "$CI_BOX_VARIANT_FILE" --profile '*' down --volumes --remove-orphans diff --git a/.gitignore b/.gitignore index b5b22357..befa7901 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ config/* # Draupnir test data draupnir-registration.yaml +# Exception for ci-in-a-box appservice config as its static unlike mx-tester one. +!ci-in-a-box/scripts/appservice/draupnir-registration.yaml # Monorepo **/node_modules/ diff --git a/REUSE.toml b/REUSE.toml index 14b31db1..7a988b81 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -69,3 +69,9 @@ path = "package.json" precedence = "aggregate" SPDX-FileCopyrightText = "Gnuxie " SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "scripts/postgres/init.sql" +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Catalan Lover +SPDX-License-Identifier = "0BSD" diff --git a/ci-in-a-box/.env.example b/ci-in-a-box/.env.example new file mode 100644 index 00000000..46624e79 --- /dev/null +++ b/ci-in-a-box/.env.example @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# SPDX-License-Identifier: Apache-2.0 +# +# Ci in a Box 3 Compose settings +POSTGRES_IMAGE=postgres:18-alpine +POSTGRES_DB=postgres +SYNAPSE_IMAGE=ghcr.io/element-hq/synapse:latest +SYNAPSE_SERVER_NAME=localhost:9999 +NGINX_IMAGE=nginx:stable-alpine + +# Paths relative to the main Draupnir checkout. +DRAUPNIR_CONTEXT=.. +DRAUPNIR_DOCKERFILE=ci-in-a-box/Dockerfile-Draupnir-Develop +DRAUPNIR_CONFIG_SOURCE=../config +NGINX_PORT=8081 +REVERSE_PROXY_CONFIG_PATH=./scripts/nginx/nginx.conf +REVERSE_PROXY_NEWS_PATH=../apps/draupnir/test/draupnir_news.json +SYNAPSE_HTTP_ANTISPAM_SOURCE=../synapse-http-antispam/synapse_http_antispam.py +SYNAPSE_ANTISPAM_BASE_URL=http://draupnir-integration:8082/api/1/spam_check diff --git a/ci-in-a-box/Dockerfile-Draupnir-Develop b/ci-in-a-box/Dockerfile-Draupnir-Develop new file mode 100644 index 00000000..ee7c9b1a --- /dev/null +++ b/ci-in-a-box/Dockerfile-Draupnir-Develop @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# Copyright 2024 Gnuxie +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# SPDX-License-Identifier: Apache-2.0 + +# syntax=docker/dockerfile:1.7 + +ARG NODE_VERSION=24 +ARG NODE_VARIANT=slim + +FROM node:${NODE_VERSION}-${NODE_VARIANT} AS build-stage +RUN if [ -f /etc/alpine-release ]; then \ + apk add --no-cache git python3 make g++; \ + else \ + apt-get update \ + && apt-get install -y --no-install-recommends git python3 make g++ \ + && rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /tmp/src +COPY package.json package-lock.json ./ +COPY packages/interface-manager/package.json ./packages/interface-manager/package.json +COPY packages/matrix-basic-types/package.json ./packages/matrix-basic-types/package.json +COPY packages/matrix-protection-suite/package.json ./packages/matrix-protection-suite/package.json +COPY packages/matrix-protection-suite-for-matrix-bot-sdk/package.json ./packages/matrix-protection-suite-for-matrix-bot-sdk/package.json +COPY packages/mps-interface-adaptor/package.json ./packages/mps-interface-adaptor/package.json +COPY packages/tsconfig/package.json ./packages/tsconfig/package.json +COPY apps/draupnir/package.json ./apps/draupnir/package.json + +# Install dependencies first so source edits don't invalidate this layer. +RUN --mount=type=cache,target=/root/.npm npm ci + +COPY . . + +# Build the complete development workspace. The CI service needs build tools, +# source, development dependencies, and generated output in the final image. +RUN npm run build + +FROM node:${NODE_VERSION}-${NODE_VARIANT} AS final-stage +RUN if [ -f /etc/alpine-release ]; then \ + apk add --no-cache git; \ + else \ + apt-get update \ + && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /workspace +COPY --from=build-stage /tmp/src /workspace + +ENV CI=1 +ENV NODE_ENV=development +ENV NODE_CONFIG_DIR=/workspace/config + +CMD ["npm", "test"] diff --git a/ci-in-a-box/README.md b/ci-in-a-box/README.md new file mode 100644 index 00000000..782021a0 --- /dev/null +++ b/ci-in-a-box/README.md @@ -0,0 +1,131 @@ + + +# Draupnir CI in a Box + +This directory contains the so called CI in a Box. Aka Ci-Box. This provides the ability to run the same validation that the CI will run but locally +and do so without needing to worry about if your in a linux or windows based environment as long as you have the ability to run docker. + +Ci in a box provides the following capabilities: + +- build and lint checks +- unit tests +- bot integration tests +- appservice integration tests + +The local commands intentionally mirror the CI workflow so the same setup is easier to troubleshoot and less prone to the black-box problems we had with the previous CI. + +## Prerequisites + +- Docker with Compose +- A local Draupnir checkout +- A local checkout of [`maunium/synapse-http-antispam`](https://github.com/maunium/synapse-http-antispam) for integration runs. (Synapse can potentially be a bit cranky even if AS doesnt need this. Thats why its a requirment for both.) + +The local layout is expected to be: + +```text +parent/ +├── Draupnir/ +├── synapse-http-antispam/ +└── ... +``` + +`ci-in-a-box/` sits inside the Draupnir repo and should be invoked from there. + +## Quick start + +1. Copy the sample environment file if you want to use non-default settings: + +```bash +cp .env.example .env +``` + +2. Review the contents of `.env` to see if your local checkouts are aligned correctly or if you need to use non defaults. + +3. Run the validation command you want to exercise, for example: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile build-lint up --build draupnir-build-lint +``` + +If you need to inspect the resolved Compose configuration while debugging, you can still run: + +```bash +docker compose config +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile '*' config --quiet +``` + +## Variant selection + +These files represent the supported runtime variants: + +```text +compose.node24-slim.yaml +compose.node24-alpine.yaml +compose.node26-slim.yaml +compose.node26-alpine.yaml +``` + +Use the base Compose file together with one of the variant files when running a check: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile build-lint up --build draupnir-build-lint +``` + +## Common contributor commands + +Build and lint: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile build-lint up --build draupnir-build-lint +``` + +Run unit tests: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile unit up --build draupnir-unit +``` + +Run bot mode integration tests: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml up -d +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile integration up --build draupnir-integration +``` + +Run appservice mode integration tests: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml up -d +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile appservice-integration up --build draupnir-appservice-integration +``` + +Clean up after a run: + +```bash +docker compose -f compose.yaml -f compose.node24-slim.yaml --profile '*' down --volumes +``` + +## Debian and Alpine checks + +Use the variant file to switch between the Debian and Alpine validation paths: + +```bash +docker compose -f compose.yaml -f compose.node24-alpine.yaml --profile unit up --build draupnir-unit +``` + +This keeps the glibc and musl validation paths in the same matrix, so you can compare the baseline Debian run against the Alpine target and diagnose distro-specific regressions. This layout avoids environment variables where possible, which makes it easier to work consistently across Windows and Linux hosts. + +## CI parity + +When changing this workflow, keep the local commands aligned with the GitHub Actions matrix. In practice, that means: + +1. Use the repo-local `ci-in-a-box/` files. +2. Check out the latest `maunium/synapse-http-antispam` revision for the integration job so compatibility regressions are caught early. +3. Run the same Compose commands locally that the workflow runs in CI. +4. Keep the cleanup step consistent with `down --volumes` after failures or successful runs. + +This keeps the contributor workflow reproducible and makes CI failures easier to diagnose from the same commands. diff --git a/ci-in-a-box/compose.node24-alpine.yaml b/ci-in-a-box/compose.node24-alpine.yaml new file mode 100644 index 00000000..4c0f2343 --- /dev/null +++ b/ci-in-a-box/compose.node24-alpine.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# SPDX-License-Identifier: Apache-2.0 +services: + draupnir-build-lint: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node24-alpine + + draupnir-unit: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node24-alpine + + draupnir-integration: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node24-alpine + + draupnir-appservice-integration: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node24-alpine + + draupnir-config-init: + image: draupnir/ci-box-draupnir:node24-alpine diff --git a/ci-in-a-box/compose.node24-slim.yaml b/ci-in-a-box/compose.node24-slim.yaml new file mode 100644 index 00000000..4d65bc89 --- /dev/null +++ b/ci-in-a-box/compose.node24-slim.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# SPDX-License-Identifier: Apache-2.0 +services: + draupnir-build-lint: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node24-slim + + draupnir-unit: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node24-slim + + draupnir-integration: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node24-slim + + draupnir-appservice-integration: + build: + args: + NODE_VERSION: "24" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node24-slim + + draupnir-config-init: + image: draupnir/ci-box-draupnir:node24-slim diff --git a/ci-in-a-box/compose.node26-alpine.yaml b/ci-in-a-box/compose.node26-alpine.yaml new file mode 100644 index 00000000..8850a12a --- /dev/null +++ b/ci-in-a-box/compose.node26-alpine.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# SPDX-License-Identifier: Apache-2.0 +services: + draupnir-build-lint: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node26-alpine + + draupnir-unit: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node26-alpine + + draupnir-integration: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node26-alpine + + draupnir-appservice-integration: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: alpine + image: draupnir/ci-box-draupnir:node26-alpine + + draupnir-config-init: + image: draupnir/ci-box-draupnir:node26-alpine diff --git a/ci-in-a-box/compose.node26-slim.yaml b/ci-in-a-box/compose.node26-slim.yaml new file mode 100644 index 00000000..944eeb16 --- /dev/null +++ b/ci-in-a-box/compose.node26-slim.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# SPDX-License-Identifier: Apache-2.0 +services: + draupnir-build-lint: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node26-slim + + draupnir-unit: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node26-slim + + draupnir-integration: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node26-slim + + draupnir-appservice-integration: + build: + args: + NODE_VERSION: "26" + NODE_VARIANT: slim + image: draupnir/ci-box-draupnir:node26-slim + + draupnir-config-init: + image: draupnir/ci-box-draupnir:node26-slim diff --git a/ci-in-a-box/compose.yaml b/ci-in-a-box/compose.yaml new file mode 100644 index 00000000..ab34353a --- /dev/null +++ b/ci-in-a-box/compose.yaml @@ -0,0 +1,181 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# SPDX-License-Identifier: Apache-2.0 +name: ci-in-a-box + +services: + postgres: + image: ${POSTGRES_IMAGE:-postgres:18-alpine} + environment: + POSTGRES_USER: ${POSTGRES_USER:-mjolnir-tester} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mjolnir-test} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 2s + timeout: 5s + retries: 15 + volumes: + - postgres-data:/var/lib/postgresql + - ./scripts/postgres/init.sql:/docker-entrypoint-initdb.d/10-synapse.sql:ro + + synapse: + image: ${SYNAPSE_IMAGE:-ghcr.io/element-hq/synapse:latest} + depends_on: + postgres: + condition: service_healthy + synapse-init: + condition: service_completed_successfully + environment: + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + PYTHONPATH: /data + volumes: + - synapse-data:/data + - ${SYNAPSE_HTTP_ANTISPAM_SOURCE:-../synapse-http-antispam/synapse_http_antispam.py}:/data/synapse_http_antispam.py:ro + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8008/_matrix/client/versions >/dev/null"] + interval: 5s + timeout: 5s + retries: 30 + + synapse-init: + image: ${SYNAPSE_IMAGE:-ghcr.io/element-hq/synapse:latest} + entrypoint: ["/bin/sh"] + depends_on: + postgres: + condition: service_healthy + environment: + SYNAPSE_SERVER_NAME: ${SYNAPSE_SERVER_NAME:-localhost:9999} + SYNAPSE_PUBLIC_BASEURL: ${SYNAPSE_PUBLIC_BASEURL:-http://localhost:8081} + SYNAPSE_POSTGRES_DB: ${SYNAPSE_POSTGRES_DB:-synapse-test-db} + SYNAPSE_ANTISPAM_BASE_URL: ${SYNAPSE_ANTISPAM_BASE_URL:-http://draupnir-integration:8082/api/1/spam_check} + PYTHONPATH: /data + SYNAPSE_REPORT_STATS: "no" + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + volumes: + - synapse-data:/data + - ./scripts/synapse/homeserver.overrides.yaml:/etc/ci-box/homeserver.overrides.yaml:ro + - ./scripts/appservice/draupnir-registration.yaml:/etc/ci-box/draupnir-registration.yaml:ro + - ${SYNAPSE_HTTP_ANTISPAM_SOURCE:-../synapse-http-antispam/synapse_http_antispam.py}:/data/synapse_http_antispam.py:ro + command: + - -c + - | + if [ ! -f /data/homeserver.yaml ]; then + /start.py generate; + fi; + sed \ + -e "s|__CI_BOX_SYNAPSE_SERVER_NAME__|$${SYNAPSE_SERVER_NAME}|g" \ + -e "s|__CI_BOX_SYNAPSE_PUBLIC_BASEURL__|$${SYNAPSE_PUBLIC_BASEURL}|g" \ + -e "s|__CI_BOX_SYNAPSE_ANTISPAM_BASE_URL__|$${SYNAPSE_ANTISPAM_BASE_URL}|g" \ + -e "s|__CI_BOX_SYNAPSE_DB__|$${SYNAPSE_POSTGRES_DB}|g" \ + /etc/ci-box/homeserver.overrides.yaml > /data/homeserver.yaml; + cp /etc/ci-box/draupnir-registration.yaml /data/draupnir-registration.yaml + restart: "no" + + draupnir-build-lint: + build: + context: ${DRAUPNIR_CONTEXT:-../Draupnir} + dockerfile: ${DRAUPNIR_DOCKERFILE:-../Draupnir-Ci-in-a-box/Dockerfile-Draupnir-Develop} + args: + NODE_VERSION: ${NODE_VERSION:-24} + NODE_VARIANT: ${NODE_VARIANT:-slim} + image: ${DRAUPNIR_IMAGE:-draupnir/ci-box-draupnir:node${NODE_VERSION:-24}-${NODE_VARIANT:-slim}} + environment: + CI: "1" + NODE_ENV: development + working_dir: /workspace + command: ["sh", "-lc", "npm run build:all && npm run lint"] + profiles: [build-lint] + + draupnir-unit: + build: + context: ${DRAUPNIR_CONTEXT:-../Draupnir} + dockerfile: ${DRAUPNIR_DOCKERFILE:-../Draupnir-Ci-in-a-box/Dockerfile-Draupnir-Develop} + args: + NODE_VERSION: ${NODE_VERSION:-24} + NODE_VARIANT: ${NODE_VARIANT:-slim} + image: ${DRAUPNIR_IMAGE:-draupnir/ci-box-draupnir:node${NODE_VERSION:-24}-${NODE_VARIANT:-slim}} + environment: + CI: "1" + NODE_ENV: development + working_dir: /workspace + command: ["sh", "-lc", "npm run build:all && npm test"] + profiles: [unit] + + draupnir-integration: + build: + context: ${DRAUPNIR_CONTEXT:-../Draupnir} + dockerfile: ${DRAUPNIR_DOCKERFILE:-../Draupnir-Ci-in-a-box/Dockerfile-Draupnir-Develop} + args: + NODE_VERSION: ${NODE_VERSION:-24} + NODE_VARIANT: ${NODE_VARIANT:-slim} + image: ${DRAUPNIR_IMAGE:-draupnir/ci-box-draupnir:node${NODE_VERSION:-24}-${NODE_VARIANT:-slim}} + depends_on: + synapse: + condition: service_healthy + draupnir-config-init: + condition: service_completed_successfully + environment: + CI: "1" + NODE_ENV: harness + SYNAPSE_BASE_URL: http://synapse:8008 + working_dir: /workspace + volumes: + - draupnir-config:/workspace/config + command: ["npm", "run", "-w", "apps/draupnir", "test:integration"] + profiles: [integration] + + draupnir-appservice-integration: + build: + context: ${DRAUPNIR_CONTEXT:-../Draupnir} + dockerfile: ${DRAUPNIR_DOCKERFILE:-../Draupnir-Ci-in-a-box/Dockerfile-Draupnir-Develop} + args: + NODE_VERSION: ${NODE_VERSION:-24} + NODE_VARIANT: ${NODE_VARIANT:-slim} + image: ${DRAUPNIR_IMAGE:-draupnir/ci-box-draupnir:node${NODE_VERSION:-24}-${NODE_VARIANT:-slim}} + depends_on: + synapse: + condition: service_healthy + draupnir-config-init: + condition: service_completed_successfully + environment: + CI: "1" + NODE_ENV: harness + SYNAPSE_BASE_URL: http://synapse:8008 + working_dir: /workspace + volumes: + - draupnir-config:/workspace/config + - ./scripts/appservice/draupnir-registration.yaml:/workspace/apps/draupnir/draupnir-registration.yaml:ro + - ${DRAUPNIR_CONTEXT:-../Draupnir}/apps/draupnir/src/appservice/config/config.harness.yaml:/workspace/apps/draupnir/src/appservice/config/config.harness.yaml:ro + command: ["npm", "run", "-w", "apps/draupnir", "test:appservice:integration"] + profiles: [appservice-integration] + + draupnir-config-init: + image: ${DRAUPNIR_IMAGE:-draupnir/ci-box-draupnir:node${NODE_VERSION:-24}-${NODE_VARIANT:-slim}} + volumes: + - draupnir-config:/config + - ${DRAUPNIR_CONFIG_SOURCE:-../Draupnir/config}:/source-config:ro + command: + - sh + - -lc + - >- + cp -a /source-config/. /config/; + sed -i + -e "s|http://localhost:8081|http://nginx:8081|g" + -e "s|127.0.0.1:8081|nginx:8081|g" + /config/harness.yaml /config/default.yaml + restart: "no" + profiles: [integration, appservice-integration, development] + + nginx: + image: ${NGINX_IMAGE:-nginx:stable-alpine} + depends_on: + synapse: + condition: service_healthy + volumes: + - ${REVERSE_PROXY_CONFIG_PATH:-./scripts/nginx/nginx.conf}:/etc/nginx/nginx.conf:ro + - ${REVERSE_PROXY_NEWS_PATH:-../Draupnir/apps/draupnir/test/draupnir_news.json}:/var/www/test/draupnir_news.json:ro + +volumes: + postgres-data: + synapse-data: + draupnir-config: diff --git a/ci-in-a-box/scripts/appservice/draupnir-registration.yaml b/ci-in-a-box/scripts/appservice/draupnir-registration.yaml new file mode 100644 index 00000000..594d52c6 --- /dev/null +++ b/ci-in-a-box/scripts/appservice/draupnir-registration.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# +# SPDX-License-Identifier: Apache-2.0 + id: "draupnir-for-all" + as_token: "as_token" + hs_token: "hs_token" + url: "http://draupnir-appservice-integration:9000" + sender_localpart: draupnir-moderation + namespaces: + users: + - exclusive: true + regex: '@mjolnir_.*' + - exclusive: true + regex: '@draupnir_.*' + rate_limited: false diff --git a/ci-in-a-box/scripts/nginx/nginx.conf b/ci-in-a-box/scripts/nginx/nginx.conf new file mode 100644 index 00000000..dfd48f41 --- /dev/null +++ b/ci-in-a-box/scripts/nginx/nginx.conf @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2024 Gnuxie +# +# SPDX-License-Identifier: CC0-1.0 + +events { +} + +http { + resolver 127.0.0.11 ipv6=off valid=5s; + + server { + listen 8081; + + location ~ ^/_matrix/client/(r0|v3)/rooms/([^/]*)/report/(.*)$ { + mirror /report_mirror; + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,Keep-Alive,X-Requested-With,If-Modified-Since' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + add_header 'Access-Control-Max-Age' 1728000; + + set $room_id $2; + set $event_id $3; + set $draupnir_integration_upstream draupnir-integration:8082; + proxy_pass http://$draupnir_integration_upstream/api/1/report/$room_id/$event_id; + } + + location / { + proxy_pass http://synapse:8008; + } + + location /report_mirror { + internal; + proxy_pass http://synapse:8008$request_uri; + } + + location /draupnir_news.json { + root /var/www/test; + default_type application/json; + } + } +} diff --git a/ci-in-a-box/scripts/postgres/init.sql b/ci-in-a-box/scripts/postgres/init.sql new file mode 100644 index 00000000..6515ea77 --- /dev/null +++ b/ci-in-a-box/scripts/postgres/init.sql @@ -0,0 +1,12 @@ +CREATE DATABASE "synapse-test-db" + WITH OWNER = "mjolnir-tester" + ENCODING = 'UTF8' + LC_COLLATE = 'C' + LC_CTYPE = 'C' + TEMPLATE = template0; + CREATE DATABASE "mjolnir-test-db" + WITH OWNER = "mjolnir-tester" + ENCODING = 'UTF8' + LC_COLLATE = 'C' + LC_CTYPE = 'C' + TEMPLATE = template0; diff --git a/ci-in-a-box/scripts/synapse/homeserver.overrides.yaml b/ci-in-a-box/scripts/synapse/homeserver.overrides.yaml new file mode 100644 index 00000000..96b2db7d --- /dev/null +++ b/ci-in-a-box/scripts/synapse/homeserver.overrides.yaml @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2026 Catalan Lover +# +# SPDX-License-Identifier: Apache-2.0 +server_name: "__CI_BOX_SYNAPSE_SERVER_NAME__" +pid_file: /data/homeserver.pid +listeners: + - port: 8008 + resources: + - compress: false + names: + - client + - federation + tls: false + type: http + x_forwarded: true +database: + name: psycopg2 + args: + user: "mjolnir-tester" + password: "mjolnir-test" + database: "__CI_BOX_SYNAPSE_DB__" + host: "postgres" + port: 5432 +log_config: "/data/__CI_BOX_SYNAPSE_SERVER_NAME__.log.config" +media_store_path: /data/media_store +registration_shared_secret: "REGISTRATION_SHARED_SECRET" +report_stats: false +macaroon_secret_key: "Cee:o8O=k7hOp4C3;eyoTH~&SL;tVa.j8zBXF1zbWGd.fcRc-m" +form_secret: "_&Vo8k;b:bG9fTMGYSV0JFKUFKarjvhRif~:.dN^eiiI_pn:Y#" +signing_key_path: "/data/__CI_BOX_SYNAPSE_SERVER_NAME__.signing.key" +trusted_key_servers: + - server_name: "matrix.org" + +public_baseurl: "__CI_BOX_SYNAPSE_PUBLIC_BASEURL__" +enable_registration: true +enable_registration_without_verification: true + +app_service_config_files: + - "/data/draupnir-registration.yaml" + +modules: + - module: synapse_http_antispam.HTTPAntispam + config: + base_url: __CI_BOX_SYNAPSE_ANTISPAM_BASE_URL__ + authorization: DEFAULT + do_ping: false + enabled_callbacks: + - user_may_invite + - user_may_join_room + - check_event_for_spam + fail_open: + user_may_invite: true + user_may_join_room: true + check_event_for_spam: true + +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_message: + per_second: 10000 + burst_count: 10000 + +rc_invites: + per_room: + per_second: 10000 + burst_count: 10000 + per_user: + per_second: 10000 + burst_count: 10000 + per_sender: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +rc_admin_redaction: + per_second: 10000 + burst_count: 10000 + +rc_joins: + local: + per_second: 10000 + burst_count: 10000 + remote: + per_second: 10000 + burst_count: 10000