Appearance
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@v1withcache: true. The action provisions Node (pinned vianode-version-file: .node-version) plus pnpm, restores the dependency cache, and runsvp install. There is no separateactions/cachestep for the pnpm store. - Vite+ task cache — each of
check,test,build, anddeploy-docsadds oneactions/cache@v4step for the pathnode_modules/.vite/task-cache, keyed per job, OS, and commit SHA, withrestore-keysfalling back to the base branch (ormain) 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.
- Set up Vite+ via
https://github.com/voidzero-dev/setup-vp@v1withnode-version-file: .node-versionandcache: true(provisions Node + pnpm, restores deps, runsvp install). - Restore the Vite+ task cache (
actions/cache@v4, pathnode_modules/.vite/task-cache). - 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 thebuildjob and indeploy-docs, not incheck. Vite Task replays cached steps for unchanged packages.
2. test
Unit tests — runs on every trigger.
- Set up Vite+ (
setup-vp@v1,cache: true). - Restore the Vite+ task cache (
actions/cache@v4). - 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/andapps/web/dist/services/api-hono/node_modules/andservices/api-hono/dist/tests/e2e/node_modules/(Playwright workspace)docs/node_modules/(the tar command has a fallback that omitsdocs/node_modulesif 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 existingCI Test Resultscomment (python3-based) and updates it, otherwise creates a new one, authenticating withGITHUB_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.
- Gate step:
git diff --name-only HEAD^ HEAD— skip the rest of the job when the push doesn't touchdocs/,pnpm-lock.yaml,vite.config.ts, or any rootpackage.json. - Set up Vite+ (
setup-vp@v1,cache: true) and restore the Vite+ task cache (actions/cache@v4). - Build docs (
vp run --cache -F @libris/docs build, which also runs@libris/api-hono#build:specfor the OpenAPI spec via dependsOn) — replayed from Vite Task cache when unchanged. - Deploy
docs/.vitepress/distviavp 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/migrationsPublish 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:
- version (
runs-on: docker— Node/pnpm, no Docker daemon) — SetsLEFTHOOK: "0"to disable git hooks, checks out withfetch-depth: 100, runsvp install --frozen-lockfile, then runsvp exec changeset versionto bump package versions and update changelogs. It reads the newapi-honoandwebversions, commits the bump onto a staging branchci/release-<run_id>, and pushes that branch. It does NOT touch main and does NOT create tags or releases. - publish (
runs-on: docker-cli— Docker, no Node) — Clones the staging branch, logs into the registry withREGISTRY_TOKEN, and computes the composite tagIMAGE_TAG=v<api-version>-web<web-version>. It checksdocker manifest inspectand skips the build if that tag already exists in the registry (IMAGE_CHANGEDguard). Otherwise it builds and pushes:<tag>and:latest. Only after the image is confirmed in the registry does it rebase the version bump ontomainand push, then create the git tags (api-hono/v<version>,web/v<version>) and Forgejo releases (release bodies extracted from each package'sCHANGELOG.md). Finally it deletes theci/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 fromgithub.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:
| Service | Port | Config |
|---|---|---|
| PostgreSQL 17 | 5433 | DB: libris_test, tmpfs storage |
| Redis 7 | 6380 | Default storage |
PostgreSQL uses tmpfs for fast ephemeral storage. Redis uses its default in-memory store.