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
17 changes: 17 additions & 0 deletions .ci/E2E-tests/determine-relevant-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@

set -e

# This script uses associative arrays and `mapfile`, both of which need bash 4+. CI runners ship
# bash 5, but macOS still ships bash 3.2 as /bin/bash, where the script dies on `declare -A` with a
# misleading "invalid option" error. Re-exec under a newer bash when one is on PATH (Homebrew
# installs it as /opt/homebrew/bin/bash) so the script is usable locally, which is what the
# e2e-pr-check agent skill and anyone debugging test selection needs.
if [ -z "${DETERMINE_RELEVANT_TESTS_REEXEC:-}" ] && [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
for candidate in "$(command -v bash || true)" /opt/homebrew/bin/bash /usr/local/bin/bash; do
# shellcheck disable=SC2016 # single quotes are required: the expansion must happen in the candidate shell
if [ -x "$candidate" ] && [ "$("$candidate" -c 'echo ${BASH_VERSINFO[0]}')" -ge 4 ]; then
DETERMINE_RELEVANT_TESTS_REEXEC=1 exec "$candidate" "${BASH_SOURCE[0]}" "$@"
fi
done
echo "ERROR: this script needs bash 4 or newer (found $BASH_VERSION)." >&2
echo "On macOS: brew install bash" >&2
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MAPPING_FILE="$SCRIPT_DIR/e2e-test-mapping.json"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
Expand Down
19 changes: 19 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "artemis",
"description": "Agent skills for developing Artemis, the interactive learning platform.",
"owner": {
"name": "Artemis Team",
"url": "https://github.com/ls1intum/Artemis"
},
"plugins": [
{
"name": "artemis",
"source": "./",
"description": "Agent skills for developing Artemis: run the E2E tests a change affects, triage red CI, write migrations and tests that pass first time, and follow the server and client conventions the build enforces.",
"category": "development",
"homepage": "https://docs.artemis.tum.de/developer/work-with-ai",
"repository": "https://github.com/ls1intum/Artemis",
"license": "MIT"
}
]
}
13 changes: 13 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "artemis",
"description": "Agent skills for developing Artemis: run the E2E tests a change affects, triage red CI, write migrations and tests that pass first time, and follow the server and client conventions the build enforces.",
"version": "1.0.0",
"author": {
"name": "Artemis Team",
"url": "https://github.com/ls1intum/Artemis"
},
"homepage": "https://docs.artemis.tum.de/developer/work-with-ai",
"repository": "https://github.com/ls1intum/Artemis",
"license": "MIT",
"keywords": ["artemis", "spring-boot", "angular", "playwright", "liquibase", "archunit"]
}
42 changes: 42 additions & 0 deletions .github/workflows/ci-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Agent Skills

# Validates the agent skills in skills/. Kept out of ci-quality.yml on purpose: that workflow is
# gated on `build_relevant`, which is false when every changed file is markdown, so a PR editing
# only skills/ would skip the one check that guards it.

on:
workflow_call:
inputs:
commit_sha:
description: 'SHA to check. PR runs use the PR head SHA (matching ci-build.yml).'
required: true
type: string

permissions: {} # default-deny; the job grants only what it needs

jobs:
skill-references:
name: Skill Path References
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
# A pure source scan (no git diff), so pin the PR head the way the style jobs do.
ref: ${{ inputs.commit_sha }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
# The fence state machine decides which lines count as code, so a regression there silently
# stops the check below from seeing example commands. Guard it before trusting the scan.
- name: Checker self-test
run: python supporting_scripts/check_skill_references.py --self-test
# A skill that cites a file which has since moved is worse than no skill: an agent acts on it
# without checking. This is what keeps the citations honest as the code around them changes.
- name: Skill path references
if: ${{ !cancelled() }}
run: python supporting_scripts/check_skill_references.py
5 changes: 5 additions & 0 deletions .github/workflows/ci-workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ jobs:
"has_gradle": {".github/workflows/ci.yml", ".github/workflows/ci-gradle-wrapper.yml"},
"has_version": {".github/workflows/ci.yml", ".github/workflows/ci-version-consistency.yml", "supporting_scripts/update_version.sh"},
"has_beans": {".github/workflows/ci.yml", ".github/workflows/ci-bean-instantiations.yml"},
"has_skills": {
".github/workflows/ci.yml",
".github/workflows/ci-skills.yml",
"supporting_scripts/check_skill_references.py",
},
}

failed = False
Expand Down
24 changes: 23 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
has_workflows: ${{ steps.decide.outputs.has_workflows }}
has_version: ${{ steps.decide.outputs.has_version }}
has_beans: ${{ steps.decide.outputs.has_beans }}
has_skills: ${{ steps.decide.outputs.has_skills }}
has_tum_ui: ${{ steps.decide.outputs.has_tum_ui }}
steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -100,6 +101,11 @@ jobs:
- 'src/main/resources/**'
- 'build.gradle'
- 'gradle/**'
has_skills:
- '.github/workflows/ci.yml'
- '.github/workflows/ci-skills.yml'
- 'skills/**'
- 'supporting_scripts/check_skill_references.py'
has_docs:
- '.github/workflows/ci.yml'
- '.github/workflows/ci-docs.yml'
Expand Down Expand Up @@ -166,11 +172,12 @@ jobs:
F_WORKFLOWS: ${{ steps.area.outputs.has_workflows }}
F_VERSION: ${{ steps.area.outputs.has_version }}
F_BEANS: ${{ steps.area.outputs.has_beans }}
F_SKILLS: ${{ steps.area.outputs.has_skills }}
F_TUM_UI: ${{ steps.area.outputs.has_tum_ui }}
run: |
set -Eeuo pipefail
if [ "$FORCE" = "true" ]; then
for k in build_relevant has_java has_docs has_i18n has_gradle has_workflows has_version has_beans has_tum_ui; do
for k in build_relevant has_java has_docs has_i18n has_gradle has_workflows has_version has_beans has_skills has_tum_ui; do
echo "$k=true" >> "$GITHUB_OUTPUT"
done
else
Expand All @@ -183,6 +190,7 @@ jobs:
echo "has_workflows=${F_WORKFLOWS:-false}"
echo "has_version=${F_VERSION:-false}"
echo "has_beans=${F_BEANS:-false}"
echo "has_skills=${F_SKILLS:-false}"
echo "has_tum_ui=${F_TUM_UI:-false}"
} >> "$GITHUB_OUTPUT"
fi
Expand Down Expand Up @@ -568,6 +576,18 @@ jobs:
with:
commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}

# Not folded into `quality`: that job is gated on `build_relevant`, which is false when every
# changed file is markdown. A PR editing only skills/ would then skip the check that guards it.
skills:
name: Agent Skills
needs: detect-changes
if: needs.detect-changes.outputs.has_skills == 'true'
uses: ./.github/workflows/ci-skills.yml
permissions:
contents: read
with:
commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}

# Required for internal PRs — part of the `all-required-ci-passed` gate (a test that fails all
# its retries blocks the merge; flaky-recovered runs stay green). Depends on `build` for the
# Docker image, not on `test`, so a flaky unit-test run never blocks it; runs only when `build`
Expand Down Expand Up @@ -651,6 +671,7 @@ jobs:
- workflows
- version-consistency
- bean-instantiations
- skills
- e2e
runs-on: ubuntu-latest
permissions: {} # reads only the `needs` context; no checkout, no token scope
Expand Down Expand Up @@ -717,6 +738,7 @@ jobs:
- translation
- version-consistency
- bean-instantiations
- skills
- e2e
- codeql
- coverage-report
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ deferredEagerBeanInstantiationViolations.dot
######################
/src/test/playwright/monocart-report
/venv/
# Running any supporting_scripts/*.py leaves one of these behind
__pycache__/
/src/test/playwright/test-exercise-repos
/docker/playwright-multinode-arch-override.yml
/docker/playwright-multinode-middleware-override.yml
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Repository Guidelines

Before making changes, read and follow **[CLAUDE.md](./CLAUDE.md)** in full; it is the single source of truth for AI coding assistants working in this repository.

For task-specific procedures, install the repository's agent skills with `npx skills add ls1intum/Artemis`. They cover running the E2E tests a change affects, triaging a red build, database migrations, the server architecture gates, the client conventions, writing tests, and local setup. See [`skills/`](./skills/) and [`documentation/docs/developer/work-with-ai.mdx`](./documentation/docs/developer/work-with-ai.mdx).
32 changes: 27 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Agent skills

This file holds **facts** about the repository. **Procedures** live in [`skills/`](./skills/) as agent skills, which load only when used and can therefore go into far more depth than this file can afford. Install them with `npx skills add ls1intum/Artemis`, or in Claude Code with `/plugin marketplace add ls1intum/Artemis` followed by `/plugin install artemis@artemis`.

- `e2e-pr-check` — run only the Playwright specs a change affects, and read the result correctly
- `ci-triage` — classify a red build before changing any code
- `server-arch-gates` — the architectural rules a server change must satisfy, and how to check each locally
- `liquibase-migration` — write a changelog that survives a rolling deploy on both databases
- `client-conventions` — Angular signal APIs, cloning, template control flow, TUM UI styling
- `write-tests` — base class selection and the test commands that silently do the wrong thing
- `local-setup` — fresh clone to a running server and client

When a convention below changes, update the corresponding skill in the same pull request. See [`documentation/docs/developer/work-with-ai.mdx`](./documentation/docs/developer/work-with-ai.mdx).

## Project Overview

Artemis is an interactive learning platform for programming exercises, quizzes, modeling tasks, and exams with automatic and manual assessment. It integrates with AI services (Iris for virtual tutoring, Athena for automated assessment, Hyperion for exercise creation).
Expand Down Expand Up @@ -67,17 +81,24 @@ pnpm run prettier:write # Fix formatting

# Client (Vitest - preferred for new tests)
pnpm run vitest # Watch mode
pnpm run vitest:run # Single run
pnpm run vitest:run # Single run, whole suite
pnpm run vitest:coverage # With coverage
pnpm run vitest -- path/to/spec.ts # Single Vitest file
pnpm exec vitest run path/to/spec.ts # Single Vitest file
# NOT `pnpm run vitest:run -- path/to/spec.ts`: the path is not forwarded as a filter and the
# whole suite runs (1298 files instead of 1). Use `pnpm exec vitest run <path>` as shown above.

# E2E Tests (Playwright) — preferred way to run locally
# The script auto-kills processes on ports 8080/9000/7921, starts Postgres, server, and client.
./run-e2e-tests-local-fast.sh # Run all E2E tests
./run-e2e-tests-local-fast.sh --filter "Quiz" # Run tests matching "Quiz"
./run-e2e-tests-local-fast.sh --filter "ExamAssessment|SystemHealth" # Multiple patterns
./run-e2e-tests-local-fast.sh --specs "e2e/exam/ExamResults.spec.ts e2e/lecture/" # Only these spec paths
./run-e2e-tests-local-fast.sh --stop # Stop all services

# --filter is Playwright --grep (matches test TITLES); --specs replaces the spec PATHS that run.
# For "only what my branch changed", resolve the paths first with the same script CI uses:
./.ci/E2E-tests/determine-relevant-tests.sh origin/develop # prints RELEVANT_TESTS=...

# Multi-node E2E (catches cluster / cache coherence regressions)
# Boots the full production-faithful stack: Postgres, JHipster Registry (Eureka),
# ActiveMQ, 3 Artemis nodes, nginx LB, containerised Playwright. Slower than the
Expand All @@ -94,6 +115,7 @@ pnpm run vitest -- path/to/spec.ts # Single Vitest file
# containers. Use this for server-side iteration on multi-node bugs. Cold ~1–2 min, warm ~30 s.
./run-e2e-tests-local-multinode-fast.sh # Full run (build WAR + infra + 3 host JVMs + tests)
./run-e2e-tests-local-multinode-fast.sh --filter "Quiz" # Filter to a subset of tests
./run-e2e-tests-local-multinode-fast.sh --specs "e2e/exam/" # Only these spec paths
./run-e2e-tests-local-multinode-fast.sh --middleware redis # Same suite, Redis instead of Hazelcast
./run-e2e-tests-local-multinode-fast.sh --skip-build --skip-up # Re-run tests against the running stack
./run-e2e-tests-local-multinode-fast.sh --stop # Tear everything down
Expand Down Expand Up @@ -200,7 +222,7 @@ Organized by feature module:
### Caching

- **Do not add `@Cache` (Hibernate L2) annotations on entities or associations.** Hibernate second-level cache is disabled cluster-wide and an ArchUnit rule (`ArchitectureTest.testNoHibernateSecondLevelCacheAnnotation`) fails the build if any reappears. Reason: `@Modifying @Query` repository methods bypass L2 invalidation, and the absence of service-level `@Transactional` leaves no clean place to coordinate eviction within a REST call — both produced cross-node stale-read bugs in the multi-node cluster (issue #12574, fixed in PR #12578; further cleanup in PR #12579).
- **For DTO / projection caching, use Spring `@Cacheable`.** It resolves against the `RoutingCacheManager` in `core/config/cache/CacheManagerConfiguration`, which serves blob caches (`files`, `plantUmlPng`, `plantUmlSvg`) from a bounded per-node Caffeine cache and every other cache from the distributed data provider. Always pair `@Cacheable` with explicit eviction — `@CacheEvict` on the writer service, or a Hibernate `PostUpdateEventListener` / `PostDeleteEventListener`. See `TitleCacheEvictionService` for the canonical pattern, and `BlobCacheEvictionService` for evicting a per-node blob cache across the cluster.
- **For DTO / projection caching, use Spring `@Cacheable`.** It resolves against the `RoutingCacheManager` in `core/config/cache/CacheManagerConfiguration`, which serves the per-node caches from a bounded Caffeine cache and every other cache from the distributed data provider. The per-node ones are the blobs of `BlobCacheConfiguration` (`files`, `plantUmlPng`, `plantUmlSvg`) and the titles of `TitleCacheConfiguration`; both expire entries after a TTL, so a cache whose staleness would be visible for long belongs in the distributed manager instead. Always pair `@Cacheable` with explicit eviction — `@CacheEvict` on the writer service, or a Hibernate `PostUpdateEventListener` / `PostDeleteEventListener`. See `TitleCacheEvictionService` for the canonical pattern, and `PerNodeCacheEvictionService` for propagating a per-node eviction across the cluster.
- The bar for adding a new cache: a measured performance gain that justifies the eviction-correctness work. The default answer is: do not cache.
- Full rationale, history, and patterns: `documentation/docs/developer/guidelines/caching.mdx`.

Expand All @@ -227,7 +249,7 @@ Organized by feature module:
- Use `inject()` for dependency injection instead of constructor injection
- Legacy decorators (`@Input`, `@Output`, `@ViewChild`, `@ViewChildren`, `@ContentChild`, `@ContentChildren`) must not be used in new code
- In modules not yet fully migrated, prefer signal-based APIs for new components but maintain consistency within existing components
- An ESLint rule (`enforce-signal-apis-in-migrated-modules`) enforces this in fully migrated modules
- An ESLint rule (`localRules/enforce-signal-apis`, in `rules/enforce-signal-apis.mjs`) enforces this in fully migrated modules
- **`ngOnChanges` is banned — use `computed()`/`effect()` instead.** An error-level rule (`localRules/prefer-signal-reactivity-over-ngonchanges`) enforces this across `src/main/webapp/app`, `packages/tum-ui/src/lib`, and `src/test/javascript`, including specs and undecorated base classes. Angular 21 does call inherited `ngOnChanges` hooks and fires them for signal inputs, so this is a consistency ban rather than a correctness fix. A genuinely unavoidable use of `SimpleChanges.previousValue`/`isFirstChange()` or pre-child-initialization ordering needs a detailed comment and a justified line-level `eslint-disable-next-line`. `ngOnInit` and `ngOnDestroy` are unaffected. See `documentation/docs/developer/guidelines/client-development.mdx`.
- **Angular template control flow: use `@if`, `@for`, `@switch`; never use `*ngIf`, `*ngFor`, `*ngSwitch`**
- Avoid `null`, use `undefined` where possible
Expand Down Expand Up @@ -280,7 +302,7 @@ Organized by feature module:
## Commit & PR Guidelines

- Concise, imperative commit messages scoped where useful (e.g. Exam mode: adjust live updates, build: bump version); wrap bodies near 72 chars. Commit messages contain no backticks
- **A PR title wraps the module name in literal backticks and follows it with a colon:** ``​`Development`: Improve documentation``. Only the module before the colon is wrapped, never the whole title and never the text after the colon. The backticks are characters in the title rather than markdown, so quote the title with single quotes so the shell leaves them alone: `gh pr create --title '`Development`: Improve documentation'`. The allowed module names and the exact pattern live in `.github/workflows/validate-pr-title.yml`, and `validate-pr-title` fails the PR when the title does not match. Do not infer the format from `git log`: GitHub strips the backticks when it squashes, so merged subjects read `Development: ...` without them
- **A PR title wraps the module name in literal backticks and follows it with a colon:** ``​`Development`: Improve documentation``. Only the module before the colon is wrapped, never the whole title and never the text after the colon. The backticks are characters in the title rather than markdown, so quote the title with single quotes so the shell leaves them alone: ``gh pr create --title '`Development`: Improve documentation'``. The allowed module names and the exact pattern live in `.github/workflows/validate-pr-title.yml`, and `validate-pr-title` fails the PR when the title does not match. Do not infer the format from `git log`: GitHub strips the backticks when it squashes, so merged subjects read `Development: ...` without them
- PRs: include problem/solution summary, linked issue, commands/tests run, screenshots for UI, and doc updates if relevant
- Target `develop` branch; rebase to reduce noise
- Run lint and tests before submitting
Expand Down
Loading
Loading