Skip to content

CI/CD

Forgejo Actions workflow at .forgejo/workflows/ci.yml. Runs on push to main and PRs to main.

Caching strategy

Every job that runs pnpm/turbo restores two caches via actions/cache@v4:

  • pnpm store~/.local/share/pnpm/store/v3, keyed on pnpm-lock.yaml hash. Skips re-downloading unchanged deps. Note this only skips the network download; the per-project node_modules/ symlink tree still has to be materialized on every install (~18s). The build job's artifact is how we skip that too.
  • Turbo outputs.turbo/, keyed on commit SHA with a base_ref/main fallback. Replays per-package task outputs when inputs haven't changed — the biggest single win for build-heavy jobs.

Combined with actions/checkout@v4 using fetch-depth: 0 so Turbo can diff against main, this lets the check and test jobs run with --affected and skip packages whose source didn't change (e.g. a docs-only PR doesn't retypecheck api/web).

Shared build artifact

The build job runs once, produces apps/web/dist/ and services/api-hono/dist/, and tars up the fully-materialized node_modules/ with all its .pnpm/ virtual store links. It publishes the tarball as build-output-<sha>. Downstream e2e shards download + extract the artifact and skip both pnpm install and pnpm run build — ~80s saved per shard.

E2E sharding

e2e-pr and e2e-main are matrix-sharded (shard: ["1/2", "2/2"]) so the full Playwright suite runs in parallel across two runners. fail-fast: false — a failure in one shard doesn't cancel the other. With two runners registered (runner1 and runner2), both shards run simultaneously.

Jobs

1. check

Code quality gate — runs on every trigger.

  1. Set up Vite+ via voidzero-dev/setup-vp@v1 (Node + pnpm + cache)
  2. Install dependencies (vp install)
  3. Run vp run --cache -r check — format + lint + typecheck across all packages, plus a VitePress build of the docs workspace. A broken docs build (missing pages, invalid frontmatter, bad links) fails CI. Vite Task replays cached steps for unchanged packages.

2. test

Unit tests — runs on every trigger.

  1. Set up Vite+ and restore caches
  2. Run monorepo tests (vp run --cache -r test) — Vite Task fingerprints inputs per package, so unchanged packages replay from cache.

3. build

Shared prerequisite for all e2e jobs — runs once after check passes. Builds every workspace project (api + web + docs) and publishes a build-output-<sha> artifact containing:

  • Root node_modules/ (fully materialized with pnpm's .pnpm/ virtual store)
  • apps/web/node_modules/ and apps/web/dist/
  • services/api-hono/node_modules/ and services/api-hono/dist/
  • tests/e2e/node_modules/ (Playwright workspace)

Artifact retention is 1 day (intermediate build artifact, not a release asset).

4. e2e-pr

Smoke E2E tests — runs only on PRs, depends on build (which itself depends on check).

Container: mcr.microsoft.com/playwright:v1.58.2-jammy — Microsoft's official Playwright image with browsers + system deps pre-installed. The image tag is pinned to the @playwright/test version in tests/e2e/package.json and must be bumped in lockstep when upgrading Playwright.

Services: PostgreSQL 17 + Redis 7 (inline service containers).

Tests run: @smoke tag only (fast feedback). Sharded across two matrix shards that run in parallel.

Artifacts:

  • Playwright HTML report (always uploaded)
  • Test results — screenshots, videos, traces (uploaded on failure)

PR integration:

  • Test results posted as PR comment via Forgejo API (creates or updates existing comment)

5. e2e-main

Full E2E suite — runs only on push to main, depends on build (which itself depends on check).

Container: same mcr.microsoft.com/playwright:v1.58.2-jammy image as e2e-pr.

Services: PostgreSQL 17 + Redis 7 (inline service containers).

Tests run: Full Playwright suite (no tag filter). Sharded across two matrix shards that run in parallel.

Artifacts:

  • Playwright HTML report (always uploaded)
  • Test results — screenshots, videos, traces (uploaded on failure)

6. deploy-docs

Deploys VitePress documentation to Cloudflare Pages — runs on push to main only, depends on check passing.

  1. Gate step: git diff --name-only HEAD^ HEAD — skip the rest of the job when the push doesn't touch docs/, pnpm-lock.yaml, vite.config.ts, or any root package.json.
  2. Set up Vite+ and restore caches.
  3. Build docs (vp run --cache -F @libris/docs build, which also runs @libris/api-hono#build:spec for the OpenAPI spec via dependsOn) — replayed from Vite Task cache when unchanged.
  4. Deploy docs/.vitepress/dist via wrangler pages deploy.

Secrets required: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN

Environment Variables (CI)

Set on the e2e-pr and e2e-main jobs:

POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=libris_test
POSTGRES_PASSWORD=libris_test
POSTGRES_DB=libris_test
REDIS_HOST=redis
REDIS_PORT=6379
LIBRIS_INBOX_PATH=/tmp/e2e-inbox
LIBRIS_LIBRARY_PATH=/tmp/e2e-library
API_SECRET_KEY=ci-e2e-test-secret-key-minimum-32-chars!
COOKIE_DOMAIN=""
MIGRATIONS_PATH=./services/api-hono/migrations

Publish Images

Manual workflow at .forgejo/workflows/publish-images.yml. Builds and pushes Docker images to the Forgejo container registry.

Trigger: workflow_dispatch (run from Forgejo UI: Actions > Publish Images > Run workflow)

Jobs:

  1. version — Runs changeset version to bump package versions and update changelogs. Creates git tags (api-hono/v<version>, web/v<version>) and Forgejo releases with changelogs.
  2. publish — Builds and pushes the unified Docker image (API + SPA).

Image produced:

  • registry.example.com/user/libris:v<api-version>-web<web-version> + latest

See docs/deployment.md for production usage.

Docker Test Services

docker-compose.test.yml for local testing:

ServicePortConfig
PostgreSQL 175433DB: libris_test, tmpfs storage
Redis 76380Default storage

PostgreSQL uses tmpfs for fast ephemeral storage. Redis uses its default in-memory store.