trilium/.github/workflows/dev.yml
Elian Doran e256260ff4
ci: shard the CKEditor suite so coverage data cannot exhaust the browser
Two runs of this branch died at the same point: 100 of 121 spec files
completed, then the browser session either dropped mid-run or stopped
responding until the step timed out. Chromedriver and Chrome are
correctly paired (150.0.7871.124 / .128), and the DOM is clean at the
end of every file — no editors, math fields, balloons or previews are
left behind, so the tests themselves are not leaking.

What accumulates is v8 coverage data. Browser mode keeps one page for
the whole run, and coverage collects for every script loaded into it.
Measured locally over the same file order:

  file    without --coverage    with --coverage
     20             218 MB              235 MB
     60             291 MB              498 MB
    100             236 MB              847 MB
    106                  -              873 MB  (peak)

Without coverage the heap sawtooths and GC keeps up. With it, the heap
climbs to ~3.6x by the 100th file — exactly where CI gives out. Locally
there is enough headroom to survive it; a runner has less.

Run the suite as two shards of ~60 files, which peaks around 500MB, and
merge the blob reports afterwards. Thresholds are zeroed for the shard
runs (each only exercises half the tree, so they would fail on files the
other shard covers) and enforced on the merged report, which reproduces
the single-run numbers exactly and still writes the JUnit file Codecov
uploads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:36:12 +03:00

415 lines
15 KiB
YAML

name: Dev
on:
push:
branches:
- main
- standalone
- "release/*"
pull_request:
branches:
- main
- standalone
- "release/*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GHCR_REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository}}
TEST_TAG: ${{ github.repository}}:test
permissions:
pull-requests: write # for PR comments
jobs:
test_dev:
name: Test development
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- name: Set up node & dependencies
uses: actions/setup-node@v7
with:
node-version: 24
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Run the client-side tests
id: test-client
run: pnpm run --filter=client test --coverage
- name: Upload client test report
uses: actions/upload-artifact@v7
if: always()
with:
name: client-test-report
path: apps/client/test-output/vitest/html/
retention-days: 30
- name: Run the server-side tests
id: test-server
run: pnpm run --filter=server test --coverage
- name: Upload server test report
uses: actions/upload-artifact@v7
if: always()
with:
name: server-test-report
path: apps/server/test-output/vitest/html/
retention-days: 30
- name: Upload client coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-client.outcome == 'success' }}
with:
files: apps/client/test-output/vitest/coverage/lcov.info
flags: client
disable_search: true
fail_ci_if_error: false
- name: Upload client test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-client.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: apps/client/test-output/vitest/junit.xml
flags: client
disable_search: true
fail_ci_if_error: false
- name: Upload server coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-server.outcome == 'success' }}
with:
files: apps/server/test-output/vitest/coverage/lcov.info
flags: server
disable_search: true
fail_ci_if_error: false
- name: Upload server test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-server.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: apps/server/test-output/vitest/junit.xml
flags: server
disable_search: true
fail_ci_if_error: false
- name: Run the standalone tests
id: test-standalone
# Runs the same trilium-core spec set as the server suite, but in
# happy-dom + sql.js WASM via BrowserSqlProvider (see
# apps/standalone/src/test_setup.ts). Catches differences
# between the Node-side and browser-side runtimes.
run: pnpm run --filter=standalone test --coverage
- name: Upload standalone coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-standalone.outcome == 'success' }}
with:
files: apps/standalone/test-output/vitest/coverage/lcov.info
flags: standalone
disable_search: true
fail_ci_if_error: false
- name: Upload standalone test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-standalone.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: apps/standalone/test-output/vitest/junit.xml
flags: standalone
disable_search: true
fail_ci_if_error: false
- name: Run the CKEditor 5 aggregate tests
id: test-ckeditor5
# The suite drives a real headless Chrome through webdriverio. When the browser session
# fails to start, vitest waits on it rather than erroring, so cap the step instead of
# letting it sit until the job's default timeout.
timeout-minutes: 15
run: |
export CHROMEDRIVER_PATH=$(which chromedriver || find /usr/local/share -name chromedriver -type f 2>/dev/null | head -1)
echo "Using chromedriver at: $CHROMEDRIVER_PATH"
# A chromedriver whose major version does not match the installed Chrome fails the
# session handshake, which surfaces as a hang or a mid-run disconnect rather than a
# clear error — so record both up front.
"$CHROMEDRIVER_PATH" --version || echo "could not read the chromedriver version"
(google-chrome --version || chromium --version || chromium-browser --version) 2>/dev/null \
|| echo "could not read the Chrome version"
# Browser mode keeps one page for the whole run, and v8 coverage data accumulates in it:
# measured locally, the heap reaches ~850MB by the 100th spec file with --coverage against
# ~240MB without it. That is where a CI runner's renderer gives out — the session either
# drops mid-run or stops responding. Run the suite in two shards (~60 files each, peaking
# around 500MB) and merge the blob reports afterwards, so the 100% gate is still evaluated
# over the whole suite rather than per shard.
SHARD_ARGS="--reporter=blob --coverage \
--coverage.thresholds.lines=0 --coverage.thresholds.functions=0 \
--coverage.thresholds.branches=0 --coverage.thresholds.statements=0"
pnpm run --filter=ckeditor5 test --shard=1/2 $SHARD_ARGS
pnpm run --filter=ckeditor5 test --shard=2/2 $SHARD_ARGS
pnpm run --filter=ckeditor5 test --mergeReports --coverage
- name: Upload ckeditor5 coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-ckeditor5.outcome == 'success' }}
with:
files: packages/ckeditor5/test-output/vitest/coverage/lcov.info
flags: ckeditor5
disable_search: true
fail_ci_if_error: false
- name: Upload ckeditor5 test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-ckeditor5.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/ckeditor5/test-output/vitest/junit.xml
flags: ckeditor5
disable_search: true
fail_ci_if_error: false
- name: Run the desktop tests
id: test-desktop
run: pnpm run --filter=desktop test --coverage
- name: Upload desktop coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-desktop.outcome == 'success' }}
with:
files: apps/desktop/test-output/vitest/coverage/lcov.info
flags: desktop
disable_search: true
fail_ci_if_error: false
- name: Upload desktop test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-desktop.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: apps/desktop/test-output/vitest/junit.xml
flags: desktop
disable_search: true
fail_ci_if_error: false
- name: Run the commons tests
id: test-commons
run: pnpm run --filter=commons test --coverage
- name: Upload commons coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-commons.outcome == 'success' }}
with:
files: packages/commons/test-output/vitest/coverage/lcov.info
flags: commons
disable_search: true
fail_ci_if_error: false
- name: Upload commons test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-commons.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/commons/test-output/vitest/junit.xml
flags: commons
disable_search: true
fail_ci_if_error: false
- name: Run the codemirror tests
id: test-codemirror
run: pnpm run --filter=codemirror test --coverage
- name: Upload codemirror coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-codemirror.outcome == 'success' }}
with:
files: packages/codemirror/test-output/vitest/coverage/lcov.info
flags: codemirror
disable_search: true
fail_ci_if_error: false
- name: Upload codemirror test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-codemirror.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/codemirror/test-output/vitest/junit.xml
flags: codemirror
disable_search: true
fail_ci_if_error: false
- name: Run the express-partial-content tests
id: test-express-partial-content
run: pnpm run --filter=express-partial-content test --coverage
- name: Upload express-partial-content coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-express-partial-content.outcome == 'success' }}
with:
files: packages/express-partial-content/test-output/vitest/coverage/lcov.info
flags: express-partial-content
disable_search: true
fail_ci_if_error: false
- name: Upload express-partial-content test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-express-partial-content.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/express-partial-content/test-output/vitest/junit.xml
flags: express-partial-content
disable_search: true
fail_ci_if_error: false
- name: Run the highlightjs tests
id: test-highlightjs
run: pnpm run --filter=highlightjs test --coverage
- name: Upload highlightjs coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-highlightjs.outcome == 'success' }}
with:
files: packages/highlightjs/test-output/vitest/coverage/lcov.info
flags: highlightjs
disable_search: true
fail_ci_if_error: false
- name: Upload highlightjs test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-highlightjs.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/highlightjs/test-output/vitest/junit.xml
flags: highlightjs
disable_search: true
fail_ci_if_error: false
- name: Run the pdfjs-viewer tests
id: test-pdfjs-viewer
run: pnpm run --filter=pdfjs-viewer test --coverage
- name: Upload pdfjs-viewer coverage to Codecov
uses: codecov/codecov-action@v7
if: ${{ !cancelled() && steps.test-pdfjs-viewer.outcome == 'success' }}
with:
files: packages/pdfjs-viewer/test-output/vitest/coverage/lcov.info
flags: pdfjs-viewer
disable_search: true
fail_ci_if_error: false
- name: Upload pdfjs-viewer test results to Codecov
uses: codecov/test-results-action@v1
if: ${{ !cancelled() && steps.test-pdfjs-viewer.outcome != 'skipped' }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/pdfjs-viewer/test-output/vitest/junit.xml
flags: pdfjs-viewer
disable_search: true
fail_ci_if_error: false
- name: Run the rest of the tests
run: pnpm run --filter=\!client --filter=\!standalone --filter=\!server --filter=\!desktop --filter=\!commons --filter=\!ckeditor5 --filter=\!codemirror --filter=\!express-partial-content --filter=\!highlightjs --filter=\!pdfjs-viewer test
build_docker:
name: Build Docker image
runs-on: ubuntu-latest
needs:
- test_dev
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- name: Set up node & dependencies
uses: actions/setup-node@v7
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update build info
run: pnpm run chore:update-build-info
- name: Trigger client build
run: pnpm client:build
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Trigger server build
run: pnpm run server:build
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: apps/server
cache-from: type=gha
cache-to: type=gha,mode=max
test_docker:
name: Check Docker build
runs-on: ubuntu-latest
needs:
- build_docker
strategy:
matrix:
include:
- dockerfile: Dockerfile.alpine
- dockerfile: Dockerfile
steps:
- name: Checkout the repository
uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- name: Set up node & dependencies
uses: actions/setup-node@v7
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update build info
run: pnpm run chore:update-build-info
- name: Trigger build
run: pnpm server:build
- name: Set IMAGE_NAME to lowercase
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
- name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build and export to Docker
uses: docker/build-push-action@v7
with:
context: apps/server
file: apps/server/${{ matrix.dockerfile }}
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Validate container run output
run: |
CONTAINER_ID=$(docker run -d --log-driver=journald --rm --name trilium_local ${{ env.TEST_TAG }})
echo "Container ID: $CONTAINER_ID"
- name: Wait for the healthchecks to pass
uses: stringbean/docker-healthcheck-action@v3
with:
container: trilium_local
wait-time: 50
require-status: running
require-healthy: true
# Print the entire log of the container thus far, regardless if the healthcheck failed or succeeded
- name: Print entire log
if: always()
run: journalctl -u docker CONTAINER_NAME=trilium_local --no-pager