# Contributing to MDMesh Thanks for being here. Code, docs, bug reports and ideas all help. This guide gets you from clone to a green PR. - 🐞 **Bug?** → [open a bug report](https://github.com/MDMesh-app/MDMesh/issues/new?template=bug_report.yml) - 💡 **Idea?** → [open a feature request](https://github.com/MDMesh-app/MDMesh/issues/new?template=feature_request.yml) - 🔧 **Code?** → read on, then send a focused PR. - **Security issue?** → see [SECURITY.md](SECURITY.md). Please don't file it publicly. --- ## Repository layout MDMesh is a monorepo with four planes (full map in **[STRUCTURE.md](STRUCTURE.md)**): | Path | Plane | Stack | |------|-------|-------| | `common/`, `server/`, `jwt/`, `notification/`, `plugins/` | Control plane (server / REST API) | Java · Jersey · Guice · MyBatis · PostgreSQL | | `web/` | Admin console | React · TypeScript · Vite | | `agent-android/` | Device agent | Kotlin · coroutines · WorkManager (Gradle) | | `supervisor/` | Updater / recovery service | Node (zero deps) | | `proto/`, `docs/adr/` | Protocol source of truth + decision records | — | Read the relevant ADR in [`docs/adr/`](docs/adr) before changing a load-bearing area (signing, device IDs, the agent contract, etc.). **Maintaining a fork?** Add console pages, tabs and sections in `web/src/extensions/` and server plugins in `plugins/fork/`, so you never edit core files: [web/EXTENDING.md](web/EXTENDING.md) is the guide. A change to `@mdmesh/console` or a slot contract in core follows its stability rules (one release of deprecation, a CHANGELOG line). --- ## Build and test You only need the toolchain for the plane you're touching; [docs/DEV.md](docs/DEV.md) sets it up and runs the dev stack. | Plane | Run from | Test | Type-check / build | |-------|----------|------|--------------------| | Console | `web/` | `npm test` (vitest; also [the runtime check](#dependency-updates)) | `npx tsc -b --noEmit` · `npm run build` | | Server | repo root | `mvn -pl common test` (fast, DB-free) | `mvn -pl server -am compile` · `mvn -pl server -am package -DskipTests` (the WAR) | | Agent | `agent-android/` | `./gradlew :proto:test :core:test` · `./gradlew detekt` | `./gradlew :app:compileDebugKotlin` · `./gradlew :app:assembleDebug` (debug APK) | | Supervisor | `supervisor/` | `node --test` | the image + smoke test, below | | Whole stack | repo root | `docker compose --env-file docker/dev.env up -d --build` and `scripts/dev-seed.sh`, then `scripts/agent-v1-e2e.sh http://localhost:8080` | — (the bring-up builds the images) | - **Console:** run `npm install` first. - **Server:** run `cp -n server/build.properties.example server/build.properties` once. A full server run needs PostgreSQL: use the dev stack. - **Supervisor image**, from the repo root: ```bash docker build -f docker/supervisor.Dockerfile -t mdmesh-supervisor:dev . \ && scripts/supervisor-smoke.sh mdmesh-supervisor:dev # boots the image the way compose runs it ``` - **Whole stack** is the dev stack. `./setup.sh` is the production installer instead (see [DEPLOY.md](DEPLOY.md)). --- ## Workflow 1. **Branch** off `main` (`feat/…`, `fix/…`, `docs/…`). Don't commit to `main` directly. 2. **Make focused changes** — one logical change per PR. Match the style of the code around you. 3. **Test what you touched**; add or update tests for behavior changes. 4. **Update docs** when you change behavior, config, or the API. 5. **Add a changelog line** under Unreleased in [CHANGELOG.md](CHANGELOG.md), in its section: one short bullet for the people running MDMesh, or "none" in the PR if they won't notice. Releases go out every three weeks ([release cycle](RELEASING.md#release-cycle)). Conflicts under Unreleased are expected: keep both lines. 6. **Open a PR** against `main` using the [PR template](.github/pull_request_template.md). ### CI | Tier | Workflow | What it runs | When | |------|----------|--------------|------| | **T0** | `t0-fast.yml` | Build + DB-free tests for each plane you touched (agent: detekt too), the supervisor smoke test, the edge (Caddy + compose) check | Every PR and push to `main` | | **T1** | `t1-e2e.yml` | The [Agent v1 end-to-end loop](docs/DEV.md#end-to-end-agent-loop-agent-v1) against a real server + Postgres, isolated from your dev stack | PRs and pushes to `main` that touch the server, `proto/`, `install/` or the dev stack | | **T2** | `t2-native.yml` | The [native rig](docs/DEV.md#t2-native-rig-native-install--upgrade): fresh install, upgrade, killed upgrade, on Debian 12, Debian 13 and Ubuntu 24.04 | PRs that touch the installers or the rig; nightly; by hand; before every release ([RELEASING.md](RELEASING.md)) | The exact triggers are the path filters in each workflow under `.github/workflows/` (the `paths` job in T0/T1, `on.pull_request.paths` in T2). In T0 and T1, a change under `.github/`, to a compose file, a lockfile or a `Dockerfile*` runs everything. New Kotlin must pass detekt; old findings are in `agent-android/config/detekt/baseline.xml`. A failed T1 uploads its logs as the `t1-e2e-logs` artifact, and a red T1 on `main` opens a tracking issue. ### Commit messages Short, imperative, prefixed: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`. Explain the *why* in the body when it isn't obvious. ### The one hard rule: the agent ↔ server contract is additive-only The `/agent/v1` contract must keep older agents working: **new request fields are optional and defaulted; never remove, rename or repurpose a field, and never add a required one.** A real break means a new `/agent/v2`. A golden contract test enforces this in CI ([ADR-0009](docs/adr/0009-agent-v1-contract-stability.md)). --- ## Dependency updates Dependabot opens grouped PRs every Monday ([`.github/dependabot.yml`](.github/dependabot.yml)). - **T0 green is not enough** for a runtime library crossing a minor/major. Also run that plane's runtime check: | Plane | Runtime check | |-------|---------------| | Web | `cd web && npm run build && cd ../scripts/shots && npm install && npx playwright install chromium && node capture.mjs --check` | | Server | the `t1-e2e` workflow (`agent-e2e` job) green on the PR | | Agent | install the APK on a device/emulator and complete a check-in | - **Ignored lines wait for their migration.** Each `ignore` names the migration that lifts it; remove it in that migration's PR, never piecemeal. The `agent-toolchain` group (AGP 9 and everything coupled to it) stays red until the AGP 9 migration and is never merged piecemeal. - **License changes** (e.g. Liquibase 5 → FSL) are blocked until reviewed against [ADR-0008](docs/adr/0008-licensing-and-rebrand.md). - **One JDK, one Node line.** The server is **JDK 17** everywhere. Node is the current LTS (today **24**); every place that names it (`web/package.json` `engines`, `@types/node`, both Node images, the native installer's `NODE_VER`) moves in one PR, a few months after the next even line becomes LTS. - **Base images float**, so rebuilds pick up patches. The edge's `caddy` image is pinned, so the Caddyfile parser only changes through a Dependabot PR that T0's `edge` check has passed. - **Vendored CI actions** (`.github/actions/ci-kit/`) change only by re-running ci-kit's `scripts/vendor.sh`. --- ## What makes a PR easy to merge - It's small and does one thing. - Tests pass for the affected plane; new behavior has a test. - UI changes include a screenshot or short clip. - Docs are updated alongside the code. - It respects the agent contract rule above and any relevant ADR. New here? Look for **good first issue**, or open a discussion before a large change. ### What to expect after you open something - A first maintainer reply within a week. If it's been longer, a polite ping is welcome. - CI doesn't run automatically on a **first-time contributor's** PR (GitHub's default); a maintainer approves it. - `main` is protected against force-pushes and deletion. Maintainers land small fixes on `main` directly; everything else goes through a PR. --- ## Regenerating the docs screenshots Every screenshot and diagram in the README comes from `docs/readme/`, and [scripts/readme/README.md](scripts/readme/README.md) is the one place that says how they are made. In short: `scripts/readme/generate.sh --build` builds the console and captures it against sample data. Both that generator and the full-page screenshots in `docs/screenshots/` (which the README doesn't use) render the real console through the harness in `scripts/shots/`. To redo the full-page ones: ```bash cd web && npm run build # build the SPA first cd ../scripts/shots && npm install && npx playwright install chromium && node capture.mjs ``` They come out in the light theme (`node capture.mjs --theme dark` for dark). Behaviour checks for every page run against the same fixtures: `node scripts/shots/v2-parity/run.mjs --build` from the repo root. `--with-fixture` adds a check of every extension slot in a build that has the test fixture extension (`web/src/ext/__fixtures__/demo`) as a fork's folder: `node scripts/shots/v2-parity/run.mjs --with-fixture extensions` runs only that one. The normal build never has the fixture. --- By contributing, you agree that your contributions are licensed under the project's [Apache-2.0 license](LICENSE).