This is an automated email from the ASF dual-hosted git repository.
jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new bb6ed268ed [ZEPPELIN-6405] Add AGENTS.md for AI coding agent guidance
bb6ed268ed is described below
commit bb6ed268edd89019f6e17fcaf9f40e8b041ca15e
Author: Jongyoul Lee <[email protected]>
AuthorDate: Fri May 8 11:06:30 2026 +0900
[ZEPPELIN-6405] Add AGENTS.md for AI coding agent guidance
## Summary
- Add comprehensive `AGENTS.md` following the [open
standard](https://github.com/anthropics/agents-md) to help AI coding agents
understand and work effectively with the Zeppelin codebase
- Covers module architecture, server-interpreter Thrift IPC communication,
plugin system with custom classloading, reflection patterns, interpreter
lifecycle, and contributing guide
- Build/test instructions kept concise; focus on deep architectural context
## Test plan
- [ ] Verify `AGENTS.md` renders correctly on GitHub
- [ ] Verify RAT license check passes (`./mvnw clean
org.apache.rat:apache-rat-plugin:check -Prat`)
Closes #5187 from jongyoul/ZEPPELIN-6405-agents-md.
Signed-off-by: Jongyoul Lee <[email protected]>
---
.gitignore | 22 +++
AGENTS.md | 561 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 583 insertions(+)
diff --git a/.gitignore b/.gitignore
index a77b3b5db6..25319152ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -149,3 +149,25 @@ tramp
# dotenv files
.env
+
+# AI coding agents — personal config (AGENTS.md is shared, these are not)
+CLAUDE.md
+GEMINI.md
+.claude/
+.gemini/
+.codex/
+.cursor/
+.cursorules
+.cursorrules
+.windsurf/
+.windsurfrules
+.cline/
+.clinerules
+.continue/
+.aider*
+.augment/
+.amazonq/
+.junie/
+.goose/
+.roo/
+.rooignore
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..085f8213db
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,561 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# AGENTS.md
+
+> Guidance for AI coding agents working on the Apache Zeppelin codebase.
+> See [AGENTS.md specification](https://github.com/agentsmd/agents.md).
+
+## Project Overview
+
+Apache Zeppelin is a web-based notebook for interactive data analytics. It
provides a unified interface to multiple data processing backends (Spark,
Flink, Python, JDBC, etc.) through a pluggable interpreter architecture. Each
interpreter runs in its own JVM process and communicates with the server via
Apache Thrift RPC.
+
+- **Language**: Java 11, Scala 2.12
+- **Build**: Maven multi-module (wrapper: `./mvnw`)
+- **Frontend**: Angular 13 (Node 18) in `zeppelin-web-angular/`
+- **Version**: 0.13.0-SNAPSHOT
+
+## Build & Test
+
+```bash
+# Full build (skip tests)
+./mvnw clean package -DskipTests
+
+# Build single module (--am builds required upstream modules)
+./mvnw clean package -pl zeppelin-server --am -DskipTests
+
+# Run module tests
+./mvnw test -pl zeppelin-interpreter --am
+
+# Run single test class/method
+./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest
+./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod
+
+# Common profiles
+# -Pspark-3.5 -Pspark-scala-2.12 Spark version
+# -Pflink-117 Flink version
+# -Pbuild-distr Full distribution
+# -Prat Apache RAT license check
+# -Pweb-classic Additionally builds the classic UI web
module when specified
+```
+
+## Build Gotchas
+
+### Shaded JAR Rebuild Chain
+
+The most common build mistake: modifying `zeppelin-interpreter` without
rebuilding `zeppelin-interpreter-shaded`. The shaded JAR is an uber JAR that
all interpreter processes use. If it's stale, you get `ClassNotFoundException`
or `NoSuchMethodError` at runtime.
+
+```bash
+# After changing zeppelin-interpreter, ALWAYS rebuild in order:
+./mvnw clean package -pl zeppelin-interpreter -DskipTests
+./mvnw clean package -pl zeppelin-interpreter-shaded -DskipTests
+# Then rebuild affected interpreter modules
+
+# Shorthand:
+./mvnw clean package -pl zeppelin-interpreter,zeppelin-interpreter-shaded
-DskipTests
+```
+
+The shaded JAR is also copied to `interpreter/` directory by
maven-antrun-plugin after packaging. If this directory has a stale JAR,
interpreter processes will load old code.
+
+### Module Build Order
+
+Maven modules are ordered in the root `pom.xml`. Key sequence:
+```
+zeppelin-interpreter → zeppelin-interpreter-shaded → zeppelin-server
+```
+
+All interpreter modules build after `zeppelin-interpreter-shaded`. A second
shading chain exists for Jupyter:
+```
+zeppelin-jupyter-interpreter → zeppelin-jupyter-interpreter-shaded → python,
rlang
+```
+
+## Module Architecture
+
+### Dependency Flow
+
+```
+zeppelin-interpreter Base API: Interpreter, InterpreterContext,
Thrift services
+ ↓
+zeppelin-interpreter-shaded Uber JAR (maven-shade-plugin, relocated packages)
+ ↓
+zeppelin-server Core engine + Jetty 11, REST/WebSocket APIs, HK2
DI, entry point
+```
+
+### Core Modules
+
+#### `zeppelin-interpreter/`
+The base framework that all interpreters depend on. Defines the interpreter
API and the Thrift communication protocol. This module is shaded into an uber
JAR (`zeppelin-interpreter-shaded`) and placed on each interpreter process's
classpath.
+
+Key classes:
+- `Interpreter` (abstract) / `AbstractInterpreter` — base class every
interpreter extends
+- `InterpreterContext` — carries notebook/paragraph/user info into
`interpret()` calls
+- `InterpreterGroup` — manages a group of interpreter instances sharing one
process
+- `InterpreterResult` / `InterpreterOutput` — execution result model
+- `RemoteInterpreterServer` — **entry point of each interpreter JVM process**;
implements the Thrift `RemoteInterpreterService` server; receives RPC calls
from zeppelin-server
+- `InterpreterLauncher` (abstract) — how an interpreter process is started
(Standard, Docker, K8s, YARN)
+- `LifecycleManager` — manages interpreter process lifecycle (Null = keep
alive, Timeout = idle shutdown)
+- `DependencyResolver` / `AbstractDependencyResolver` — Maven artifact
resolution for `%dep` paragraphs
+
+Thrift definitions (`src/main/thrift/`):
+- `RemoteInterpreterService.thrift` — server → interpreter RPCs
+- `RemoteInterpreterEventService.thrift` — interpreter → server event callbacks
+
+#### `zeppelin-server/`
+The entry point and core of the Zeppelin application. Combines the web server
/ API layer with the core notebook engine, interpreter lifecycle management,
scheduling, search, and plugin loading.
+
+Web / API layer (`org.apache.zeppelin.server`, `rest`, `socket`):
+- `ZeppelinServer` — `main()`, embedded Jetty 11 server, HK2 DI setup
+- `NotebookRestApi`, `InterpreterRestApi`, `SecurityRestApi`,
`ConfigurationsRestApi` — REST endpoints in `org.apache.zeppelin.rest`
+- `NotebookServer` — WebSocket endpoint (`/ws`) for real-time notebook
operations and paragraph execution
+- `RemoteInterpreterEventServer` — Thrift server receiving callbacks from
interpreter processes (output streaming, status updates)
+
+Engine / runtime (`org.apache.zeppelin.notebook`, `interpreter`, `scheduler`,
`search`, `plugin`, `storage`, `conf`):
+- `Notebook` / `Note` / `Paragraph` — notebook data model and execution
+- `InterpreterFactory` — creates interpreter instances
+- `InterpreterSettingManager` — loads `interpreter-setting.json` from each
interpreter directory, manages interpreter configurations
+- `InterpreterSetting` — one interpreter's config + runtime state; creates
`InterpreterLauncher` and `RemoteInterpreterProcess`
+- `ManagedInterpreterGroup` — server-side `InterpreterGroup` implementation;
owns the `RemoteInterpreterProcess`
+- `NoteManager` — notebook CRUD, folder tree
+- `SchedulerService` — Quartz-based cron scheduling
+- `SearchService` — Lucene-based notebook search
+- `PluginManager` — loads launcher and notebook-repo plugins (custom
classloading, not Java SPI)
+- `ZeppelinConfiguration` — config management (env vars → system properties →
`zeppelin-site.xml` → defaults)
+- `RecoveryStorage` — persists interpreter process info for server-restart
recovery
+- `ConfigStorage` — persists interpreter settings to JSON
+
+#### `zeppelin-interpreter-shaded/`
+Uses maven-shade-plugin to package `zeppelin-interpreter` + dependencies into
an uber JAR with relocated packages (e.g., `org.apache.thrift` →
`org.apache.zeppelin.shaded.org.apache.thrift`). This JAR is placed on each
interpreter process's classpath.
+
+#### `zeppelin-client/`
+REST/WebSocket client library for programmatic access to Zeppelin.
+
+### Interpreter Modules
+
+Each interpreter is an independent Maven module inheriting from
`zeppelin-interpreter-parent`:
+
+| Module | Description |
+|--------|-------------|
+| `spark/` | Apache Spark (Scala/Python/R/SQL) — most complex interpreter |
+| `python/` | IPython/Python |
+| `flink/` | Apache Flink (Scala/Python/SQL) |
+| `jdbc/` | JDBC (PostgreSQL, MySQL, Hive, etc.) |
+| `shell/` | Bash/Shell commands |
+| `markdown/` | Markdown rendering (Flexmark) |
+| `java/` | Java interpreter |
+| `groovy/` | Groovy |
+| `neo4j/` | Neo4j Cypher |
+| `mongodb/` | MongoDB |
+| `elasticsearch/` | Elasticsearch |
+| `bigquery/` | Google BigQuery |
+| `cassandra/` | Apache Cassandra CQL |
+| `hbase/` | Apache HBase |
+| `rlang/` | R language |
+| `livy/` | Apache Livy (remote Spark) |
+| `sparql/` | SPARQL queries |
+| `influxdb/` | InfluxDB |
+| `file/` | HDFS/local file browser |
+| `alluxio/` | Alluxio file system |
+
+### Plugin Modules (`zeppelin-plugins/`)
+
+**Launcher plugins** (`launcher/`) — how interpreter processes are started:
+- `StandardInterpreterLauncher` (builtin) — local JVM process via
`bin/interpreter.sh`
+- `SparkInterpreterLauncher` (builtin) — Spark-specific launcher with
`spark-submit`
+- `DockerInterpreterLauncher` — Docker container
+- `K8sStandardInterpreterLauncher` — Kubernetes pod
+- `YarnInterpreterLauncher` — YARN container
+- `FlinkInterpreterLauncher` — Flink-specific
+- `ClusterInterpreterLauncher` — Zeppelin cluster mode
+
+**NotebookRepo plugins** (`notebookrepo/`) — where notebooks are persisted:
+- `VFSNotebookRepo` (builtin) — local filesystem (Apache VFS)
+- `GitNotebookRepo` (builtin) — local git repo
+- `GitHubNotebookRepo` — GitHub
+- `S3NotebookRepo` — Amazon S3
+- `GCSNotebookRepo` — Google Cloud Storage
+- `AzureNotebookRepo` — Azure Blob Storage
+- `MongoNotebookRepo` — MongoDB
+- `OSSNotebookRepo` — Alibaba Cloud OSS
+
+### Frontend
+
+- `zeppelin-web-angular/` — Angular 13, Node 18 (active frontend)
+- `zeppelin-web/` — Legacy AngularJS (activated with `-Pweb-classic`)
+
+### Configuration Files
+
+| File | Purpose |
+|------|---------|
+| `conf/zeppelin-site.xml` | Main server config (port, SSL, notebook storage,
interpreter settings). Copy from `.template` |
+| `conf/zeppelin-env.sh` | Shell environment (JAVA_OPTS, memory, Spark
master). Copy from `.template` |
+| `conf/shiro.ini` | Authentication/authorization (users, roles, LDAP,
Kerberos, PAM). Copy from `.template` |
+| `conf/interpreter.json` | Runtime interpreter settings — **auto-generated**,
do not edit manually |
+| `conf/log4j2.properties` | Logging configuration |
+| `conf/interpreter-list` | Static list of available interpreters with Maven
coordinates |
+| `{interpreter}/resources/interpreter-setting.json` | Interpreter defaults
(build-time, bundled in JAR) |
+
+`conf/*.template` files are the source of truth. Actual config files
(`zeppelin-site.xml`, `shiro.ini`, etc.) are `.gitignored`.
+
+### Module Boundaries
+
+Where new code should go:
+
+| If the code... | Put it in |
+|----------------|-----------|
+| Is a base interface/class that all interpreters need |
`zeppelin-interpreter` |
+| Handles notebook state, interpreter lifecycle, scheduling, search,
REST/WebSocket, or authentication realm | `zeppelin-server` |
+| Is specific to one backend (Spark, Flink, JDBC, etc.) | That interpreter's
module |
+| Is a new way to launch interpreter processes | `zeppelin-plugins/launcher/` |
+| Is a new notebook storage backend | `zeppelin-plugins/notebookrepo/` |
+
+**Important**: Code added to `zeppelin-interpreter` is exposed to **every
interpreter process** via the shaded JAR. Only add code there if all
interpreters genuinely need it.
+
+## Server–Interpreter Communication
+
+Zeppelin's most important architectural concept: the server and each
interpreter run in **separate JVM processes** communicating via **Apache Thrift
RPC**. This provides isolation, fault tolerance, and the ability to run
interpreters on remote hosts or containers.
+
+### Thrift Code Generation
+
+The `.thrift` files are in `zeppelin-interpreter/src/main/thrift/`. Generated
Java files are **checked into git** (not generated at build time) in
`zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/`.
+
+To regenerate after modifying `.thrift` files:
+```bash
+cd zeppelin-interpreter/src/main/thrift
+./genthrift.sh # requires 'thrift' compiler (v0.13.0) installed locally
+```
+
+The script runs the Thrift compiler, prepends ASF license headers, and moves
files to the source tree. **Never edit the generated Java files directly** —
changes will be lost on next regeneration.
+
+### Thrift IPC — Bidirectional
+
+**Server → Interpreter** (`RemoteInterpreterService`):
+```
+init(properties) — initialize interpreter process with
config
+createInterpreter(className, ...) — instantiate an interpreter class
+open(sessionId, className) — open/initialize an interpreter
+interpret(sessionId, className, code, context) — execute code (core method)
+cancel(sessionId, className, ...) — cancel running execution
+getProgress(sessionId, className) — poll execution progress (0-100)
+completion(sessionId, className, buf, cursor) — code completion
+close(sessionId, className) — close an interpreter
+shutdown() — terminate the interpreter process
+```
+
+**Interpreter → Server** (`RemoteInterpreterEventService`):
+```
+registerInterpreterProcess(info) — register after process startup
+appendOutput(event) — stream execution output incrementally
+updateOutput(event) — replace output content
+sendParagraphInfo(info) — update paragraph metadata
+updateAppStatus(event) — Zeppelin Application status
+runParagraphs(request) — trigger paragraph execution from
interpreter
+getResource(resourceId) — access ResourcePool shared state
+getParagraphList(noteId) — query notebook structure
+```
+
+### Paragraph Execution Chain
+
+When a user runs a paragraph, the full call chain is:
+
+```
+User clicks "Run" in browser
+ → WebSocket message to NotebookServer
+ → NotebookServer.runParagraph()
+ → Notebook.run()
+ → Paragraph.execute()
+ → RemoteInterpreter.interpret(code, context)
+ → RemoteInterpreterProcess.callRemoteFunction()
+ → [Thrift RPC over TCP]
+ → RemoteInterpreterServer.interpret()
+ → actual Interpreter.interpret() (e.g. SparkInterpreter)
+ → result returned via Thrift
+ → meanwhile: interpreter calls appendOutput() to stream partial
results back
+```
+
+### Interpreter Launch Chain
+
+When an interpreter process needs to be started:
+
+```
+RemoteInterpreter.interpret() [first call triggers launch]
+ → ManagedInterpreterGroup.getOrCreateInterpreterProcess()
+ → InterpreterSetting.createInterpreterProcess()
+ → InterpreterSetting.createLauncher(properties)
+ → PluginManager.loadInterpreterLauncher(launcherPlugin)
+ → [builtin: Class.forName() / external: URLClassLoader]
+ → InterpreterLauncher.launch(context)
+ → new ExecRemoteInterpreterProcess(...)
+ → ExecRemoteInterpreterProcess.start()
+ → ProcessBuilder → "bin/interpreter.sh"
+ → java -cp ... RemoteInterpreterServer [new JVM]
+ → RemoteInterpreterServer.main()
+ → registerInterpreterProcess() callback to server
+```
+
+### Interpreter Process Lifecycle
+
+1. **Launch**: Server creates `RemoteInterpreterProcess` via launcher plugin
+2. **Start**: Process starts as separate JVM (`bin/interpreter.sh` →
`RemoteInterpreterServer.main()`)
+3. **Register**: Process calls `registerInterpreterProcess()` back to server's
`RemoteInterpreterEventServer`
+4. **Init**: Server calls `init(properties)` — passes all configuration as a
flat `Map<String, String>`
+5. **Create**: Server calls `createInterpreter(className, properties)` —
instantiates interpreter via reflection
+6. **Open**: First `interpret()` triggers `LazyOpenInterpreter.open()` —
interpreter initializes resources
+7. **Execute**: `interpret(code, context)` — runs code; partial output streams
via `appendOutput()` events
+8. **Shutdown**: `close()` → `shutdown()` → JVM exits
+9. **Recovery**: `RecoveryStorage` persists process info; on server restart,
reconnects to surviving processes
+
+### InterpreterGroup Scoping
+
+`InterpreterOption` controls process isolation via `perNote` and `perUser`
settings:
+
+| perNote | perUser | Behavior |
+|---------|---------|----------|
+| `shared` | `shared` | All users share one process (default) |
+| `scoped` | `shared` | Separate interpreter instance per note, same process |
+| `isolated` | `shared` | Separate process per note |
+| `shared` | `scoped` | Separate interpreter instance per user, same process |
+| `shared` | `isolated` | Separate process per user |
+| `scoped` | `scoped` | Separate instance per user+note |
+| `isolated` | `isolated` | Separate process per user+note (full isolation) |
+
+## Plugin System & Reflection Patterns
+
+### PluginManager — Custom Classloading
+
+`PluginManager` (`zeppelin-server/.../plugin/PluginManager.java`) loads
plugins without Java SPI:
+
+```
+Plugin loading flow:
+1. Check builtin list (hardcoded class names):
+ - Launchers: StandardInterpreterLauncher, SparkInterpreterLauncher
+ - NotebookRepos: VFSNotebookRepo, GitNotebookRepo
+ → if builtin: Class.forName(className) — direct classloading
+
+2. If not builtin → external plugin:
+ → Scan pluginsDir/{Launcher|NotebookRepo}/{pluginName}/ for JARs
+ → Create URLClassLoader with those JARs
+ → classLoader.loadClass(className)
+ → Instantiate via reflection (constructor parameters)
+```
+
+External plugin directory structure:
+```
+plugins/
+ Launcher/
+ DockerInterpreterLauncher/
+ *.jar
+ K8sStandardInterpreterLauncher/
+ *.jar
+ NotebookRepo/
+ S3NotebookRepo/
+ *.jar
+ GCSNotebookRepo/
+ *.jar
+```
+
+### ReflectionUtils
+
+`ReflectionUtils` (`zeppelin-server/.../util/ReflectionUtils.java`) provides
generic reflection-based instantiation:
+
+```java
+// No-arg constructor
+ReflectionUtils.createClazzInstance(className)
+
+// Parameterized constructor
+ReflectionUtils.createClazzInstance(className, parameterTypes, parameters)
+```
+
+Used to instantiate:
+- `RecoveryStorage` — in `RemoteInterpreterServer` and
`InterpreterSettingManager`
+- `ConfigStorage` — in `InterpreterSettingManager`
+- `LifecycleManager` — in `RemoteInterpreterServer`
+- `NotebookRepo` — in `PluginManager`
+- `InterpreterLauncher` — in `PluginManager`
+
+### Interpreter Discovery
+
+`InterpreterSettingManager` discovers interpreters at startup:
+
+```
+1. Scan interpreterDir (default: interpreter/) for subdirectories
+2. For each subdirectory, look for interpreter-setting.json
+3. Parse JSON → List<RegisteredInterpreter>
+4. Register each interpreter's className, properties, editor settings
+```
+
+`interpreter-setting.json` format (in each interpreter module's resources):
+```json
+[{
+ "group": "spark",
+ "name": "spark",
+ "className": "org.apache.zeppelin.spark.SparkInterpreter",
+ "properties": {
+ "spark.master": { "defaultValue": "local[*]", "description": "Spark
master" }
+ },
+ "editor": { "language": "scala", "editOnDblClick": false }
+}]
+```
+
+### ZeppelinConfiguration Priority
+
+Configuration values are resolved in order (first match wins):
+1. **Environment variables** (e.g., `ZEPPELIN_HOME`, `ZEPPELIN_PORT`)
+2. **System properties** (e.g., `-Dzeppelin.server.port=8080`)
+3. **zeppelin-site.xml** (`conf/zeppelin-site.xml`)
+4. **Hardcoded defaults** (`ConfVars` enum in `ZeppelinConfiguration`)
+
+### HK2 Dependency Injection (zeppelin-server)
+
+`ZeppelinServer.startZeppelin()` sets up HK2 DI via
`ServiceLocatorUtilities.bind()`:
+
+```java
+new AbstractBinder() {
+ protected void configure() {
+ bind(storage).to(ConfigStorage.class);
+ bindAsContract(PluginManager.class).in(Singleton.class);
+ bindAsContract(InterpreterFactory.class).in(Singleton.class);
+
bindAsContract(NotebookRepoSync.class).to(NotebookRepo.class).in(Singleton.class);
+ bindAsContract(Notebook.class).in(Singleton.class);
+ // ... InterpreterSettingManager, SearchService, etc.
+ }
+}
+```
+
+REST API classes use `@Inject` to receive these singletons.
+
+## Contributing Guide
+
+### Prerequisites
+
+| Tool | Version | Notes |
+|------|---------|-------|
+| JDK | 11 | Required. Not 8, not 17 |
+| Maven | 3.6.3+ | Use the wrapper `./mvnw` — no separate install needed |
+| Node.js | 18.x | Only for frontend (`zeppelin-web-angular/`) |
+
+### Initial Setup
+
+```bash
+# Clone the repository
+git clone https://github.com/apache/zeppelin.git
+cd zeppelin
+
+# First build — skip tests to verify environment works
+./mvnw clean package -DskipTests
+# This takes ~10 minutes. If it succeeds, your environment is ready.
+
+# Frontend setup (only if working on UI)
+cd zeppelin-web-angular
+npm install
+cd ..
+```
+
+### Development Workflow
+
+When starting a new change, use a **git worktree** instead of switching
branches in your main checkout. This keeps your primary working directory clean
and allows parallel work across multiple branches:
+
+```bash
+# Create a worktree for your feature branch
+git worktree add ../zeppelin-ZEPPELIN-XXXX -b ZEPPELIN-XXXX-description
+cd ../zeppelin-ZEPPELIN-XXXX
+
+# When done, clean up
+git worktree remove ../zeppelin-ZEPPELIN-XXXX
+```
+
+```bash
+# Build only the module you're changing (--am builds required upstream modules)
+./mvnw clean package -pl zeppelin-server --am -DskipTests
+
+# Run tests for your module
+./mvnw test -pl zeppelin-server --am
+
+# Run a specific test
+./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod
+
+# Start the dev frontend (proxies API to localhost:8080)
+cd zeppelin-web-angular && npm start
+```
+
+For Spark or Flink work, add the version profile:
+```bash
+./mvnw clean package -pl spark -Pspark-3.5 -Pspark-scala-2.12 -DskipTests
+```
+
+### Before Submitting a PR
+
+1. **Write unit tests**. Every code change must include corresponding unit
tests. Bug fixes should include a test that reproduces the bug. New features
should have tests covering the main paths.
+
+2. **Run tests for affected modules**:
+ ```bash
+ ./mvnw test -pl <your-module>
+ ```
+
+3. **Check license headers** — all new files must have the Apache License 2.0
header:
+ ```bash
+ ./mvnw clean org.apache.rat:apache-rat-plugin:check -Prat
+ ```
+
+4. **Lint frontend changes** (if applicable):
+ ```bash
+ cd zeppelin-web-angular && npm run lint:fix
+ ```
+
+5. **Create a JIRA issue** at
[issues.apache.org/jira/browse/ZEPPELIN](https://issues.apache.org/jira/browse/ZEPPELIN)
and use the issue number in PR title: `[ZEPPELIN-XXXX] description`.
+
+### REST API Pattern
+
+All REST endpoints follow this pattern:
+
+```java
+@Path("/notebook")
+@Produces("application/json")
+@Singleton
+public class NotebookRestApi extends AbstractRestApi {
+ @Inject
+ public NotebookRestApi(Notebook notebook, ...) {
+ super(authenticationService);
+ }
+
+ @GET
+ @Path("/{noteId}")
+ @ZeppelinApi
+ public Response getNote(@PathParam("noteId") String noteId) {
+ // Authorization check
+ checkIfUserCanRead(noteId, "Insufficient privileges");
+ // Business logic via service layer
+ Note note = notebook.getNote(noteId);
+ // Return JsonResponse
+ return new JsonResponse<>(Status.OK, "", note).build();
+ }
+}
+```
+
+Key conventions:
+- Extend `AbstractRestApi` (provides `getServiceContext()` for auth)
+- Use `@Inject` constructor for HK2 DI
+- Annotate public methods with `@ZeppelinApi`
+- Return `JsonResponse<T>(status, message, body).build()`
+- Authorization via `checkIfUserCan{Read|Write|Run}()`
+
+### Code Style
+
+- **Java**: Google Java Style (2-space indent). Checkstyle enforced — no tabs,
LF line endings, newline at EOF
+- **Frontend**: ESLint + Prettier, auto-enforced via pre-commit hook (Husky +
lint-staged)
+- **Testing**: JUnit 5 (Jupiter) + Mockito (Java; a small number of legacy
JUnit 4 tests still exist), Playwright (frontend E2E)
+- **Logging**: SLF4J + Log4j2
+- **License**: Apache License 2.0 — all new files need the ASF header