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
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ TITLE FORMAT (required):
✗ Bad: Added weekly ranking filter to leaderboard

BEFORE PUSHING:
bun run format && bun run check # Format + lint + typecheck all services
bun run format && bun run check # Apply formatting, then run the local quality gate

AFTER API CHANGES:
bun run generate:api # Regenerate all OpenAPI clients
Expand Down
12 changes: 11 additions & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
bun run pre-push
if [ "${HEPHAESTUS_SKIP_PRE_PUSH:-}" = "1" ]; then
echo "WARNING: pre-push verification skipped. Run: bun run check"
exit 0
fi

echo "Pre-push: bun run check"
if ! bun run check; then
echo "Pre-push failed. Run: bun run check"
exit 1
fi
echo "Pre-push passed."
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ drifts. Copy a skill nowhere else.
Finish every change set with `bun run format` then `bun run check`, so styling and type checks
reflect the final state. Document any skipped gate in the PR description.

Use `bun run check:affected` for in-session feedback; it is not the pre-push gate. Its contract is in
`docs/contributor/local-verification.mdx`.

| Command | Does |
|---|---|
| `bun run format` / `format:check` | Apply / verify formatting (Java + TypeScript) |
| `bun run check` | Static analysis, formatting checks, agent tests, and repository policy checks — every leg is listed in the root `package.json` |
| `bun run verify` | Complete local CI mirror for checks that need no live service, image build, or hosted credential |
| `bun run test:webapp` | Vitest |
| `bun run test:agents` | Agent runtime and precompute specs, on Bun |
| `cd server && ./mvnw test` | Server unit tests — see `server/AGENTS.md` for all four tiers |
Expand All @@ -55,9 +59,10 @@ Naming: `format` applies, `format:check` verifies read-only for CI, `lint` lints
comprehensive local quality gate. A `:webapp`, `:server` or `:agents` suffix scopes any of them; `:java`
scopes `format` and `lint` only — the Java leg of `check` is `check:server`.

`bun run check` is the complete local quality gate. CI distributes its legs across required jobs,
`bun run check` is the complete local quality gate; `bun run verify` adds locally runnable builds and
broader tests. CI distributes these checks across required jobs,
using path filters where appropriate, and adds builds, generated-file checks, broader tests, image
checks, and security scans. Run `check` before pushing, then run the affected tests or builds.
checks, and security scans. Run `check` before pushing and `verify` before requesting review.

### Lint and format scopes

Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Contributions that do not adhere to these guidelines will be rejected. We align

## Contribution Process

Use `bun run check:affected` for fast feedback. Before pushing, run `bun run check`; the hook runs it
automatically. Run `bun run verify` before requesting review. Scope, budgets, and exclusions are documented in the
[local verification guide](https://ls1intum.github.io/Hephaestus/contributor/local-verification).

1. **External contributors only**: Fork the Repository and create a branch.
2. **Create a feature branch**: Work on your changes in a separate branch.
3. **Follow pull request title guidelines**: Ensure your PR title follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
Expand Down
5 changes: 2 additions & 3 deletions docs/contributor/local-development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ bun run typecheck:webapp # The SPA's typecheck — a separate leg, not part
bun run check # Complete local quality gate
```

Run `bun run check` before pushing for the complete local gate. CI distributes those checks across
required jobs, using path filters where appropriate, and adds builds, generated-file checks, broader
tests, image checks, and security scans.
Use the [local verification contract](./local-verification.mdx) for affected checks, the pre-push gate,
budgets, and CI exclusions.

IntelliJ and WebStorm are also supported.

Expand Down
78 changes: 78 additions & 0 deletions docs/contributor/local-verification.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Local verification
---

# Local verification

| Command | What success proves | Excludes | Warm budget |
|---|---|---|---:|
| `bun run check:affected` | Static and policy checks selected for the branch and working tree passed | Tests, builds, unaffected scopes, and CI-only checks | Webapp 6 s; server 12 s; agents/docs 3 s |
| `bun run check` | Every script in `package.json#scripts.check` passed | Builds, browser and live-service tests, images, mutation analysis, and release security | 4 min |
| `bun run verify` | The complete credential-free local CI inventory passed | Live-service integration and E2E tests, images, mutation analysis, and release security | 10 min |

Run `check:affected` while working, `check` before pushing, and `verify` before requesting review. Each
success message names a different level of evidence.

## Affected selection

The command compares committed, staged, unstaged, untracked, and deleted paths with the merge base of
`origin/main`. For another target, run `bun run check:affected --base <revision>`.

| Changed path | Commands |
|---|---|
| `webapp/**` | `check:webapp:affected` |
| Handwritten `server/**` | `check:server:affected` |
| Agent runtime or precompute TypeScript | `check:agent-runtime` |
| `docs/**` | `check:docs` |
| `docs/images/readme/**` | `check:docs` and `check:webapp:affected` |
| Shared tooling, instructions, generated artifacts, or any unclassified path | `check` |

The exact checks behind each scoped command are defined in the root `package.json`. Selection uses
`git diff --no-renames`, so both sides of a rename are evaluated. Git documents the underlying change
statuses in [`git diff`](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB).

## Pre-push hook

The hook runs the read-only `bun run check`. For an emergency bypass,
`HEPHAESTUS_SKIP_PRE_PUSH=1 git push` prints an unverified result and the corrective command. Git's
standard `git push --no-verify` skips the hook without output.

## Complete local verification

`bun run verify` adds the complete webapp test suites, Storybook browser tests and production build,
server unit and architecture tests with coverage, webapp build and generated-route comparison, and
docs build. The webapp build check restores the generated route file before it exits, so verification
never rewrites the working tree.

## Measurement snapshot

Measured 2026-08-28 on Ubuntu 24.04, Intel Core i5-13500 (20 logical CPUs), 62 GiB RAM, Bun 1.4.0,
and JDK 21. Dependencies were cached. “First” and “warm” are consecutive processes in the same
checkout; GNU `time` reports wall time and largest-child RSS.

| Path | First | Warm | Largest-child RSS | Observed first failure |
|---|---:|---:|---:|---|
| `format:check` | 21 s | 20 s | 841 MiB | Not measured |
| `check:webapp` | 5 s | 4 s | 1.6 GiB | Not measured |
| `test:agents` | 3 s | 3 s | 141 MiB | Not measured |
| `check` | 63 s (failed) | 42 s | 1.6 GiB | Tooling lint at 63 s |
| `verify` | Not measured | 8 min 27 s | 5.3 GiB | Not measured |

The selected commands were also run twice on the same checkout:

| Scope | First | Warm | Largest-child RSS | Planted failure detected after |
|---|---:|---:|---:|---:|
| Webapp | 4.6 s | 5.7 s | 1.7 GiB | 2.5 s |
| Server | 12.1 s | 11.9 s | 1.3 GiB | 3.0 s |
| Agent runtime | 1.3 s | 1.4 s | 458 MiB | 0.8 s |
| Docs | 2.3 s | 2.3 s | 190 MiB | 1.0 s |

The failures were invalid source files placed in each selected tree and removed after the command
failed. Automated tests additionally plant committed, staged, unstaged, untracked, deleted, and
renamed paths in a temporary Git repository. Table-driven cases assert fail-closed expansion for
shared configuration, generated API and route artifacts, cross-workspace tooling, hooks, and unknown
root inputs.

The live hosted-check inventory is documented in [CI/CD](./ci-cd.mdx). CI additionally owns checks
that need browsers, services, images, release infrastructure, or credentials. Hosted CI performance is
tracked in [issue #1496](https://github.com/ls1intum/Hephaestus/issues/1496).
1 change: 1 addition & 0 deletions docs/sidebars.contributor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const sidebars: SidebarsConfig = {
label: "Development Workflow",
items: [
"local-development",
"local-verification",
"testing",
"e2e-testing",
"coding-guidelines",
Expand Down
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
"format:java:check": "bun scripts/run-mvnw.ts -pl application spotless:check -q",
"lint:java": "bun run prepare:server:generated && bun scripts/run-mvnw.ts -f application/pom.xml compile pmd:check -q",
"lint:java:report": "bun run prepare:server:generated && bun scripts/run-mvnw.ts -f application/pom.xml compile pmd:pmd && echo 'Report: server/application/target/site/pmd.html'",
"pre-push": "bun run check",
"check:affected": "bun scripts/check-affected.ts",
"format:webapp": "bun run --filter webapp format",
"format:webapp:check": "bun run --filter webapp format:check",
"lint:webapp": "bun run --filter webapp lint",
"lint:webapp:fix": "bun run --filter webapp lint:fix",
"check:webapp": "bun run --filter webapp check",
"check:webapp:affected": "bun --bun run-s check:client check:components check:stories check:story-sort",
"check:webapp:fix": "bun run --filter webapp check:fix",
"typecheck:webapp": "bun run --filter webapp typecheck",
"test:webapp": "bun run --filter webapp test",
Expand All @@ -37,6 +38,7 @@
"test:agents": "bun test server/application/src/test/resources/agent docker/agents/precompute",
"format:agents": "oxfmt --write 'server/application/src/{main,test}/resources/agent/**/*.ts' 'server/application/src/main/resources/practices/precompute/**/*.ts' 'docker/agents/precompute/**/*.ts' 'scripts/**/*.ts'",
"format:agents:check": "oxfmt --check 'server/application/src/{main,test}/resources/agent/**/*.ts' 'server/application/src/main/resources/practices/precompute/**/*.ts' 'docker/agents/precompute/**/*.ts' 'scripts/**/*.ts'",
"format:agent-runtime:check": "oxfmt --check 'server/application/src/{main,test}/resources/agent/**/*.ts' 'server/application/src/main/resources/practices/precompute/**/*.ts' 'docker/agents/precompute/**/*.ts'",
"format:docs": "oxfmt --write 'docs/**/*.{js,jsx,ts,tsx,json,jsonc,css}'",
"format:docs:check": "oxfmt --check 'docs/**/*.{js,jsx,ts,tsx,json,jsonc,css}'",
"format:config": "oxfmt --write *.ts *.json .oxfmtrc.json .oxlintrc.json .changeset/*.cjs .changeset/*.json .vscode/*.json scripts/tsconfig.json project.code-workspace",
Expand All @@ -50,6 +52,9 @@
"format:server:check": "bun run format:java:check",
"lint:server": "bun run lint:java",
"check:server": "bun run format:server:check && bun run lint:server",
"check:server:affected": "bun --bun run-s check:java-nullness check:server",
"check:agent-runtime": "bun run format:agent-runtime:check && oxlint server/application/src/main/resources/agent server/application/src/test/resources/agent server/application/src/main/resources/practices/precompute docker/agents/precompute && bun run typecheck:agents",
"check:docs": "bun run format:docs:check && oxlint docs && bun run docs:lint && bun run check:diagrams && bun run check:instructions",
"check:contracts": "bun scripts/validate-artifact-source-contracts.ts && bun scripts/check-artifact-source-contract-immutability.ts && bun test scripts/check-artifact-source-contract-immutability.test.ts",
"format:client": "bun run format:webapp",
"format:client:check": "bun run format:webapp:check",
Expand All @@ -59,6 +64,10 @@
"format:check": "bun run format:server:check && bun run format:client:check && bun run format:agents:check && bun run format:docs:check && bun run format:config:check",
"lint": "bun --bun run-s lint:server lint:client lint:agents",
"check": "bun --bun run-s check:package-manager check:agent-runtime-pins check:java-nullness check:server check:client check:agents test:agents check:stories check:story-sort check:components check:diagrams check:preview-stack check:env check:contracts check:instructions check:changesets docs:lint",
"verify": "bun --bun run-s check test:webapp test:webapp:stories test:server:verification verify:webapp-build verify:storybook-build docs:build",
"test:webapp:stories": "bun run --filter webapp test:storybook",
"verify:webapp-build": "bun scripts/verify-webapp-build.ts",
"verify:storybook-build": "bun run --filter webapp build-storybook",
"typecheck": "bun run typecheck:webapp && bun run typecheck:scripts && bun run typecheck:agents",
"db:draft-changelog": "bun scripts/db-utils.ts draft-changelog",
"db:generate-erd-docs": "bun scripts/db-utils.ts generate-erd",
Expand Down
123 changes: 123 additions & 0 deletions scripts/check-affected.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";

import {
changedPaths,
commandsFor,
environmentWithoutGitRepository,
parseBase,
scopesFor,
} from "./check-affected.ts";

await test("accepts only the documented arguments", () => {
assert.equal(parseBase([]), "origin/main");
assert.equal(parseBase(["--base", "upstream/trunk"]), "upstream/trunk");
for (const args of [["--base"], ["--base", ""], ["--unknown"], ["--base", "main", "extra"]])
assert.throws(() => parseBase(args), /Usage:/);
});

await test("selects ordinary workspace changes", () => {
assert.deepEqual(scopesFor(["webapp/src/a.tsx"]), ["webapp"]);
assert.deepEqual(scopesFor(["server/application/src/main/java/A.java"]), ["server"]);
});

await test("combines independent workspaces", () => {
assert.deepEqual(scopesFor(["webapp/src/a.tsx", "server/application/src/main/java/A.java"]), [
"server",
"webapp",
]);
});

await test("selects the Bun runtime and precompute trees", () => {
assert.deepEqual(
scopesFor([
"server/application/src/main/resources/agent/main.ts",
"docker/agents/precompute/a.ts",
]),
["agents"],
);
});

await test("selects documentation changes", () => {
assert.deepEqual(scopesFor(["docs/contributor/example.mdx"]), ["docs"]);
assert.deepEqual(scopesFor(["docs/images/readme/example.png"]), ["docs", "webapp"]);
});

await test("maps scopes to the documented commands", () => {
assert.deepEqual(commandsFor(["agents", "docs", "server", "webapp"]), [
["bun", "run", "check:agent-runtime"],
["bun", "run", "check:docs"],
["bun", "run", "check:server:affected"],
["bun", "run", "check:webapp:affected"],
]);
});

await test("fails closed for shared, generated, contract, tooling, and unknown inputs", () => {
for (const path of [
"package.json",
".oxlintrc.json",
"scripts/check-affected.ts",
"server/openapi.yaml",
"webapp/src/api/core/a.ts",
"webapp/src/routeTree.gen.ts",
"webapp/tools/oxlint/index.ts",
"docs/contributor/erd/schema.mmd",
".husky/pre-push",
"docker/compose.app.yaml",
"webapp/CLAUDE.md",
"some-new-root-input.txt",
])
assert.deepEqual(scopesFor([path]), ["full"], path);
});

await test("a full-gate input overrides scoped inputs", () => {
assert.deepEqual(scopesFor(["webapp/src/a.tsx", "package.json"]), ["full"]);
assert.deepEqual(commandsFor(["full"]), [["bun", "run", "check"]]);
});

await test("discovers committed, staged, unstaged, untracked, deleted, and renamed paths", async () => {
const directory = await mkdtemp(join(tmpdir(), "check-affected-"));
const git = (...args: string[]) =>
execFileSync("git", args, { cwd: directory, env: environmentWithoutGitRepository() });
const put = async (path: string, content = path) => {
await mkdir(join(directory, path, ".."), { recursive: true });
await writeFile(join(directory, path), content);
};
try {
git("init", "-b", "main");
git("config", "user.email", "test@example.com");
git("config", "user.name", "Test");
await put("server/renamed.ts");
await put("docs/deleted.md");
await put("webapp/unstaged.ts");
git("add", ".");
git("commit", "-m", "initial");
git("checkout", "-q", "-b", "feature");
await put("server/committed.ts");
git("add", ".");
git("commit", "-m", "feature");
await put("webapp/unstaged.ts", "changed");
await put("docs/staged.md");
git("add", "docs/staged.md");
await put("docker/untracked.txt");
await rm(join(directory, "docs/deleted.md"));
await mkdir(join(directory, "webapp"), { recursive: true });
git("mv", "server/renamed.ts", "webapp/renamed.ts");

assert.deepEqual(changedPaths("main", directory), [
"docker/untracked.txt",
"docs/deleted.md",
"docs/staged.md",
"server/committed.ts",
"server/renamed.ts",
"webapp/renamed.ts",
"webapp/unstaged.ts",
]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
Loading
Loading