Skip to content

chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /frontend #1436

chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /frontend

chore(deps): bump dompurify from 3.4.12 to 3.4.13 in /frontend #1436

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
issue_comment:
types: [created]
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.issue.number || github.ref }}
cancel-in-progress: true
# Least-privilege default for every job. The `gate` and `report` jobs override
# with the extra write scopes they need (checks / pull-requests).
permissions:
contents: read
jobs:
gate:
name: Gate (automatic, /ci, or workflow_dispatch)
# `/ci` checks out the PR head and runs its code with access to repo
# secrets, so restrict it to org members/collaborators. Ordinary PR
# CI still runs for everyone via the `pull_request` trigger above.
if: |
github.event_name == 'pull_request' ||
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/ci') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
runs-on: ubuntu-latest
permissions:
pull-requests: write
checks: write
outputs:
sha: ${{ steps.resolve.outputs.sha }}
base_sha: ${{ steps.resolve.outputs.base_sha }}
check_run_id: ${{ steps.check.outputs.id }}
steps:
- name: Resolve target SHA
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
SHA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} -q .head.sha)
BASE_SHA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} -q .base.sha)
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
SHA="${{ github.event.pull_request.head.sha }}"
BASE_SHA="${{ github.event.pull_request.base.sha }}"
elif [[ "${{ github.event_name }}" == "push" ]]; then
SHA="${{ github.sha }}"
BASE_SHA="${{ github.event.before }}"
else
SHA="${{ github.sha }}"
BASE_SHA=""
fi
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
- name: React to /ci comment
if: github.event_name == 'issue_comment'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api -X POST \
repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
-f content=rocket
- name: Create in-progress check run
id: check
if: github.event_name == 'issue_comment'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
ID=$(gh api -X POST repos/${{ github.repository }}/check-runs \
-f name="CI" \
-f head_sha="${{ steps.resolve.outputs.sha }}" \
-f status=in_progress \
-f "details_url=$RUN_URL" \
-q .id)
echo "id=$ID" >> "$GITHUB_OUTPUT"
changes:
name: Determine changed areas
needs: gate
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.gate.outputs.sha }}
persist-credentials: false
fetch-depth: 0
- name: Detect changed areas
id: filter
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ needs.gate.outputs.base_sha }}
TARGET_SHA: ${{ needs.gate.outputs.sha }}
run: |
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "backend=true" >> "$GITHUB_OUTPUT"
echo "frontend=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ]]; then
echo "No usable base commit; running all checks."
echo "backend=true" >> "$GITHUB_OUTPUT"
echo "frontend=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$TARGET_SHA" > changed-files.txt
echo "Changed files:"
cat changed-files.txt
# Shared build/config/workflow changes can affect either side.
if grep -qE '^(backend/|Makefile$|\.github/workflows/ci\.yml$)' changed-files.txt; then
echo "backend=true" >> "$GITHUB_OUTPUT"
else
echo "backend=false" >> "$GITHUB_OUTPUT"
fi
if grep -qE '^(frontend/|Makefile$|\.github/workflows/ci\.yml$)' changed-files.txt; then
echo "frontend=true" >> "$GITHUB_OUTPUT"
else
echo "frontend=false" >> "$GITHUB_OUTPUT"
fi
backend-check:
name: Backend Check (ruff + mypy + pytest unit)
needs: [gate, changes]
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.gate.outputs.sha }}
persist-credentials: false
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
# Ephemeral runner — skip post-run `uv cache prune --ci`
# which occasionally hangs on a concurrent lock and fails the job.
prune-cache: false
- name: Install backend
run: make backend-install
- name: Backend CI checks
env:
# Dummy env vars required because tests/conftest.py forces ENV_FOR_DYNACONF=test
# which loads config.test.yaml, and that file uses @format {env[...]} for secrets.
# Without these, dynaconf raises DynaconfFormatError during test collection.
# Real e2e job (Task 9) overrides these with GitHub Secrets.
CUBEPLEX_E2E_LLM_BASE_URL: http://dummy
CUBEPLEX_E2E_LLM_API_KEY: dummy
CUBEPLEX_E2E_LLM_MODEL_ID: dummy
CUBEPLEX_SANDBOX__DOMAIN: dummy:9000
CUBEPLEX_SANDBOX__IMAGE: dummy:image
CUBEPLEX_SANDBOX__API_KEY: dummy
run: make backend-check-ci
- name: Upload coverage
uses: actions/upload-artifact@v7
if: always()
with:
name: backend-coverage
path: backend/coverage.xml
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v7
with:
files: backend/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
frontend-check:
name: Frontend Check (eslint + prettier + type-check + vitest + build)
needs: [gate, changes]
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.gate.outputs.sha }}
persist-credentials: false
- uses: pnpm/action-setup@v6
with:
package_json_file: frontend/package.json
- uses: actions/setup-node@v7
with:
node-version: "20"
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend
run: make frontend-install
- name: Frontend CI checks
run: make frontend-check-ci
backend-e2e:
name: Backend E2E (pytest, shard ${{ matrix.shard }})
needs: [gate, changes]
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
# The complete suite contains more than 850 tests and took over 30 minutes
# in a single process. Stable node-id sharding keeps full coverage while
# giving every shard enough time to finish and report all failures.
timeout-minutes: 30
env:
ENV_FOR_DYNACONF: test
CUBEPLEX_E2E_SHARD_INDEX: ${{ matrix.shard }}
CUBEPLEX_E2E_SHARD_TOTAL: 4
CUBEPLEX_E2E_LLM_BASE_URL: ${{ secrets.CUBEPLEX_E2E_LLM_BASE_URL || 'http://127.0.0.1:9' }}
CUBEPLEX_E2E_LLM_API_KEY: ${{ secrets.CUBEPLEX_E2E_LLM_API_KEY || 'ci-placeholder' }}
CUBEPLEX_E2E_LLM_MODEL_ID: ${{ secrets.CUBEPLEX_E2E_LLM_MODEL_ID || 'ci-placeholder' }}
CUBEPLEX_SANDBOX__DOMAIN: ${{ secrets.CUBEPLEX_SANDBOX__DOMAIN || '127.0.0.1:9' }}
CUBEPLEX_SANDBOX__IMAGE: ${{ secrets.CUBEPLEX_SANDBOX__IMAGE || 'ci-placeholder' }}
CUBEPLEX_SANDBOX__API_KEY: ${{ secrets.CUBEPLEX_SANDBOX__API_KEY || 'ci-placeholder' }}
# Optional: gates the conversation-search E2E test. The test skips
# cleanly when this secret isn't set in the repo, so PRs from forks
# without the secret stay green; the real-network path runs in main.
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
# Test-only Fernet key for CI startup and vault E2E. Production must set its own key.
CUBEPLEX_AUTH__VAULT_KEY: 1EwVRAtWccVJhdXX_2iepp7OlPfadfHpWNwVFpRzLBI=
services:
postgres:
image: cubeplex/postgresql-pgroonga-pgvector:18.2-pgroonga4.0.6-pgvector0.8.2
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpass
POSTGRES_DB: cubeplex_test
ports: ['5432:5432']
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=10
redis:
image: redis:7-alpine
ports: ['6379:6379']
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.gate.outputs.sha }}
persist-credentials: false
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
# Ephemeral runner — skip post-run `uv cache prune --ci`
# which occasionally hangs on a concurrent lock and fails the job.
prune-cache: false
- name: Install backend
run: make backend-install
- name: Prepare RustFS data dirs
run: |
mkdir -p /tmp/rustfs/data /tmp/rustfs/logs
sudo chown -R 10001:10001 /tmp/rustfs
- name: Start RustFS
run: |
docker run -d --name rustfs \
-p 9000:9000 -p 9001:9001 \
-v /tmp/rustfs/data:/data \
-v /tmp/rustfs/logs:/logs \
rustfs/rustfs:1.0.0-alpha.97
# TCP/HTTP-level readiness; RustFS alpha lacks a unified /health endpoint.
# Accept any 2xx/3xx/4xx HTTP response as ready.
for i in {1..30}; do
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9000/ | grep -qE '^(2|3|4)'; then
echo "RustFS ready"
break
fi
sleep 2
done
- name: Create RustFS test bucket
env:
AWS_ACCESS_KEY_ID: rustfsadmin
AWS_SECRET_ACCESS_KEY: rustfsadmin
run: |
aws --endpoint-url http://127.0.0.1:9000 \
s3 mb s3://cubeplex-test --region us-east-1
- name: Alembic migrate
run: make backend-migrate
- name: Pytest e2e (fast lane — excludes real_llm)
# The 10 `@pytest.mark.real_llm` tests (prompt-cache discipline,
# journey middleware, send_message billing) run in nightly-real-llm.yml
# against the live LLM gateway. Keeping them off the PR path saves
# ~5-10× wall-clock per push and prevents vendor-rate-limit flakes
# from blocking unrelated merges.
#
# -ci variant additionally writes coverage-e2e.xml: e2e tests are the
# primary test type for business-flow code in this repo (see
# docs/testing.md), so without this the Codecov patch-coverage gate
# only ever sees the unit-test run and reports business logic as
# untested even when it's covered here.
run: make backend-test-e2e-ci
- name: Pytest e2e (licensed lane)
# The default environment has no optional package, so tests/e2e/licensed/
# skips itself there — which means the authorisation and validation
# contracts for the relocated cost routes would otherwise be checked
# nowhere. Runs on one shard only: it is a handful of tests and sharding
# them would just install the package four times. Reuses the services this
# job already stood up rather than duplicating postgres/redis/rustfs.
#
# Both sync flags: `--group` alone re-resolves and drops test deps.
if: matrix.shard == 0
run: |
cd backend
uv sync --all-extras --group licensed
uv run python scripts/dev/license_keygen.py dev-env --features multi_org \
>> "$GITHUB_ENV"
- name: Pytest e2e (licensed lane — run)
if: matrix.shard == 0
run: |
cd backend
# `env -u`, not blanking: the shard gate checks `is None`, so an empty
# string gets past it and then fails int("") with a UsageError.
# Both paths, explicitly: a licensed test directory that no command
# names runs nowhere — importorskip hides it from the default lane too.
env -u CUBEPLEX_E2E_SHARD_INDEX -u CUBEPLEX_E2E_SHARD_TOTAL \
uv run pytest tests/e2e/licensed/ tests/unit/licensed/ --no-cov -q
- name: Cleanup leftover test sandboxes
if: always()
run: make backend-cleanup-sandboxes
- name: Upload e2e coverage to Codecov
if: always()
uses: codecov/codecov-action@v7
with:
files: backend/coverage-e2e.xml
flags: backend-e2e-shard-${{ matrix.shard }}
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
frontend-e2e:
name: Frontend E2E (playwright)
needs: [gate, changes]
if: needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
env:
ENV_FOR_DYNACONF: test
CUBEPLEX_E2E_LLM_BASE_URL: ${{ secrets.CUBEPLEX_E2E_LLM_BASE_URL || 'http://127.0.0.1:9' }}
CUBEPLEX_E2E_LLM_API_KEY: ${{ secrets.CUBEPLEX_E2E_LLM_API_KEY || 'ci-placeholder' }}
CUBEPLEX_E2E_LLM_MODEL_ID: ${{ secrets.CUBEPLEX_E2E_LLM_MODEL_ID || 'ci-placeholder' }}
CUBEPLEX_E2E_REAL_LLM: ${{ secrets.CUBEPLEX_E2E_LLM_BASE_URL != '' && secrets.CUBEPLEX_E2E_LLM_API_KEY != '' && secrets.CUBEPLEX_E2E_LLM_MODEL_ID != '' }}
CUBEPLEX_SANDBOX__DOMAIN: ${{ secrets.CUBEPLEX_SANDBOX__DOMAIN || '127.0.0.1:9' }}
CUBEPLEX_SANDBOX__IMAGE: ${{ secrets.CUBEPLEX_SANDBOX__IMAGE || 'ci-placeholder' }}
CUBEPLEX_SANDBOX__API_KEY: ${{ secrets.CUBEPLEX_SANDBOX__API_KEY || 'ci-placeholder' }}
# Test-only Fernet key for CI startup and vault E2E. Production must set its own key.
CUBEPLEX_AUTH__VAULT_KEY: 1EwVRAtWccVJhdXX_2iepp7OlPfadfHpWNwVFpRzLBI=
# Frontend proxies to local backend:8001 (matches config.test.yaml)
CUBEPLEX_API_URL: http://127.0.0.1:8001
services:
postgres:
image: cubeplex/postgresql-pgroonga-pgvector:18.2-pgroonga4.0.6-pgvector0.8.2
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpass
POSTGRES_DB: cubeplex_test
ports: ['5432:5432']
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=10
redis:
image: redis:7-alpine
ports: ['6379:6379']
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.gate.outputs.sha }}
persist-credentials: false
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
# Ephemeral runner — skip post-run `uv cache prune --ci`
# which occasionally hangs on a concurrent lock and fails the job.
prune-cache: false
- uses: pnpm/action-setup@v6
with:
package_json_file: frontend/package.json
- uses: actions/setup-node@v7
with:
node-version: "20"
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install backend
run: make backend-install
- name: Install frontend
run: make frontend-install
- name: Build @cubeplex/core
run: make frontend-build-core
- name: Prepare RustFS data dirs
run: |
mkdir -p /tmp/rustfs/data /tmp/rustfs/logs
sudo chown -R 10001:10001 /tmp/rustfs
- name: Start RustFS
run: |
docker run -d --name rustfs \
-p 9000:9000 -p 9001:9001 \
-v /tmp/rustfs/data:/data \
-v /tmp/rustfs/logs:/logs \
rustfs/rustfs:1.0.0-alpha.97
# TCP/HTTP-level readiness; RustFS alpha lacks a unified /health endpoint.
# Accept any 2xx/3xx/4xx HTTP response as ready.
for i in {1..30}; do
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9000/ | grep -qE '^(2|3|4)'; then
echo "RustFS ready"
break
fi
sleep 2
done
- name: Create RustFS test bucket
env:
AWS_ACCESS_KEY_ID: rustfsadmin
AWS_SECRET_ACCESS_KEY: rustfsadmin
run: |
aws --endpoint-url http://127.0.0.1:9000 \
s3 mb s3://cubeplex-test --region us-east-1
- name: Install the optional package and mint an ephemeral licence
# admin-sso and admin-insights drive pages behind <EEGate>, and
# admin-insights now calls cost endpoints the optional package serves, so
# this backend needs both the package and a key. `--group` on its own
# re-resolves and drops the test dependencies, hence both flags. The
# keypair is generated here and thrown away: nothing is committed, and
# there is no key that expires out from under CI later.
run: |
cd backend
uv sync --all-extras --group licensed
uv run python scripts/dev/license_keygen.py dev-env --features multi_org \
>> "$GITHUB_ENV"
- name: Alembic migrate
run: make backend-migrate
- name: Start backend in background
run: |
nohup make backend-start > /tmp/backend-console.log 2>&1 &
- name: Wait for backend ready
run: |
timeout 60 bash -c '
until curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8001/docs | grep -qE "^(2|3|4)"; do
sleep 2
done
'
- name: Install playwright browsers
run: make frontend-install-browsers
- name: Playwright e2e
run: make frontend-test-e2e-ci
- name: Cleanup leftover test sandboxes
if: always()
run: make backend-cleanup-sandboxes
- name: Upload backend logs on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: backend-log
path: |
/tmp/backend.log
/tmp/backend-console.log
- name: Upload playwright traces on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: playwright-traces
path: |
frontend/test-results/
frontend/playwright-report/
report:
name: Finalize CI check run
if: |
always() &&
needs.gate.result == 'success' &&
github.event_name == 'issue_comment'
needs: [gate, changes, backend-check, frontend-check, backend-e2e, frontend-e2e]
runs-on: ubuntu-latest
permissions:
checks: write
steps:
- name: Compute conclusion and update check run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BACKEND_CHECK: ${{ needs.backend-check.result }}
FRONTEND_CHECK: ${{ needs.frontend-check.result }}
BACKEND_E2E: ${{ needs.backend-e2e.result }}
FRONTEND_E2E: ${{ needs.frontend-e2e.result }}
CHECK_ID: ${{ needs.gate.outputs.check_run_id }}
run: |
conclusion=success
for r in "$BACKEND_CHECK" "$FRONTEND_CHECK" "$BACKEND_E2E" "$FRONTEND_E2E"; do
case "$r" in
failure|cancelled|timed_out)
conclusion=failure
;;
esac
done
summary="backend-check=$BACKEND_CHECK, frontend-check=$FRONTEND_CHECK, backend-e2e=$BACKEND_E2E, frontend-e2e=$FRONTEND_E2E"
echo "Conclusion: $conclusion ($summary)"
gh api -X PATCH repos/${{ github.repository }}/check-runs/$CHECK_ID \
-f status=completed \
-f conclusion=$conclusion \
-f "output[title]=CI ($conclusion)" \
-f "output[summary]=$summary"