Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Web checks

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
web:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.13.1

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm

- name: Check web CI configuration
run: node --test scripts/web-ci.test.mjs

- name: Install JS dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Test web
run: pnpm web:test

- name: Typecheck web
run: pnpm web:typecheck

- name: Build web
run: pnpm web:build
13 changes: 11 additions & 2 deletions docs/CI.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# CI and quality gates

This document describes automated checks that protect the TypeScript workspace
(`packages/*` and `services/*`).
This document describes automated checks for the web app and TypeScript
workspaces (`apps/*`, `packages/*`, and `services/*`).

## Web checks

The `Web checks` workflow (`.github/workflows/web.yml`) runs on pushes to
`main` and on pull requests. Its single blocking job installs locked JS
dependencies without lifecycle scripts, then runs `pnpm web:test`,
`pnpm web:typecheck`, and `pnpm web:build` in order. Run the same three commands
locally to check `apps/web`; `node --test scripts/web-ci.test.mjs` verifies the
CI job configuration.

## Test coverage gate

Expand Down
26 changes: 26 additions & 0 deletions scripts/web-ci.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2026 Sub Rosa contributors
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";

const workflow = readFileSync(new URL("../.github/workflows/web.yml", import.meta.url), "utf8");
const scripts = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).scripts;

test("web CI runs all three root checks as blocking steps in one job", () => {
assert.match(workflow, /^ push:\n branches: \[main\]$/m);
assert.match(workflow, /^ pull_request:$/m);

const job = workflow.match(/^ web:\n([\s\S]*)$/m)?.[1];
assert.ok(job, "web job must exist");
assert.doesNotMatch(job, /^ [a-z][\w-]*:/m, "checks must stay in the web job");
assert.doesNotMatch(job, /\bcontinue-on-error\s*:/, "checks must block on failure");
assert.match(job, /^ run: pnpm install --frozen-lockfile --ignore-scripts$/m);

let previousStep = -1;
for (const script of ["web:test", "web:typecheck", "web:build"]) {
assert.ok(scripts[script], `root script ${script} must exist`);
const step = job.indexOf(` run: pnpm ${script}\n`);
assert.ok(step > previousStep, `${script} must run after the preceding check`);
previousStep = step;
}
});
Loading