Skip to content

CI/CD

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

All ci.yml jobs run on the Forgejo docker runner label (runs-on: docker).

Caching strategy

This repo is orchestrated by Vite+ (the vp CLI), not Turbo or a standalone pnpm install step. CI caching has two layers:

  • Dependencies — handled by voidzero-dev/setup-vp@v1 with cache: true. The action provisions Node (pinned via node-version-file: .node-version) plus pnpm, restores the dependency cache, and runs vp install. There is no separate actions/cache step for the pnpm store.
  • Vite+ task cache — each of check, test, build, and deploy-docs adds one actions/cache@v4 step for the path node_modules/.vite/task-cache, keyed per job, OS, and commit SHA, with restore-keys falling back to the base branch (or main) and then any prior run for that job and OS. This replays per-package task outputs across runs when inputs are unchanged — the rough equivalent of a remote build cache.

The exact key and fallbacks:

yaml
key: vp-task-<job>-${{ runner.os }}-${{ github.sha }}
restore-keys: |
  vp-task-<job>-<os>-${{ github.base_ref || 'main' }}-
  vp-task-<job>-<os>-

actions/checkout@v4 uses fetch-depth: 0. Jobs run their task recursively across all packages with vp run --cache -r <task> (the check job adds -v). Skipping unchanged work comes from the task-cache replay, not from an affected-graph filter — there is no --affected flag in the workflow.

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> via actions/upload-artifact@v3. Downstream e2e shards download it via actions/download-artifact@v3 and extract the artifact, so they skip both install and build — they run setup-vp with run-install: false.

E2E sharding

e2e-pr and e2e-main are matrix-sharded (shard: ["1/2", "2/2"]) so the Playwright suite runs in parallel across two shards. fail-fast: false — a failure in one shard does not cancel the other.

Jobs

1. check

Code quality gate — runs on every trigger.

  1. Set up Vite+ via https://github.com/voidzero-dev/setup-vp@v1 with node-version-file: .node-version and cache: true (provisions Node + pnpm, restores deps, runs vp install).
  2. Restore the Vite+ task cache (actions/cache@v4, path node_modules/.vite/task-cache).
  3. Run vp run --cache -r -v check — format + lint + typecheck across all packages. The docs workspace runs only fmt + lint here; the full VitePress build that catches missing pages, invalid frontmatter, and bad links runs in the build job and in deploy-docs, not in check. Vite Task replays cached steps for unchanged packages.

2. test

Unit tests — runs on every trigger.

  1. Set up Vite+ (setup-vp@v1, cache: true).
  2. Restore the Vite+ task cache (actions/cache@v4).
  3. Run 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 with vp run --cache -r build (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)
  • docs/node_modules/ (the tar command has a fallback that omits docs/node_modules if it is absent)

The artifact uses actions/upload-artifact@v3. 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.60.0-jammy — Microsoft's official Playwright image with browsers + system deps pre-installed. The Playwright version is pinned in pnpm-workspace.yaml under the catalog (@playwright/test: 1.60.0); tests/e2e/package.json references it as catalog:. When upgrading Playwright, bump the workspace catalog and the two ci.yml container tags in lockstep. (docker-compose.test.yml uses the -noble variant, v1.60.0-noble, for the local Playwright service.)

Services: PostgreSQL 17 + Redis 7 (inline service containers, each with a health check — pg_isready / redis-cli ping — with a 2s interval).

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

Artifacts:

  • Playwright HTML report — e2e-report-pr-<idx> (always uploaded, 7-day retention).
  • Test results (screenshots, videos, traces) — e2e-test-results-pr-<idx> (uploaded on failure, 7-day retention).

<idx> is strategy.job-index. Artifacts use actions/upload-artifact@v3.

PR integration:

  • A test-results comment is posted to the PR via the Forgejo issues/comments API. It is posted only from shard 1/2 (if: always() && matrix.shard == '1/2') and reflects only that single shard's pass/fail — other shards may still be running. The step looks up an existing CI Test Results comment (python3-based) and updates it, otherwise creates a new one, authenticating with GITHUB_TOKEN.

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.60.0-jammy image as e2e-pr.

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

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

Artifacts:

  • Playwright HTML report — e2e-report-<idx> (always uploaded, 7-day retention).
  • Test results (screenshots, videos, traces) — e2e-test-results-<idx> (uploaded on failure, 7-day retention).

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+ (setup-vp@v1, cache: true) and restore the Vite+ task cache (actions/cache@v4).
  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 vp exec wrangler pages deploy.

Secrets required: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN

Environment Variables (CI)

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

CI=true
E2E_TEST=1
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. Versions packages and pushes the unified Docker image (API + SPA) to the Forgejo container registry, then lands the version bump on main.

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

The workflow is split across two runner labels so the version bump only reaches main after the image is confirmed in the registry:

Jobs:

  1. version (runs-on: docker — Node/pnpm, no Docker daemon) — Sets LEFTHOOK: "0" to disable git hooks, checks out with fetch-depth: 100, runs vp install --frozen-lockfile, then runs vp exec changeset version to bump package versions and update changelogs. It reads the new api-hono and web versions, commits the bump onto a staging branch ci/release-<run_id>, and pushes that branch. It does NOT touch main and does NOT create tags or releases.
  2. publish (runs-on: docker-cli — Docker, no Node) — Clones the staging branch, logs into the registry with REGISTRY_TOKEN, and computes the composite tag IMAGE_TAG=v<api-version>-web<web-version>. It checks docker manifest inspect and skips the build if that tag already exists in the registry (IMAGE_CHANGED guard). Otherwise it builds and pushes :<tag> and :latest. Only after the image is confirmed in the registry does it rebase the version bump onto main and push, then create the git tags (api-hono/v<version>, web/v<version>) and Forgejo releases (release bodies extracted from each package's CHANGELOG.md). Finally it deletes the ci/release-<run_id> staging branch (if: always()).

Image produced:

  • <registry>/<owner>/<repo>:v<api-version>-web<web-version> + :latest. The registry/repo path is derived dynamically from github.server_url + github.repository (lowercased) — i.e. the Forgejo server URL plus owner/repo.

Secrets required: GITHUB_TOKEN (checkout, push, tags, releases), REGISTRY_TOKEN (registry login).

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.